Se modifico la logia en el envio de archivos falta corregir el endpoint de servicio/update

This commit is contained in:
santiago
2026-01-07 19:53:59 -06:00
parent bd7b34c59c
commit d3ba08a3cf
24 changed files with 1270 additions and 525 deletions
+783 -373
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -13,14 +13,14 @@
"bootstrap": "^5.3.8",
"file-saver": "^2.0.5",
"moment": "^2.30.1",
"next": "15.5.3",
"next": "^16.0.8",
"react": "19.1.0",
"react-bootstrap": "^2.10.10",
"react-datepicker": "^8.7.0",
"react-dom": "19.1.0",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"sass": "^1.92.1",
"sass": "^1.95.0",
"sweetalert2": "^11.26.2",
"sweetalert2-react-content": "^5.1.0",
"validator": "^13.15.15"
+3 -3
View File
@@ -19,9 +19,9 @@ axiosInstance.interceptors.request.use(
config.headers.Authorization = `Bearer ${token}`;
//Quitar estas lienas
config.headers['token'] = token;
config.headers['x-access-token'] = token;
config.headers['token-v2'] = token;
// config.headers['token'] = token;
// config.headers['x-access-token'] = token;
// config.headers['token-v2'] = token;
}
}
+7 -7
View File
@@ -32,9 +32,9 @@ interface InfoAlumno{
//createdAt: Date,
idCuestionarioAlumno: number,
idCuestionarioAlumno2: number,
Carrera: Carrera,
Status: Status,
Programa: Programa,
carrera: Carrera,
status: Status,
programa: Programa,
}
interface Carrera {
@@ -93,8 +93,8 @@ export default function Home() {
return (
<div>
{ data?.Status.idStatus && (
<BarraProgreso idStatus={data?.Status.idStatus}/>
{ data?.status.idStatus && (
<BarraProgreso idStatus={data?.status.idStatus}/>
)}
<div className="my-5">
@@ -123,7 +123,7 @@ export default function Home() {
/>
)}
{ data?.Status.idStatus === 2 && (
{ data?.status.idStatus === 2 && (
<CompletarDatosPersonales
idServicio={123}
alumno={{ token: { headers: { Authorization: "Bearer ..." } }, tokenArchivo: { headers: { Authorization: "Bearer ..." } } }}
@@ -135,7 +135,7 @@ export default function Home() {
/>
)}
{ data?.Status.idStatus !== undefined && data?.Status.idStatus >= 4 && (
{ data?.status.idStatus !== undefined && data?.status.idStatus >= 4 && (
<PreTermino
alumno={{ tokenArchivo: "token123" }}
servicio={data}
+13 -4
View File
@@ -1,3 +1,4 @@
@import "react-datepicker/dist/react-datepicker.css";
:root {
--background: #ffffff;
--foreground: #171717;
@@ -10,9 +11,6 @@
}
}
@import "react-datepicker/dist/react-datepicker.css";
html,
body {
max-width: 100vw;
@@ -20,7 +18,7 @@ body {
}
body {
color: var(--foreground);
/* Do not override text color globally so Bootstrap utilities and components keep their intended colors */
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -60,6 +58,17 @@ button {
}
}
/* Ensure fw-bold utility is available and wins if other rules override it */
.fw-bold,
h1.fw-bold, h2.fw-bold, h3.fw-bold, h4.fw-bold, h5.fw-bold, h6.fw-bold {
font-weight: 700 !important;
}
/* Ensure form labels are easily readable and bold like before */
.form-label {
font-weight: 700 !important;
}
table {
width: 100%;
+1
View File
@@ -3,6 +3,7 @@ import Header from "@/components/layout/header";
import "./globals.css";
import Footer from "@/components/layout/footer";
import "@/sass/bootstrap.scss"
import { Metadata } from 'next';
export const metadata: Metadata = {
+29 -7
View File
@@ -8,7 +8,7 @@ import { axiosInstance } from "@/api/config";
import type { AxiosError } from 'axios';
type Responsable = { idUsuario?: number; idTipoUsuario?: number; tipoUsuario?: string | null; token?: string; tokenArchivo?: string };
type Alumno = { nombre?: string; usuario?: string };
type Alumno = { nombre?: string; idUsuario?: number };
export default function Page() {
const router = useRouter();
@@ -16,6 +16,8 @@ export default function Page() {
const [idServicio, setIdServicio] = useState<number | null>(null);
const [alumno, setAlumno] = useState<Alumno>({});
const [responsable, setResponsable] = useState<Responsable>({});
// Para verificar que se guardo el idServicio
const [isReady, setIsReady] = useState(false);
const updateIsLoading = (v: boolean) => setIsLoading(v);
@@ -54,9 +56,17 @@ export default function Page() {
const tipoUsuario = localStorage.getItem('tipoUsuario');
const token = localStorage.getItem('token') || undefined;
const tokenArchivo = localStorage.getItem('token') || undefined;
setResponsable({ idUsuario: Number.isNaN(idUsuario) ? undefined : idUsuario, idTipoUsuario: Number.isNaN(idTipoUsuario) ? undefined : idTipoUsuario, tipoUsuario, token: token ?? undefined, tokenArchivo: tokenArchivo ?? undefined });
setResponsable({
idUsuario: Number.isNaN(idUsuario) ? undefined : idUsuario,
idTipoUsuario: Number.isNaN(idTipoUsuario) ? undefined : idTipoUsuario,
tipoUsuario,
token: token ?? undefined,
tokenArchivo: tokenArchivo ?? undefined
});
const s = Number(localStorage.getItem('idServicio'));
setIdServicio(Number.isNaN(s) ? null : s);
setIsReady(true);
};
const obtenerRegistro = async () => {
@@ -64,8 +74,12 @@ export default function Page() {
updateIsLoading(true);
try {
const headers = responsable.token ? { Authorization: `Bearer ${responsable.token}` } : undefined;
const res = await axiosInstance.get<{ Usuario?: Alumno; Status?: { idStatus?: number } }>(`/servicio/admin?idServicio=${idServicio}`, { headers });
setAlumno(res.data.Usuario ?? {});
const res = await axiosInstance.get<{ usuario?: Alumno; Status?: { idStatus?: number } }>(`/servicio/admin/${idServicio}`);
console.log('Respuesta al obtener el servicio:', res.data);
console.log("Info Usuario: ", res.data.usuario)
console.log("Nombre del alumno: ", res.data.usuario?.nombre);
setAlumno(res.data.usuario ?? {});
updateIsLoading(false);
const s = res.data.Status?.idStatus ?? 0;
// Se comennto para hacer pruebas
@@ -82,20 +96,28 @@ export default function Page() {
useEffect(() => { getLocalhostInfo(); }, []);
useEffect(() => {
if (!isReady) return; // Para esperar a que cargue la info del localStorage
if (responsable.idTipoUsuario === 1) router.push('/admin');
if (responsable.idTipoUsuario === 3) router.push('/alumno');
if (responsable.idTipoUsuario === 4) router.push('/casoEspecial');
if (idServicio !== null) {
console.log("ID Servicio no es nulo, se obtiene el registro.", idServicio);
obtenerRegistro();
} else (
router.push('/responsable')
)
// Se comento para hacer pruebas
//if (idServicio === null) router.push('/responsable');
else if (idServicio !== null) obtenerRegistro();
// else if (idServicio !== null) obtenerRegistro();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [responsable, idServicio]);
}, [isReady, responsable, idServicio]);
return (
<section className="container px-2 pb-6">
<BotonRegresar />
<h3 className="title">{alumno.nombre} {alumno.usuario}</h3>
<h3 className="title">{alumno.nombre}</h3>
{idServicio !== null && (
<UploadArchivo
+21 -4
View File
@@ -19,6 +19,10 @@ export default function Page() {
const [idServicio, setIdServicio] = useState<number | null>(null);
const [alumno, setAlumno] = useState<Record<string, unknown> | null>(null);
const [responsable, setResponsable] = useState<Responsable>({});
// Para esperar a que cargue la info del localStorage
const [isReady, setIsReady] = useState(false);
const updateIsLoading = (v: boolean) => setIsLoading(v);
@@ -59,9 +63,16 @@ export default function Page() {
const idTipoUsuario = Number(localStorage.getItem('idTipoUsuario'));
const tipoUsuario = localStorage.getItem('tipoUsuario');
const token = localStorage.getItem('token') || undefined;
setResponsable({ idUsuario: Number.isNaN(idUsuario) ? undefined : idUsuario, idTipoUsuario: Number.isNaN(idTipoUsuario) ? undefined : idTipoUsuario, tipoUsuario, token: token ?? undefined });
setResponsable({
idUsuario: Number.isNaN(idUsuario) ? undefined : idUsuario,
idTipoUsuario: Number.isNaN(idTipoUsuario) ? undefined : idTipoUsuario,
tipoUsuario,
token: token ?? undefined
});
const s = Number(localStorage.getItem('idServicio'));
setIdServicio(Number.isNaN(s) ? null : s);
setIsReady(true);
};
const obtenerRegistro = async () => {
@@ -69,7 +80,7 @@ export default function Page() {
updateIsLoading(true);
try {
const headers = responsable.token ? { Authorization: `Bearer ${responsable.token}` } : undefined;
const res = await axiosInstance.get<{ Usuario?: Record<string, unknown>; idCuestionarioPrograma?: number }>(`/servicio/admin?idServicio=${idServicio}`, { headers });
const res = await axiosInstance.get<{ Usuario?: Record<string, unknown>; idCuestionarioPrograma?: number }>(`/servicio/admin/${idServicio}`,);
setAlumno(res.data.Usuario ?? null);
updateIsLoading(false);
if (res.data.idCuestionarioPrograma) router.push('/responsable');
@@ -90,14 +101,20 @@ export default function Page() {
}, []);
useEffect(() => {
// Hacemos la validacion para asegurarnos de que cargo la info del local
if (!isReady) return; // Para esperar a que cargue la info del localStorage
// Redireccionamientos segun tipo de usuario
if (responsable.idTipoUsuario === 1) router.push('/administrador');
if (responsable.idTipoUsuario === 3) router.push('/alumno');
if (responsable.idTipoUsuario === 4) router.push('/casoEspecial');
// Para verificar el servicio
//if (idServicio === null) router.push('/responsable'); // Esta mal la logica
else if (idServicio !== null) obtenerRegistro();
if (idServicio !== null) {
obtenerRegistro();
} else router.push('/responsable');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [responsable, idServicio]);
}, [isReady, responsable, idServicio]);
return (
<section className="container my-5 px-3">
+1 -1
View File
@@ -59,7 +59,7 @@ export default function Page() {
}, []);
useEffect(() => {
if (responsable.idTipoUsuario === 1) router.push('/admin');
if (responsable.idTipoUsuario === 1) router.push('/administrador');
if (responsable.idTipoUsuario === 3) router.push('/alumno');
if (responsable.idTipoUsuario === 4) router.push('/casoEspecial');
// eslint-disable-next-line react-hooks/exhaustive-deps
+55 -15
View File
@@ -25,7 +25,7 @@ interface Datos {
//Programa?: Programa
//Carrera?: Carrera;
//Usuario?: Usuario;
Status?: Status;
status?: Status;
creditos?: string;
correo?: string;
fechaRegistro?: string;
@@ -97,6 +97,11 @@ export default function Archivo({
};
const rechazarArchivo = () => {
// Depuramos para ver errores
console.log("Title", title)
console.log("status", datos.status?.idStatus)
console.log("idServicio", datos.idServicio)
switch (title) {
case "carta de aceptación":
rechazarFunc(rechazarCartaAceptacion);
@@ -118,6 +123,8 @@ export default function Archivo({
mensaje: mensajeRechazo,
};
console.log("Datos para rechazar:", data);
try {
updateIsLoading(true);
const res = await funcRechazar(data);
@@ -146,19 +153,47 @@ export default function Archivo({
return (
<div className="container mb-4">
<div className="flex items-center">
<h5 className="pr-4 text-lg">
Ver {title}
<a
className="btn btn-link btn-light text-decoration-none ms-3 text-morado"
target="_blank"
rel="noopener noreferrer"
href={`https://drive.google.com/file/d/${archivo()}/view?usp=sharing`}
style={{ background: '#eae4f8'}}
<div className="d-flex align-items-center">
{/* Texto */}
<h5 className="mb-0">
Ver {title}
</h5>
<div className="d-flex align-items-center gap-2">
<a
className="btn btn-link btn-light text-decoration-none ms-3 text-morado"
target="_blank"
rel="noopener noreferrer"
href={`https://drive.google.com/file/d/${archivo()}/view?usp=sharing`}
style={{ background: '#eae4f8'}}
>
Ver
</a>
<button
className="btn btn-danger"
onClick={updateRechazar}
>
Ver
</a>
</h5>
Rechazar
</button>
{((datos.status?.idStatus === 1 && title === "carta de aceptación") ||
(datos.status?.idStatus === 5 &&
(title === "carta de termino" || title === "informe global"))) && (
<button
className="btn btn-danger"
onClick={updateRechazar}
>
Rechazar
</button>
)}
</div>
{/* Solo de moemento para la parte visual falta corregir esto */}
{/*
<div className="pr-4">
@@ -173,8 +208,8 @@ export default function Archivo({
</div>
*/}
{((datos.Status?.idStatus === 1 && title === "carta de aceptación") ||
(datos.Status?.idStatus === 5 &&
{/* {((datos.status?.idStatus === 1 && title === "carta de aceptación") ||
(datos.status?.idStatus === 5 &&
(title === "carta de termino" || title === "informe global"))) && (
<button
className="btn btn-danger"
@@ -183,6 +218,11 @@ export default function Archivo({
Rechazar
</button>
)}
</div> */}
</div>
{rechazar && (
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import { axiosInstance } from "@/api/config";
import { isAxiosError } from 'axios';
import axios, { isAxiosError } from 'axios';
import moment from "moment";
import validator from "validator";
import DatePicker from "react-datepicker";
@@ -31,7 +31,7 @@ interface AdminToken {
tokenArchivo?: { headers: Record<string, string> };
}; */}
interface Props {
admin: string;
admin: AdminToken;
imprimirError: (err: unknown) => void;
imprimirMensaje: (msg: string) => void;
imprimirWarning: (msg: string, onConfirm: () => void) => void;
@@ -146,13 +146,41 @@ export default function EditarServicio({
formData.append("data", JSON.stringify(data));
console.log("Datos a actualizar:", formData);
for (const [key, value] of formData.entries()) {
console.log("FormData:", key, value);
}
try {
updateIsLoading(true);
const res = await axiosInstance.put(`/servicio/update`, formData); //, admin.tokenArchivo
// Agregue esta parte para verificar que funcione
if (cartaAceptacion) {
console.log("Carta Aceptación:", {
name: cartaAceptacion.name,
size: cartaAceptacion.size,
type: cartaAceptacion.type,
});
}
if (cartaTermino) {
console.log("Carta Término:", cartaTermino.name);
}
if (informeGlobal) {
console.log("Informe Global:", informeGlobal.name);
}
const token = localStorage.getItem("token") || "";
const res = await axios.put(`http://localhost:3392/servicio/update`, formData, {
headers: {
Authorization: `Bearer ${token}`,
},
}); //, admin.tokenArchivo
imprimirMensaje(res.data.message);
updateIsLoading(false);
router.push("/administrador/servicio");
router.replace("/administrador/servicio");
} catch (err: unknown) {
updateIsLoading(false);
if (isAxiosError(err)) imprimirError(err.response?.data ?? err.message);
@@ -176,7 +204,7 @@ export default function EditarServicio({
const res = await axiosInstance.post(`/usuario/new-password-alumno/${data.servicioid}`);
imprimirMensaje(res.data.message);
updateIsLoading(false);
router.push("/administrador");
router.replace("/administrador");
} catch (err: unknown) {
console.log("No esta mandando la solicitud")
updateIsLoading(false);
@@ -423,7 +451,7 @@ export default function EditarServicio({
<input
id="pdfInput"
type="file"
accept=".pdf"
accept="application/pdf"
style={{ display: 'none'}}
onChange={(e) => {
setCartaAceptacion(e.target.files ? e.target.files[0] : null)
@@ -302,7 +302,7 @@ function formatDate(value?: string | null): string {
const d = new Date(value.includes("T") ? value : `${value}T00:00:00`);
if (isNaN(d.getTime())) {
console.warn("⚠️ Fecha inválida:", value);
// console.warn("⚠️ Fecha inválida:", value);
return "";
}
@@ -159,6 +159,7 @@ export default function TablaServicioSocial({ admin, imprimirError }: Props) {
<FaInfoCircle />
</InputGroup.Text>
<FormSelect
suppressHydrationWarning
value={search.idStatus}
onChange={(e) => setSearch(Prev => ({ ...Prev, idStatus: e.target.value }))}
className="rounded-4"
+12 -11
View File
@@ -19,9 +19,9 @@ interface Carrera {
}
export interface Servicio {
Programa: Programa;
Usuario: Usuario;
Carrera: Carrera;
programa: Programa;
//usuario: Usuario;
carrera: Carrera;
creditos?: string;
telefono?: string;
direccion?: string;
@@ -57,22 +57,22 @@ export default function InformacinoServicio({ servicio }: Props) {
<div className="mb-2">
<label className="form-label fw-semibold">Institución:</label>
<p className="form-control">{servicio.Programa.institucion}</p>
<p className="form-control">{servicio.programa.institucion}</p>
</div>
<div className="mb-2">
<label className="form-label fw-semibold">Dependencia:</label>
<p className="form-control">{servicio.Programa.dependencia}</p>
<p className="form-control">{servicio.programa.dependencia}</p>
</div>
<div className="mb-2">
<label className="form-label fw-semibold">Programa:</label>
<p className="form-control">{servicio.Programa.programa}</p>
<p className="form-control">{servicio.programa.programa}</p>
</div>
<div className="mb-2">
<label className="form-label fw-semibold">Clave de programa:</label>
<p className="form-control">{servicio.Programa.clavePrograma}</p>
<p className="form-control">{servicio.programa.clavePrograma}</p>
</div>
</div>
@@ -80,19 +80,20 @@ export default function InformacinoServicio({ servicio }: Props) {
<div className="mb-5">
<h4 className="is-size-4 pb-2">Datos personales</h4>
{/*
<div className="mb-2">
<label className="form-label fw-semibold">Número de cuenta:</label>
<p className="form-control">{servicio.Usuario.usuario}</p>
<p className="form-control">{servicio.usuario.usuario}</p>
</div>
<div className="mb-2">
<label className="form-label fw-semibold">Nombre:</label>
<p className="form-control">{servicio.Usuario.nombre}</p>
<p className="form-control">{servicio.usuario.nombre}</p>
</div>
*/}
<div className="mb-2">
<label className="form-label fw-semibold">Carrera:</label>
<p className="form-control">{servicio.Carrera.carrera}</p>
<p className="form-control">{servicio.carrera.carrera}</p>
</div>
<div className="mb-2">
+2 -2
View File
@@ -7,8 +7,8 @@ const BotonRegresar: React.FC = () => {
const router = useRouter();
const handleGoBack = () => {
//window.history.back();
router.back();
window.history.back();
//router.back();
};
@@ -114,7 +114,7 @@ export default function CasoEspecialForm({
const buscarAlumno = async (): Promise<void> => {
try {
updateIsLoading(true);
const res = await axiosInstance.get(`/usuario/escolares?numeroCuenta=${numeroCuenta}`);
const res = await axiosInstance.post(`/usuario/escolares/${numeroCuenta}`);
resetear();
setAlumno(res.data);
} catch (err: unknown) {
@@ -137,17 +137,17 @@ export default function CasoEspecialForm({
if (!file) return;
const data = {
idUsuario: alumno.idUsuario,
idCarrera: alumno.idCarrera,
idStatus,
numeroCuenta,
creditos: alumno.creditos,
correo,
fechaInicio: moment(fechaInicio),
fechaFin: moment(fechaFin),
fechaNacimiento: moment(fechaNacimiento),
direccion,
telefono,
idUsuario: Number(alumno.idUsuario),
idCarrera: Number(alumno.idCarrera),
idStatus: Number(idStatus),
numeroCuenta: numeroCuenta.toString(),
creditos: alumno.creditos?.toString() || '',
correo: correo.trim(),
fechaInicio: fechaInicio.toISOString(),
fechaFin: fechaFin.toISOString(),
fechaNacimiento: fechaNacimiento.toISOString(),
direccion: direccion.trim(),
telefono: telefono.toString(),
institucion,
dependencia,
motivo,
@@ -159,7 +159,7 @@ export default function CasoEspecialForm({
try {
updateIsLoading(true);
const res = await axiosInstance.post(`/caso_especial/nuevo`, formData);
const res = await axiosInstance.post(`/caso-especial`, formData);
resetear();
setNumeroCuenta('');
imprimirMensaje(res.data.message);
@@ -475,6 +475,7 @@ export default function CasoEspecialForm({
<input
id='fileInput'
type="file"
accept='.zip, .rar'
style={{ display: 'none'}}
onChange={e => setFile(e.target.files ? e.target.files[0] : null)}
/>
@@ -204,6 +204,7 @@ export default function CuestionarioResponsbale2({
}
}
}
for (const t of formulario.tablas) {
for (const r of t.renglones) {
const tablaResp = respuestas[String(t.idTabla)] as Record<number, string | null> | undefined;
@@ -235,12 +236,32 @@ export default function CuestionarioResponsbale2({
const valor = respuestasTabla[idR as unknown as number];
let clave = `p${numPregunta}_${idR}`;
if (subLetra) clave = `p${numPregunta}_${subLetra}_${idR}`;
resultado[clave] = valor ?? '';
// antes resultado[clave] = valor ?? '';
// Modifique esta linea
if (valor !== null && valor !== '') {
resultado[clave] = valor;
}
}
} else {
const preguntaDef = formulario.preguntas.find((p) => p.id === key);
if (!preguntaDef) continue;
resultado[`p${preguntaDef.numeroPregunta}`] = resps[key] as string;
// VALIDAR CONDICIONAL (p6)
if (preguntaDef.condicional) {
const cond = preguntaDef.condicional;
if (resps[cond.preguntaId] !== cond.valor) {
continue; // ⛔ NO mandar esta pregunta
}
}
const valor = resps[key];
if (valor !== null && String(valor).trim() !== '') {
resultado[`p${preguntaDef.numeroPregunta}`] = valor as string;
}
//resultado[`p${preguntaDef.numeroPregunta}`] = resps[key] as string;
}
}
return resultado;
@@ -251,9 +272,12 @@ export default function CuestionarioResponsbale2({
updateIsLoading(true);
setIsLoading(true);
try {
const data = { ...formatearRespuestas(respuestas), idServicio: localStorage.getItem('idServicio') };
const data = { ...formatearRespuestas(respuestas), idServicio: Number(localStorage.getItem('idServicio')) };
console.log('Datos a enviar:', data);
const headers = responsable?.token ? { Authorization: `Bearer ${responsable.token}` } : undefined;
await axiosInstance.post('/cuestionario_programa', data, { headers });
await axiosInstance.post('/cuestionario-programa2', data, { headers });
imprimirMensaje('Se enviaron los datos correctamente');
try { localStorage.removeItem('idCuestionarioAlumno'); } catch (_) {}
if (typeof window !== 'undefined') window.location.href = '/alumno';
+51 -9
View File
@@ -94,7 +94,8 @@ export default function NuevoServicio({
updateIsLoading(true);
try {
const headers = responsable?.token ? { Authorization: `Bearer ${responsable.token}` } : undefined;
const res = await axiosInstance.post<Alumno>(`/usuario/escolares/${numeroCuenta}`, { headers });
// Enviar headers como tercer parámetro (config), no como body
const res = await axiosInstance.post<Alumno>(`/usuario/escolares/${numeroCuenta}`, undefined, { headers });
resetar();
setAlumno(res.data);
obtenerProgramas();
@@ -128,12 +129,26 @@ export default function NuevoServicio({
} as const;
formData.append("alumno", JSON.stringify(data));
if (file) formData.append("cartaAceptacion", file, file.name);
if (!file) {
imprimirError({ message: 'No hay archivo seleccionado' });
return;
}
// Para verificar que se envie el archivo
console.log("Archivo a enviar:", file);
//if (file)
formData.append("cartaAceptacion", file, file.name);
updateIsLoading(true);
try {
const headers = responsable?.tokenArchivo ? { Authorization: `Bearer ${responsable.tokenArchivo}`, 'Content-Type': 'multipart/form-data' } : { 'Content-Type': 'multipart/form-data' };
const res = await axiosInstance.post("/servicio/nuevo", formData, { headers });
const headers = responsable?.tokenArchivo ? { Authorization: `Bearer ${responsable.tokenArchivo}` }: {};
const res = await axiosInstance.post("/servicio/nuevo", formData, {
headers: {
...(responsable?.tokenArchivo && {
Authorization: `Bearer ${responsable.tokenArchivo}`,
}),
},
});
setNumeroCuenta("");
resetar();
updateIsLoading(false);
@@ -150,8 +165,24 @@ export default function NuevoServicio({
const obtenerProgramas = async () => {
try {
const headers = responsable?.token ? { Authorization: `Bearer ${responsable.token}` } : undefined;
const res = await axiosInstance.get<Programa[]>(`/programa/programas_responsable/${responsable.idUsuario}`, { headers });
setProgramas(res.data);
const res = await axiosInstance.get(`/programa/programas_responsable/${responsable.idUsuario}`, { headers });
// Normalizar la respuesta: soportar varias formas comunes:
// - res.data = Array
// - res.data = { data: Array }
// - res.data = { programas: Array }
const payload = res?.data ?? res;
console.log("Esta es la info que traen los programas", payload);
if (Array.isArray(payload)) {
setProgramas(payload as Programa[]);
} else if (payload && Array.isArray((payload as any).data)) {
setProgramas((payload as any).data as Programa[]);
} else if (payload && Array.isArray((payload as any).programas)) {
setProgramas((payload as any).programas as Programa[]);
} else {
console.warn('Respuesta inesperada al obtener programas, se usará lista vacía', payload);
setProgramas([]);
}
} catch (err: unknown) {
const axiosErr = err as AxiosError;
if (axiosErr?.response?.data) imprimirError(axiosErr.response.data as object);
@@ -188,6 +219,14 @@ export default function NuevoServicio({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Fetch programas when responsable id is available
useEffect(() => {
if (responsable?.idUsuario) {
obtenerProgramas();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [responsable?.idUsuario]);
useEffect(() => {
updateFechas();
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -205,12 +244,12 @@ export default function NuevoServicio({
type="text"
placeholder="Número de Cuenta"
maxLength={9}
value={numeroCuenta}
value={numeroCuenta ?? ''}
onChange={(e) => setNumeroCuenta(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && buscarAlumno()}
className="form-control"
/>
<button className="" onClick={() => buscarAlumno()}>Buscar</button>
<button type="button" className="" onClick={() => buscarAlumno()}>Buscar</button>
</div>
</div>
@@ -239,7 +278,10 @@ export default function NuevoServicio({
<label className="form-label fw-bolder">Seleccione programa</label>
<select className="form-select" value={JSON.stringify(programa) } onChange={(e) => { try { setPrograma(JSON.parse(e.target.value)); } catch { setPrograma({}); } }}>
<option value={JSON.stringify({})} disabled>Seleccionar</option>
{programas.map((p, i) => (
{(!Array.isArray(programas) || programas.length === 0) && (
<option value={JSON.stringify({})} disabled>No hay programas disponibles</option>
)}
{Array.isArray(programas) && programas.length > 0 && programas.map((p, i) => (
<option key={i} value={JSON.stringify(p)}>{p.programa}</option>
))}
</select>
@@ -1,5 +1,3 @@
"use client";
import React, { useEffect, useState } from "react";
import { axiosInstance } from "@/api/config";
import type { AxiosError } from "axios";
@@ -23,8 +21,9 @@ export default function TablaServiciosResponsable({ responsable, imprimirError }
const [total, setTotal] = useState(0);
const [data, setData] = useState<ServicioSocialResponse[]>([]);
const [status, setStatus] = useState<StatusItem[]>([]);
const [search, setSearch] = useState<{ numeroCuenta?: string; nombre?: string; idStatus?: string }>({ idStatus: '' });
const [search, setSearch] = useState<{ numeroCuenta?: string; nombre?: string; idStatus?: string }>({ numeroCuenta: '', nombre: '', idStatus: '' });
const [searchAnterior, setSearchAnterior] = useState<{ numeroCuenta?: string; nombre?: string; idStatus?: string }>({});
const [mounted, setMounted] = useState(false);
const onPageChange = (p: number) => {
setPage(p);
@@ -94,6 +93,8 @@ export default function TablaServiciosResponsable({ responsable, imprimirError }
useEffect(() => {
obtenerCatalogoStatus();
// mark as mounted to avoid rendering dynamic options during SSR hydration
setMounted(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -140,7 +141,7 @@ export default function TablaServiciosResponsable({ responsable, imprimirError }
</InputGroup>
</FormGroup>
</Col>
{/* llega hasta el 10 */}
{/* llega hasta el 10 */}
<Col md={3}>
<FormGroup>
<FormLabel>Status</FormLabel>
@@ -149,12 +150,14 @@ export default function TablaServiciosResponsable({ responsable, imprimirError }
<FaInfoCircle />
</InputGroup.Text>
<FormSelect
suppressHydrationWarning
value={search.idStatus}
onChange={(e) => setSearch(Prev => ({ ...Prev, idStatus: e.target.value}))}
className="rounded-4"
onKeyDown={(e) => {if (e.key === 'Enter') obtenerServicios()}}
>
<option value="">Status</option>
{status.slice(0, 9).map((s) => (
{mounted && status.slice(0, 9).map((s) => (
<option key={s.idStatus} value={s.idStatus}>
{s.status}
</option>
@@ -165,10 +168,11 @@ export default function TablaServiciosResponsable({ responsable, imprimirError }
</Col>
<Col md={3} className="d-flex align-items-end">
<Button
type="button"
<Button
type="submit"
className="w-100 rounded-5"
disabled={isLoading}>{isLoading ? 'Buscando...' : 'Buscar'}
disabled={isLoading}>
{isLoading ? 'Buscando...' : 'Buscar'}
</Button>
</Col>
</Row>
+55 -12
View File
@@ -3,6 +3,8 @@
import React, { useEffect, useState } from "react";
import { axiosInstance } from "@/api/config";
import type { AxiosError } from "axios";
import { Form } from "react-bootstrap";
import { FaUpload } from "react-icons/fa";
type Responsable = { tokenArchivo?: string };
@@ -38,20 +40,27 @@ export default function UploadArchivo({
formData.append('data', JSON.stringify(data));
formData.append(variableName, file, file.name);
console.log("Esta es la info que se mandara al backend:", formData);
updateIsLoading(true);
try {
const headers = responsable?.tokenArchivo ? { Authorization: `Bearer ${responsable.tokenArchivo}`, 'Content-Type': 'multipart/form-data' } : { 'Content-Type': 'multipart/form-data' };
const res = await axiosInstance.put(`/servicio/${path}`, formData, { headers });
try { localStorage.removeItem('idServicio'); } catch (error) {}
updateIsLoading(false);
imprimirMensaje(res.data?.message || 'Archivo subido');
if (typeof window !== 'undefined') window.location.href = '/responsable';
const res = await axiosInstance.put(`/servicio/${path}`, formData, {
headers: {
Authorization: `Bearer ${responsable.tokenArchivo}`,
'Content-Type': 'multipart/form-data',
},
});
try { localStorage.removeItem('idServicio'); }
catch (error) {}
updateIsLoading(false);
imprimirMensaje(res.data?.message || 'Archivo subido');
if (typeof window !== 'undefined') window.location.href = '/responsable';
} catch (err: unknown) {
updateIsLoading(false);
const axiosErr = err as AxiosError;
if (axiosErr?.response?.data) imprimirError(axiosErr.response.data as object);
else if (err instanceof Error) imprimirError(err.message);
else imprimirError({ message: 'Error desconocido' });
updateIsLoading(false);
const axiosErr = err as AxiosError;
if (axiosErr?.response?.data) imprimirError(axiosErr.response.data as object);
else if (err instanceof Error) imprimirError(err.message);
else imprimirError({ message: 'Error desconocido' });
}
};
@@ -65,13 +74,46 @@ export default function UploadArchivo({
return (
<div>
<div className="mb-3">
{/* <div className="mb-3">
<label className="form-label">Carta de {tipoCarta} (en formato .pdf, no se aceptan fotos).</label>
<input type="file" accept="application/pdf" className="form-control" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
<div className="mt-2 text-center">
<p className="h5">{file?.name || 'Arrastra aquí tu archivo o da click aquí para buscarlo.'}</p>
<p className="small">Tamaño máximo 20MB</p>
</div>
</div> */}
{/*
Falta corregir y arreglar
*/}
<div className="ph-5">
<Form.Group className="mb-3">
<Form.Label>
Carta de {tipoCarta} (en formato .pdf, no se aceptan fotos).
</Form.Label>
<div
className="border p-4 text-center rounded"
style={{ cursor: 'pointer' }}
onClick={() => document.getElementById('fileInput')?.click()}
>
<FaUpload size={40} className="mb-2"/>
<p className="mb-1">
{file?.name || 'Arrastra aquí tu archivo o da click aquí para buscar'}
</p>
<p className="is-size-6">Tamaño máximo 20MB</p>
<p className="is-size-7">Si al momento de elegir un archivo este no se selecciona, haga click en cancelar en la ventana emergente e intente de nuevo.
</p>
<input
type="file"
accept="application/pdf"
id="fileInput"
className="d-none"
onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
</div>
</Form.Group>
</div>
<div className="text-center my-4">
@@ -79,6 +121,7 @@ export default function UploadArchivo({
Enviar
</button>
</div>
</div>
);
}
+64 -24
View File
@@ -68,6 +68,30 @@ export default function ServicioSocialTabla({
}
}
const servicioSelectedResponsable = (row: ServicioSocialResponse | ServicioSocialConCasoEspecial) => {
if (!row) return;
if (idTipoUsuario === 2) {
console.log("Responsable seleccionó un servicio social:", row);
if (row.idServicio) {
localStorage.setItem("idServicio", String(row.idServicio));
}
router.push("/responsable/cuestionario");
}
}
const servicioSelectedResponsableCartaTermino = (row: ServicioSocialResponse | ServicioSocialConCasoEspecial) => {
if (!row) return;
if (idTipoUsuario === 2) {
console.log("Responsable seleccionó un servicio social para carta de termino:", row);
if (row.idServicio) {
localStorage.setItem("idServicio", String(row.idServicio));
}
router.push("/responsable/carta_termino");
}
}
const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEspecial) => {
if (!row) return;
@@ -156,16 +180,16 @@ const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEsp
<table className="table table-striped">
<thead>
<tr>
{columnaFechaRegistro && <th>Fecha Registro</th>}
<th>Número de Cuenta</th>
<th>Nombre</th>
<th>Carrera</th>
{columnaFechaInicio && <th>Fecha Inicio</th>}
{columnaFechaFin && <th>Fecha Fin</th>}
<th>Status</th>
{columnaCuestionarioCompleto && <th>Cuestionario Completo</th>}
{columnaCartaTermino && <th>Carta Termino</th>}
{columnaCuestionario && <th>Cuestionario</th>}
{columnaFechaRegistro && <th className="text-center align-middle">Fecha Registro</th>}
<th className="text-center align-middle">Número de Cuenta</th>
<th className="text-center align-middle">Nombre</th>
<th className="text-center align-middle">Carrera</th>
{columnaFechaInicio && <th className="text-center align-middle">Fecha Inicio</th>}
{columnaFechaFin && <th className="text-center align-middle">Fecha Fin</th>}
<th className="text-center align-middle">Status</th>
{columnaCuestionarioCompleto && <th className="text-center align-middle">Cuestionario Completo</th>}
{columnaCartaTermino && <th className="text-center align-middle">Carta Termino</th>}
{columnaCuestionario && <th className="text-center align-middle">Cuestionario</th>}
</tr>
</thead>
<tbody>
@@ -182,21 +206,24 @@ const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEsp
//idTipoUsuario === 1 ? servicioSelected(item) : undefined
//}
>
{columnaFechaRegistro && <td>{formatDate(item.createdAt)}</td>}
<td>{item.usuario?.usuario}</td>
<td>{item.usuario?.nombre}</td>
<td>{item.carrera?.carrera}</td>
{columnaFechaRegistro && <td className="text-center align-middle">{formatDate(item.createdAt)}</td>}
<td className="text-center align-middle">{item.usuario?.usuario}</td>
<td className="text-center align-middle">{item.usuario?.nombre}</td>
<td className="text-center align-middle">{item.carrera?.carrera}</td>
{/*
Poner si quieres hacer condicional para no mostrar infromacion
? formatDate(item.fechaInicio) : "-" */}
{columnaFechaInicio && (
<td>{formatDate(item.fechaInicio)}</td>
<td className="text-center align-middle">{formatDate(item.fechaInicio)}</td>
)}
{columnaFechaFin && <td>{formatDate(item.fechaFin)}</td>}
<td>
<td className="text-center align-middle">
{columnasResponsable && item.status?.idStatus === 7 ? (
<button
className="btn btn-sm btn-danger"
className="badge bg-danger border-0"
onClick={() =>
onRowAction?.(item, "carta_aceptacion")
}
@@ -214,15 +241,22 @@ const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEsp
)}
</td>
{columnaCartaTermino && (
<td>{item.cartaTermino ? (
<span className="badge bg-success">Completado</span>
<td className="text-center align-middle"
style={{ cursor: idTipoUsuario === 2 ? 'pointer' : 'default'}}
onClick={() => (
idTipoUsuario === 2 ? servicioSelectedResponsableCartaTermino(item) : undefined
)}
>
{item.cartaTermino ? (
<span className="badge"></span>
) : (
<span className="badge bg-danger">No Completado</span>
)}</td>
)}
{columnaCuestionarioCompleto && (
<td>{item.cuestionarioCompletado ? (
<td className="text-center align-middle"
>{item.cuestionarioCompletado ? (
<span className="badge bg-success">Completado</span>
) : (
<span className="badge bg-danger">No Completado</span>
@@ -231,9 +265,14 @@ const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEsp
{columnaCuestionario && (
<td>
<td className="text-center align-middle"
style={{ cursor: idTipoUsuario === 2 ? 'pointer' : 'default'}}
onClick={() => (
idTipoUsuario === 2 ? servicioSelectedResponsable(item) : undefined
)}
>
{item.cuestionarioCompletado ? (
<span className="badge bg-success">Completado</span>
<span className="badge"></span>
) : (
<span className="badge bg-danger">No Completado</span>
)}
@@ -280,6 +319,7 @@ const servicioSelected = (row: ServicioSocialResponse | ServicioSocialConCasoEsp
);
}
function formatDate(value?: string | null): string {
if (!value) return "";
@@ -287,7 +327,7 @@ function formatDate(value?: string | null): string {
const d = new Date(value.includes("T") ? value : `${value}T00:00:00`);
if (isNaN(d.getTime())) {
console.warn("⚠️ Fecha inválida:", value);
// console.warn("⚠️ Fecha inválida:", value);
return "";
}
+27
View File
@@ -0,0 +1,27 @@
// Funciones (SIEMPRE primero)
@import "bootstrap/scss/functions";
// VARIABLES PERSONALIZADAS
$primary: #5a2d82;
$secondary: #198754;
$border-radius: 1.5rem;
// Variables base (SIN maps ni dark)
@import "bootstrap/scss/variables";
// Mixins
@import "bootstrap/scss/mixins";
// Core CSS
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
// 6Componentes que se usan
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/utilities/api";
+35 -14
View File
@@ -1,4 +1,4 @@
@import 'bootstrap/scss/functions';
@import 'bootstrap/dist/css/bootstrap.min.css';
$azul: #003d79;
$dorado: #bd8c01;
@@ -19,8 +19,7 @@ $font-family-sans-serif: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI'
'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/variables-dark';
$custom-colors: (
'azul': $azul,
@@ -65,7 +64,7 @@ $custom-colors: (
}
$theme-colors: map-merge($theme-colors, $custom-colors);
$theme-colors: if(variable-exists("theme-colors"), map-merge($theme-colors, $custom-colors), $custom-colors);
.form-registro {
.form-floating {
@@ -93,9 +92,6 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
}
}
@import 'bootstrap/scss/maps';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/bootstrap';
.aviso {
@@ -137,7 +133,7 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
height: 2px;
background-color: $azul;
@include media-breakpoint-up(md) {
@media (min-width: 768px) {
left: 10rem;
}
}
@@ -208,12 +204,12 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
}
.full-center {
//flex-grow-1 d-flex flex-column justify-content-center align-items-center
@extend .flex-grow-1;
@extend .d-flex;
@extend .flex-column;
@extend .justify-content-center;
@extend .align-items-center;
/* Equivalent of Bootstrap utility classes without using @extend */
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.minNavbar {
@@ -268,3 +264,28 @@ $theme-colors: map-merge($theme-colors, $custom-colors);
.imageFlip:hover {
transform: rotateY(360deg);
}
/* Utility classes for custom colors (restore bg-<name> / text-<name> used in JSX)
These are minimal helpers so we don't need to import Bootstrap SCSS utilities. */
.bg-azul { background-color: #003d79 !important; }
.text-azul { color: #003d79 !important; }
.bg-dorado { background-color: #bd8c01 !important; }
.text-dorado { color: #bd8c01 !important; }
.bg-morado { background-color: #714dd2 !important; }
.text-morado { color: #714dd2 !important; }
.bg-verde { background-color: #48c78e !important; }
.text-verde { color: #48c78e !important; }
.bg-red-guapa { background-color: #9e1d15 !important; }
.text-red-guapa { color: #9e1d15 !important; }
.bg-blue-guapa { background-color: #3d6cb4 !important; }
.text-blue-guapa { color: #3d6cb4 !important; }
.bg-yellow-guapa { background-color: #eca728 !important; }
.text-yellow-guapa { color: #eca728 !important; }
/* Restore common Bootstrap text utilities if overridden */
.text-white { color: #fff !important; }
.text-dark { color: #000 !important; }
.text-muted { color: #6c757d !important; }
.text-body { color: var(--bs-body-color, #212529) !important; }
.fw-bold { font-weight: 700 !important; }
+19 -5
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}