Se agregaron nuevos componentes

This commit is contained in:
santiago
2025-10-14 21:48:31 +02:00
parent 91d25315e6
commit 0568e07ba1
8 changed files with 835 additions and 2 deletions
-2
View File
@@ -5,9 +5,7 @@ import React from "react";
export default function Layout({ children }: { children: React.ReactNode}) {
return (
<>
<Header />
{children}
<Footer />
</>
)
}
+50
View File
@@ -1,8 +1,58 @@
"use client"
import BarraProgreso from "@/components/alumno/barra-progreso";
import CompletarDatosPersonales from "@/components/alumno/completar-datos-personales";
import InformacinoServicio from "@/components/alumno/informacion-servicio";
import MensajeAlumno from "@/components/alumno/mensajes-alumno";
import PreTermino from "@/components/alumno/pre-termino";
export default function Home() {
return (
<div>
Hola Mundo
<BarraProgreso idStatus={5}/>
<CompletarDatosPersonales
idServicio={123}
alumno={{ token: { headers: { Authorization: "Bearer ..." } }, tokenArchivo: { headers: { Authorization: "Bearer ..." } } }}
imprimirMensaje={(msg) => console.log(msg)}
imprimirWarning={(msg, callback) => { if (confirm(msg)) callback(); }}
imprimirError={(err) => console.error(err)}
obtenerServicio={() => console.log("obtener servicio")}
updateIsLoading={(loading) => console.log("loading", loading)}
/>
<InformacinoServicio
servicio={{
Programa: {
institucion: "UNAM",
dependencia: "Académicos",
programa: "Servicio Social",
clavePrograma: "SS123",
},
Usuario: { usuario: "123456", nombre: "Juan Pérez" },
Carrera: { carrera: "Matemáticas" },
creditos: "80",
correo: "juan@correo.com",
fechaInicio: new Date().toISOString(),
fechaFin: new Date().toISOString(),
createdAt: new Date().toISOString(),
}}
/>
<MensajeAlumno Status={{ idStatus: 2 }} />
<PreTermino
alumno={{ tokenArchivo: "token123" }}
servicio={{ idServicio: 123, informeGlobal: undefined }}
imprimirMensaje={(msg) => console.log(msg)}
imprimirWarning={(msg, callback) => {
if (confirm(msg)) callback();
}}
imprimirError={(err) => console.error(err)}
obtenerServicio={() => console.log("obtener servicio")}
updateIsLoading={(loading) => console.log("loading", loading)}
/>
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import axios, { AxiosResponse } from "axios";
interface Status {
idStatus: number;
}
interface Alumno {
idServicio: number;
Status: Status;
cartaAceptacion?: string;
cartaTermino?: string;
informeGlobal?: string;
}
interface Admin {
token: { headers: Record<string, string> };
}
interface Props {
title: string;
admin: Admin;
alumno: Alumno;
imprimirError: (err: unknown) => void;
imprimirMensaje: (msg: string) => void;
imprimirWarning: (msg: string, onConfirm: () => void) => void;
updateIsLoading: (value: boolean) => void;
}
interface ApiResponse {
data: { message: string };
}
interface RechazarData {
idServicio: number;
mensaje: string;
}
export default function VerDocumento({
title,
admin,
alumno,
imprimirError,
imprimirMensaje,
imprimirWarning,
updateIsLoading,
}: Props) {
const [rechazar, setRechazar] = useState(false);
const [mensajeRechazo, setMensajeRechazo] = useState("");
const router = useRouter();
const updateRechazar = () => {
if (rechazar) setMensajeRechazo("");
setRechazar(!rechazar);
};
const archivo = (): string | undefined => {
switch (title) {
case "carta de aceptación":
return alumno.cartaAceptacion;
case "carta de termino":
return alumno.cartaTermino;
case "informe global":
return alumno.informeGlobal;
default:
return undefined;
}
};
const rechazarArchivo = () => {
switch (title) {
case "carta de aceptación":
rechazarFunc(rechazarCartaAceptacion);
break;
case "carta de termino":
rechazarFunc(rechazarCartaTermino);
break;
case "informe global":
rechazarFunc(rechazarInformeGlobal);
break;
}
};
const rechazarFunc = async (
funcRechazar: (data: RechazarData) => Promise<AxiosResponse<ApiResponse>>
) => {
const data: RechazarData = {
idServicio: alumno.idServicio,
mensaje: mensajeRechazo,
};
try {
updateIsLoading(true);
const res = await funcRechazar(data);
localStorage.removeItem("idServicio");
imprimirMensaje(res.data.data.message);
router.push("/administrador");
} catch (err: unknown) {
if (axios.isAxiosError(err)) {
imprimirError(err.response?.data || err.message);
} else {
imprimirError(err);
}
} finally {
updateIsLoading(false);
}
};
const rechazarCartaAceptacion = (data: RechazarData) =>
axios.put(`${process.env.NEXT_PUBLIC_API}/servicio/rechazar_aceptacion`, data, admin.token);
const rechazarCartaTermino = (data: RechazarData) =>
axios.put(`${process.env.NEXT_PUBLIC_API}/servicio/rechazar_termino`, data, admin.token);
const rechazarInformeGlobal = (data: RechazarData) =>
axios.put(`${process.env.NEXT_PUBLIC_API}/servicio/rechazar_informe`, data, admin.token);
return (
<div className="mb-4">
<div className="flex items-center">
<h6 className="pr-4 text-lg">Ver {title}</h6>
<div className="pr-4">
<a
className="btn btn-link btn-light"
target="_blank"
rel="noopener noreferrer"
href={`https://drive.google.com/file/d/${archivo()}/view?usp=sharing`}
>
Ver
</a>
</div>
{((alumno.Status.idStatus === 1 && title === "carta de aceptación") ||
(alumno.Status.idStatus === 5 &&
(title === "carta de termino" || title === "informe global"))) && (
<button
className="btn btn-danger"
onClick={updateRechazar}
>
Rechazar
</button>
)}
</div>
{rechazar && (
<div className="mt-2">
<label>Razón del rechazo:</label>
<textarea
maxLength={500}
value={mensajeRechazo}
onChange={(e) => setMensajeRechazo(e.target.value)}
className="form-control"
/>
<button
className="btn btn-link"
disabled={!mensajeRechazo}
onClick={() =>
imprimirWarning(
"¿Seguro(a) que quiere rechazar este documento?",
rechazarArchivo
)
}
>
Rechazar {title}
</button>
</div>
)}
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import React from "react";
interface Props {
idStatus: number;
}
interface Step {
label: string;
icon: string; // Puedes usar iconos de heroicons o font-awesome
visible: boolean;
type?: "default" | "danger";
}
export default function BarraProgreso({ idStatus }: Props) {
const steps: Step[] = [
{ label: "Pre Registro", icon: "account-plus", visible: idStatus < 6 },
{ label: "Pre Registro Validado", icon: "account-clock", visible: idStatus < 6 },
{ label: "Registro", icon: "account", visible: idStatus < 6 },
{ label: "Pre Termino", icon: "account-details", visible: idStatus < 6 },
{ label: "Termino", icon: "account-clock", visible: idStatus < 6 },
{ label: "Liberacion", icon: "account-check", visible: idStatus < 6 },
{ label: "Carta Aceptación Rechazada", icon: "file", visible: idStatus === 6, type: "danger" },
{ label: "Carta Termino Rechazada", icon: "file", visible: idStatus === 7, type: "danger" },
{ label: "Informe Global Rechazado", icon: "file", visible: idStatus === 8, type: "danger" },
{ label: "Cancelado", icon: "account-cancel", visible: idStatus === 9, type: "danger" },
];
return (
<div className="flex flex-wrap gap-2 mt-6 mb-5">
{steps
.filter((step) => step.visible)
.map((step, index) => (
<div
key={index}
className={`flex items-center px-4 py-2 rounded ${
step.type === "danger"
? "bg-red-500 text-white"
: "bg-blue-500 text-white"
}`}
>
<span className="mr-2">
{/* Aquí puedes usar un icono según step.icon */}
<i className={`fa fa-${step.icon}`}></i>
</span>
<span>{step.label}</span>
</div>
))}
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import React, { useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import axios, { AxiosResponse } from "axios";
import moment from "moment";
interface Alumno {
token: { headers: Record<string, string> };
tokenArchivo?: { headers: Record<string, string> }; // opcional si algunas llamadas lo usan
}
interface Props {
idServicio: number;
alumno: Alumno;
imprimirMensaje: (msg: string) => void;
imprimirWarning: (msg: string, onConfirm: () => void) => void;
imprimirError: (err: unknown) => void;
obtenerServicio: () => void;
updateIsLoading: (value: boolean) => void;
}
interface PreRegistroData {
idServicio: number;
direccion: string;
telefono: string;
fechaNacimiento: string;
}
interface ApiResponse {
data: { message: string };
}
export default function CompletarDatosPersonales({
idServicio,
alumno,
imprimirMensaje,
imprimirWarning,
imprimirError,
obtenerServicio,
updateIsLoading,
}: Props) {
const [direccion, setDireccion] = useState("");
const [telefono, setTelefono] = useState("");
const [nacimiento, setNacimiento] = useState<Date>(new Date());
const maxDate = new Date();
const terminarPreRegistro = async () => {
const data: PreRegistroData = {
idServicio,
direccion,
telefono,
fechaNacimiento: moment(nacimiento).format("YYYY-MM-DD"),
};
try {
updateIsLoading(true);
const res: AxiosResponse<ApiResponse> = await axios.put(
`${process.env.NEXT_PUBLIC_API}/servicio/registro_validado`,
data,
alumno.token
);
imprimirMensaje(res.data.data.message);
obtenerServicio();
} catch (err: unknown) {
if (axios.isAxiosError(err)) {
imprimirError(err.response?.data || err.message);
} else {
imprimirError(err);
}
} finally {
updateIsLoading(false);
}
};
return (
<div className="space-y-4">
<h3 className="text-xl font-semibold">Formulario Pre-registro</h3>
{/* Fecha de nacimiento */}
<div>
<label className="block mb-1 font-medium">Fecha de nacimiento</label>
<DatePicker
selected={nacimiento}
onChange={(date: Date | null) => {
if (date) setNacimiento(date);
}}
maxDate={maxDate}
className="border border-gray-300 rounded px-3 py-2 w-full"
placeholderText="Fecha de nacimiento"
/>
</div>
{/* Teléfono */}
<div>
<label className="block mb-1 font-medium">Teléfono</label>
<input
type="tel"
placeholder="Teléfono"
maxLength={10}
value={telefono}
onChange={(e) => setTelefono(e.target.value)}
className="border border-gray-300 rounded px-3 py-2 w-full"
/>
</div>
{/* Dirección */}
<div>
<label className="block mb-1 font-medium">Dirección</label>
<input
type="text"
placeholder="Dirección"
maxLength={200}
value={direccion}
onChange={(e) => setDireccion(e.target.value)}
className="border border-gray-300 rounded px-3 py-2 w-full"
/>
</div>
{/* Botón enviar */}
<div className="text-center mt-4">
<button
disabled={!telefono || !direccion}
onClick={() =>
imprimirWarning(
"¿Estas seguro(a) que tus datos son correctos?",
terminarPreRegistro
)
}
className={`px-4 py-2 rounded text-white ${
!telefono || !direccion
? "bg-gray-400 cursor-not-allowed"
: "bg-green-600 hover:bg-green-700"
}`}
>
Enviar
</button>
</div>
</div>
);
}
@@ -0,0 +1,165 @@
"use client";
import moment from "moment";
interface Programa {
institucion: string;
dependencia: string;
programa: string;
clavePrograma: string;
}
interface Usuario {
usuario: string;
nombre: string;
}
interface Carrera {
carrera: string;
}
export interface Servicio {
Programa: Programa;
Usuario: Usuario;
Carrera: Carrera;
creditos?: string;
telefono?: string;
direccion?: string;
correo: string;
programaInterno?: string;
profesor?: string;
createdAt?: string;
fechaInicio: string;
fechaFin: string;
fechaLiberacion?: string;
}
interface Props {
servicio: Servicio;
}
export default function InformacinoServicio({ servicio }: Props) {
// Función para formatear fechas
const fecha = (date?: string) => {
if (!date) return "";
const f = moment(date);
const day = f.date() < 10 ? "0" + f.date() : f.date();
const month = f.month() < 9 ? "0" + (f.month() + 1) : f.month() + 1;
const year = f.year();
return `${day}/${month}/${year}`;
};
return (
<div>
{/* Datos del programa */}
<div className="mb-5">
<h4 className="is-size-4 pb-2">Datos del programa</h4>
<div className="mb-2">
<label>Institución:</label>
<p className="input">{servicio.Programa.institucion}</p>
</div>
<div className="mb-2">
<label>Dependencia:</label>
<p className="input">{servicio.Programa.dependencia}</p>
</div>
<div className="mb-2">
<label>Programa:</label>
<p className="input">{servicio.Programa.programa}</p>
</div>
<div className="mb-2">
<label>Clave de programa:</label>
<p className="input">{servicio.Programa.clavePrograma}</p>
</div>
</div>
{/* Datos personales */}
<div className="mb-5">
<h4 className="is-size-4 pb-2">Datos personales</h4>
<div className="mb-2">
<label>Número de cuenta:</label>
<p className="input">{servicio.Usuario.usuario}</p>
</div>
<div className="mb-2">
<label>Nombre:</label>
<p className="input">{servicio.Usuario.nombre}</p>
</div>
<div className="mb-2">
<label>Carrera:</label>
<p className="input">{servicio.Carrera.carrera}</p>
</div>
<div className="mb-2">
<label>Créditos:</label>
<p className="input">
{servicio.creditos ? parseInt(servicio.creditos) : ""}
{servicio.creditos && "%"}
</p>
</div>
{servicio.telefono && (
<div className="mb-2">
<label>Teléfono:</label>
<p className="input">{servicio.telefono}</p>
</div>
)}
{servicio.direccion && (
<div className="mb-2">
<label>Dirección:</label>
<p className="input">{servicio.direccion}</p>
</div>
)}
<div className="mb-2">
<label>Email:</label>
<p className="input">{servicio.correo}</p>
</div>
{servicio.programaInterno && (
<div className="mb-2">
<label>Programa Interno:</label>
<p className="input">{servicio.programaInterno}</p>
</div>
)}
{servicio.profesor && (
<div className="mb-2">
<label>Profesor:</label>
<p className="input">{servicio.profesor}</p>
</div>
)}
{servicio.createdAt && (
<div className="mb-2">
<label>Fecha de registro:</label>
<p className="input">{fecha(servicio.createdAt)}</p>
</div>
)}
<div className="mb-2">
<label>Fecha de inicio:</label>
<p className="input">{fecha(servicio.fechaInicio)}</p>
</div>
<div className="mb-2">
<label>Fecha de término:</label>
<p className="input">{fecha(servicio.fechaFin)}</p>
</div>
{servicio.fechaLiberacion && (
<div className="mb-2">
<label>Fecha de liberación:</label>
<p className="input">{fecha(servicio.fechaLiberacion)}</p>
</div>
)}
</div>
</div>
);
}
+98
View File
@@ -0,0 +1,98 @@
"use client";
interface Status {
idStatus: number;
}
interface Props {
Status: Status;
}
export default function MensajeAlumno({ Status }: Props) {
return (
<div className="mb-6">
<h4 className="block is-size-4">Estimado alumno(a):</h4>
<p className="has-text-justified is-size-6">
{Status.idStatus === 2 && (
<>
Estimado alumno(a): Para poder terminar con el registro de tu trámite
de servicio social te pedimos que verifiques que tus datos sean
correctos, de no ser así puedes acudir a las ventanillas de servicio
social de la Secretaría de Asuntos Académicos Estudiantiles, en la
planta baja del Edificio A-8 de lunes a viernes de 09:00 a 15:00 hrs.
y de 17:00 a 19:00 hrs. o bien, comunicarte al 5623 1686 o al correo
tramites.ss@acatlan.unam.mx y también contesta el formulario que se
encuentra hasta el final de esta página.
</>
)}
{Status.idStatus === 3 && (
<>
Estimado alumno(a): Te informamos que el Área de Registro y Control
de Servicio Social ha validado tu solicitud de registro de servicio
social por lo que el siguiente trámite lo realizarás hasta que
concluyas 480 horas en un periodo de mínimo 6 meses y obtengas tu
carta de término por parte de la institución, quien se encargará de
subir dicho archivo a este sistema, además de responder un
cuestionario sobre tu desempeño. A su vez, deberás responder el
siguiente cuestionario de evaluación del programa de servicio social
en donde participaste y enviar el informe global de actividades
elaborado por ti, con firma y sello de visto bueno de tu jefe
inmediato. En cuanto el Área de Registro y Control de Servicio Social
valide tu solicitud, podrás revisar las indicaciones del siguiente
paso en el panel llamado Liberación. Cualquier duda puedes acudir a
las ventanillas de servicio social de la Secretaría de Asuntos
Académicos Estudiantiles, en la planta baja del Edificio A-8 de lunes
a viernes de 09:00 a 15:00 hrs. y de 17:00 a 19:00 hrs. o bien,
comunicarte al 5623 1686 o al correo tramites.ss@acatlan.unam.mx
</>
)}
{Status.idStatus === 4 && (
<>
Estimado alumno(a): Para continuar con el proceso de término de tu
servicio social debes de subir tu informe global y contestar el
cuestionario que se encuentra hasta el final de esta página.
</>
)}
{Status.idStatus === 5 && (
<>
Estimado alumno(a): Espera a que tus documentos sean validados por
el Departamento de Servicio Social y Bolsa de Trabajo.
</>
)}
{Status.idStatus === 6 && (
<>
Estimado alumno(a): Te confirmamos que has concluido con los trámites
necesarios para la liberación de tu servicio social por lo que ahora
sólo queda esperar a que en máximo 15 días hábiles se te envíe por
correo tu carta de liberación. Cualquier duda puedes acudir a las
ventanillas de servicio social la Secretaría de Asuntos Académicos
Estudiantiles, en la planta baja del Edificio A-8 de lunes a viernes
de 09:00 a 15:00 hrs. y de 17:00 a 19:00 hrs. o bien, comunicarte al
5623 1686 o al correo tramites.ss@acatlan.unam.mx
</>
)}
{Status.idStatus === 8 && (
<>
Estimado alumno(a): Te informamos que la carta de término que el(la)
responsable de tu servicio social mandó fue rechazada. Espera a que
la vuelvan a subir al sistema para volver a verificarla.
</>
)}
{Status.idStatus === 9 && (
<>
Estimado alumno(a): Te informamos que el informe global que mandaste
fue rechazado. Súbelo nuevamente y asegúrate de que cumpla con todos
los requisitos.
</>
)}
</p>
</div>
);
}
+152
View File
@@ -0,0 +1,152 @@
"use client";
import { useState, useEffect } from "react";
import axios, { AxiosResponse } from "axios";
interface Alumno {
tokenArchivo: string;
}
interface Servicio {
idServicio: number;
idCuestionarioAlumno?: number;
idCuestionarioAlumno2?: number;
informeGlobal?: string;
}
interface Props {
alumno: Alumno;
servicio: Servicio;
imprimirMensaje: (msg: string) => void;
imprimirWarning: (msg: string, callback: () => void) => void;
imprimirError: (err: any) => void;
obtenerServicio: () => void;
updateIsLoading: (loading: boolean) => void;
}
export default function PreTermino({
alumno,
servicio,
imprimirMensaje,
imprimirWarning,
imprimirError,
obtenerServicio,
updateIsLoading,
}: Props) {
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
if (file && file.size >= 20000000) {
imprimirError({ message: "El tamaño del archivo excede los 20MB" });
setFile(null);
}
}, [file, imprimirError]);
const enviarInformeGlobal = async () => {
if (!file) return;
const formData = new FormData();
const data = { idServicio: servicio.idServicio };
formData.append("data", JSON.stringify(data));
formData.append("informeGlobal", file);
try {
updateIsLoading(true);
const res: AxiosResponse<{ message: string }> = await axios.put(
`${process.env.api}/servicio/informe_global`,
formData,
{ headers: { "Content-Type": "multipart/form-data", Authorization: alumno.tokenArchivo } }
);
imprimirMensaje(res.data.message);
obtenerServicio();
setFile(null);
} catch (err: unknown) {
if (axios.isAxiosError(err)) {
imprimirError(err.response?.data || err.message);
} else {
imprimirError(err);
}
} finally {
updateIsLoading(false);
}
};
return (
<div>
<h3 className="label">
Cuestionario de evaluación del programa de servicio social.
</h3>
{!servicio.idCuestionarioAlumno && !servicio.idCuestionarioAlumno2 ? (
<div className="mb-6">
<a
href="/alumno/cuestionario"
className="button is-info is-light"
>
<span className="icon">
<i className="fas fa-book-open"></i>
</span>
<span>Cuestionario</span>
</a>
</div>
) : (
<div className="mb-6">
<p className="block is-size-6">
Cuestionario contestado{" "}
<span className="icon has-text-success">
<i className="fas fa-check-bold"></i>
</span>
</p>
</div>
)}
<h3 className="label">
Informe global de actividades{" "}
{!servicio.informeGlobal && (
<span>(en formato .PDF. No se aceptan fotos)</span>
)}
.
</h3>
{!servicio.informeGlobal ? (
<div>
<div className="field">
<input
type="file"
accept="application/pdf"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
}}
/>
<p className="is-size-6">Tamaño máximo 20MB</p>
</div>
<div className="field has-text-centered my-5">
<button
className="button is-success"
disabled={!file}
onClick={() =>
imprimirWarning(
"¿Estas seguro(a) de querer subir este informe global?",
enviarInformeGlobal
)
}
>
Enviar
</button>
</div>
</div>
) : (
<p className="block is-size-6">
Informe Global enviado.{" "}
<span className="icon has-text-success">
<i className="fas fa-check-bold"></i>
</span>
</p>
)}
</div>
);
}