Cambio en diseños
This commit is contained in:
@@ -15,7 +15,7 @@ export default function Login() {
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:4000/login', {
|
||||
const response = await axios.post('https://venus.acatlan.unam.mx/servicios_pcpuma_test/login', {
|
||||
email: value,
|
||||
password: password,
|
||||
});
|
||||
|
||||
+13
-18
@@ -8,20 +8,17 @@ import axios from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// Estilos CSS adicionales para el input group
|
||||
const inputGroupStyles = `
|
||||
|
||||
`;
|
||||
|
||||
export default function Page() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post("https://venus.acatlan.unam.mx/servicios_pcpuma_test/login", {
|
||||
const response = await axios.post("http://https://venus.acatlan.unam.mx/servicios_pcpuma_test/login", {
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
@@ -37,19 +34,19 @@ export default function Page() {
|
||||
setPassword("");
|
||||
setError("");
|
||||
|
||||
// Opcional: mostrar mensaje de éxito
|
||||
alert("Login exitoso. Ahora puedes acceder a Carga y Visualización.");
|
||||
|
||||
|
||||
} catch (err: any) {
|
||||
const message =
|
||||
err.response?.data?.message || err.message || "Error al iniciar sesión";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Inyectar estilos CSS */}
|
||||
|
||||
<motion.main
|
||||
className="auth-page flex-fill flex-grow-1 d-flex flex-column justify-content-center align-items-center"
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
@@ -57,7 +54,8 @@ export default function Page() {
|
||||
transition={{ delay: 0, duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-dorado">Bienvenido al sistema de</h2>
|
||||
<h1 className="text-azul">Servicios PC PUMA y CEDETEC</h1>
|
||||
<h1 className="text-dorado">Servicios PC PUMA y CEDETEC</h1>
|
||||
|
||||
<SimpleInput
|
||||
label="Usuario"
|
||||
className={{
|
||||
@@ -66,6 +64,7 @@ export default function Page() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Contraseña"
|
||||
className={{
|
||||
@@ -80,13 +79,9 @@ export default function Page() {
|
||||
{error && <p className="text-danger text-center mb-3">{error}</p>}
|
||||
|
||||
<div className="d-flex gap-3">
|
||||
<Button onClick={handleLogin}>Iniciar Sesión</Button>
|
||||
{/* <Button
|
||||
onClick={() => router.replace("/visualizacion")}
|
||||
className="btn-success"
|
||||
>
|
||||
Iniciar Sin Contraseña
|
||||
</Button> */}
|
||||
<Button onClick={handleLogin} disabled={loading}>
|
||||
{loading ? "Iniciando sesión..." : "Iniciar Sesión"}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.main>
|
||||
</>
|
||||
|
||||
+60
-34
@@ -5,6 +5,7 @@ import FuenteCard from "@/components/fuenteCard";
|
||||
import CargaArchivo from "@/components/cargaArchivo";
|
||||
import Button from "@/components/button";
|
||||
import { useAuthRedirect } from "@/hooks/auth";
|
||||
import { getUserFromToken } from "@/components/Hook";
|
||||
|
||||
interface Fuente {
|
||||
nombre: string;
|
||||
@@ -16,36 +17,39 @@ interface Fuente {
|
||||
export default function Dashboard() {
|
||||
const loading = useAuthRedirect();
|
||||
const [fuentes, setFuentes] = useState<Fuente[]>([]);
|
||||
const [descargando, setDescargando] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMovimientos = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
const fetchMovimientos = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
const res = await fetch("https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/movimientos", {
|
||||
const res = await fetch(
|
||||
"https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/movimientos",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
console.log("Respuesta del backend:", data);
|
||||
const data = await res.json();
|
||||
|
||||
const fuentesConvertidas: Fuente[] = data.move.map((mov: any) => ({
|
||||
nombre: `${mov.reporte}: ${mov.id_mov}`,
|
||||
id_mov: mov.id_mov,
|
||||
fecha: new Date(mov.fecha_mov).toLocaleString("es-MX", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
activa: data.button,
|
||||
}));
|
||||
const fuentesConvertidas: Fuente[] = data.move.map((mov: any) => ({
|
||||
nombre: `${mov.reporte}: ${mov.id_mov}`,
|
||||
id_mov: mov.id_mov,
|
||||
fecha: new Date(mov.fecha_mov).toLocaleString("es-MX", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}),
|
||||
activa: data.button,
|
||||
}));
|
||||
|
||||
setFuentes(fuentesConvertidas);
|
||||
};
|
||||
setFuentes(fuentesConvertidas);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMovimientos();
|
||||
}, []);
|
||||
|
||||
@@ -56,13 +60,18 @@ export default function Dashboard() {
|
||||
return;
|
||||
}
|
||||
|
||||
setDescargando(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/download", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const response = await fetch(
|
||||
"https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/download",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json();
|
||||
@@ -82,6 +91,8 @@ export default function Dashboard() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
alert(`Error al descargar: ${error.message}`);
|
||||
} finally {
|
||||
setDescargando(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,18 +118,33 @@ export default function Dashboard() {
|
||||
fecha={f.fecha}
|
||||
activa={f.activa}
|
||||
id_mov={f.id_mov}
|
||||
onValidated={fetchMovimientos} // ← actualiza fuentes al validar
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 style={{ marginTop: "2rem" }} className="text-xl font-semibold mt-6 mb-4">Carga</h2>
|
||||
<div>
|
||||
<CargaArchivo />
|
||||
</div>
|
||||
{getUserFromToken()?.tipo === "LOAD" && (
|
||||
<>
|
||||
<h2
|
||||
style={{ marginTop: "2rem" }}
|
||||
className="text-xl font-semibold mt-4 mb-4"
|
||||
>
|
||||
Carga
|
||||
</h2>
|
||||
<div>
|
||||
<CargaArchivo />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="text-xl font-semibold mt-6 mb-4">Descarga de Datos</h2>
|
||||
<Button onClick={handleDownload} className="mb-3" variant="azul">
|
||||
Descarga Base de Datos
|
||||
<h2 className="text-xl font-semibold mt-5 mb-4">Descarga de Datos</h2>
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={descargando}
|
||||
className="mb-3"
|
||||
variant="azul"
|
||||
>
|
||||
{descargando ? "Descargando..." : "Descarga Base de Datos"}
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function Consulta() {
|
||||
<main>
|
||||
<div>
|
||||
<div>
|
||||
<strong className="text-dorado">Servicios PC PUMA y CEDETEC</strong>
|
||||
<h2 className="text-xl font-semibold mb-4">Servicios habilitados</h2>
|
||||
|
||||
<Input
|
||||
@@ -103,7 +104,7 @@ export default function Consulta() {
|
||||
borderBottom: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
{key.toUpperCase()}
|
||||
{key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
interface JwtPayload {
|
||||
sub: string,
|
||||
email: string,
|
||||
tipo: string
|
||||
}
|
||||
|
||||
export function getUserFromToken(): JwtPayload | null {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const payload: JwtPayload = jwtDecode(token);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.error("Token inválido:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,23 @@ import Button from "./button";
|
||||
const CargaArchivo: React.FC = () => {
|
||||
const [archivo, setArchivo] = useState<File | null>(null);
|
||||
const [errores, setErrores] = useState<string[] | null>(null);
|
||||
const [loading, setLoading] = useState(false); // ← estado de carga
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) setArchivo(file);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) setArchivo(file);
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!archivo) return;
|
||||
|
||||
@@ -23,6 +34,8 @@ const CargaArchivo: React.FC = () => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", archivo);
|
||||
|
||||
setLoading(true); // ← empieza a subir
|
||||
|
||||
try {
|
||||
const res = await fetch("https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/load", {
|
||||
method: "POST",
|
||||
@@ -40,7 +53,7 @@ const CargaArchivo: React.FC = () => {
|
||||
);
|
||||
setArchivo(null);
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
setErrores(data.errors); // ← Mostrar ventana si hay errores
|
||||
setErrores(data.errors);
|
||||
}
|
||||
} else {
|
||||
console.error(data);
|
||||
@@ -48,35 +61,62 @@ const CargaArchivo: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error en la solicitud: ${err}`);
|
||||
} finally {
|
||||
setLoading(false); // ← termina de subir
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
<div className="w-full max-w-xl mx-auto">
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
accept=".xls,.xlsx"
|
||||
onChange={handleChange}
|
||||
className="mb-4 block text-sm"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Button onClick={handleUpload} disabled={!archivo} variant="azul">
|
||||
Subir archivo
|
||||
</Button>
|
||||
|
||||
<div
|
||||
className="rounded p-5 text-center bg-white shadow-lg transition hover:shadow-xl"
|
||||
style={{
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById("fileInput")?.click()}
|
||||
>
|
||||
<p className="text-gray-600 mb-2">
|
||||
Arrastra y suelta un archivo aquí o{" "}
|
||||
<span className="text-blue-600 font-semibold">
|
||||
haz clic para seleccionarlo
|
||||
</span>.
|
||||
</p>
|
||||
<i className="bi bi-upload fs-1 text-primary"></i>
|
||||
|
||||
{archivo && (
|
||||
<p className="my-0 text-green-600 font-semibold">
|
||||
Archivo seleccionado: {archivo.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button onClick={handleUpload} disabled={!archivo || loading} variant="azul">
|
||||
{loading ? "Subiendo..." : "Subir archivo"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Modal de errores */}
|
||||
{errores && (
|
||||
<div className="">
|
||||
<div className="mt-6 p-4 bg-red-100 border border-red-300 rounded">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-xl font-semibold mt-6 mb-4">Reporte de Errores</h2>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setErrores(null)}
|
||||
className="text-sm"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-red-800">Errores detectados</h2>
|
||||
<Button variant="danger" onClick={() => setErrores(null)} className="text-sm">
|
||||
<i className="fas fa-times"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="list-disc list-inside text-sm max-h-60 overflow-y-auto">
|
||||
<ul className="list-disc list-inside text-sm max-h-60 overflow-y-auto text-red-700">
|
||||
{errores.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Button from "./button";
|
||||
|
||||
type FuenteCardProps = {
|
||||
nombre: string;
|
||||
fecha: string;
|
||||
activa: boolean;
|
||||
id_mov: number;
|
||||
onValidated?: () => void;
|
||||
};
|
||||
|
||||
const FuenteCard: React.FC<FuenteCardProps> = ({ nombre, fecha, activa, id_mov }) => {
|
||||
const FuenteCard: React.FC<FuenteCardProps> = ({
|
||||
nombre,
|
||||
fecha,
|
||||
activa,
|
||||
id_mov,
|
||||
onValidated,
|
||||
}) => {
|
||||
const handleActivar = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
@@ -16,17 +25,21 @@ const FuenteCard: React.FC<FuenteCardProps> = ({ nombre, fecha, activa, id_mov }
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/activar-servicios/${id_mov}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
const res = await fetch(
|
||||
`https://venus.acatlan.unam.mx/servicios_pcpuma_test/excel/activar-servicios/${id_mov}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
alert("Usuarios validados correctamente");
|
||||
onValidated?.(); // ← Recargar fuentes
|
||||
} else {
|
||||
alert(`Error: ${data.message || "No se pudo validar"}`);
|
||||
}
|
||||
@@ -40,21 +53,24 @@ const FuenteCard: React.FC<FuenteCardProps> = ({ nombre, fecha, activa, id_mov }
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-sm">{nombre}</span>
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full ${activa ? "bg-green-500" : "bg-red-500"}`}
|
||||
className={`w-3 h-3 rounded-full ${activa ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-700 leading-tight">
|
||||
Fecha de carga:<br />
|
||||
Fecha de carga:
|
||||
<br />
|
||||
{fecha}
|
||||
</p>
|
||||
|
||||
{activa && (
|
||||
<button
|
||||
<Button
|
||||
onClick={handleActivar}
|
||||
className="mt-3 px-3 py-1 btn btn-success btn-sm rounded-pill"
|
||||
className="mb-3 px-3 py-1 mt-3"
|
||||
variant="success"
|
||||
>
|
||||
Validar
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,15 +8,15 @@ import axios from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// Estilos CSS adicionales para el input group
|
||||
|
||||
export default function LoginPage() {
|
||||
export default function Page() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post("https://venus.acatlan.unam.mx/servicios_pcpuma_test/login", {
|
||||
email: email,
|
||||
@@ -35,26 +35,27 @@ export default function LoginPage() {
|
||||
setError("");
|
||||
window.location.reload();
|
||||
|
||||
|
||||
// Opcional: mostrar mensaje de éxito
|
||||
alert("Login exitoso. Ahora puedes acceder a Carga y Visualización.");
|
||||
// Redirigir al usuario
|
||||
} catch (err: any) {
|
||||
const message =
|
||||
err.response?.data?.message || err.message || "Error al iniciar sesión";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.main
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
className="auth-page flex-fill flex-grow-1 d-flex flex-column justify-content-center align-items-center"
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0, duration: 0.5 }}
|
||||
>
|
||||
<h2 className="text-dorado fw-bold">Bienvenido al sistema de</h2>
|
||||
<h1 className="text-azul fw-bold">Servicios PC Puma</h1>
|
||||
<h2 className="text-dorado">Bienvenido al sistema de</h2>
|
||||
<h1 className="text-dorado">Servicios PC PUMA y CEDETEC</h1>
|
||||
|
||||
<SimpleInput
|
||||
label="Usuario"
|
||||
className={{
|
||||
@@ -63,10 +64,13 @@ export default function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Contraseña"
|
||||
className={{
|
||||
container: "w-300px mb-3",
|
||||
input: "form-control",
|
||||
button: "btn btn-outline-secondary",
|
||||
}}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
@@ -74,9 +78,11 @@ export default function LoginPage() {
|
||||
|
||||
{error && <p className="text-danger text-center mb-3">{error}</p>}
|
||||
|
||||
<Button onClick={handleLogin} className="rounded-pill" variant="azul">
|
||||
Iniciar Sesión
|
||||
</Button>
|
||||
<div className="d-flex gap-3">
|
||||
<Button onClick={handleLogin} disabled={loading}>
|
||||
{loading ? "Iniciando sesión..." : "Iniciar Sesión"}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.main>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user