Merge branch 'Lino' of https://github.com/jls846/front-censo into Sarabia

This commit is contained in:
jls846
2025-11-04 13:24:18 -06:00
16 changed files with 1012 additions and 439 deletions
@@ -7,7 +7,7 @@ import toast from "react-hot-toast";
import "../../styles/layout/ForgotPasswordPage.scss"; // ajusta ruta si tu proyecto la tiene en otro sitio
import Link from "next/link";
export default function ForgotPasswordPage() {
export default function Page() {
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
+1 -2
View File
@@ -9,7 +9,7 @@ import axios from "axios";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
function CrearCuenta() {
export default function Page() {
const [nombre, setNombre] = useState("");
const [correo, setCorreo] = useState("");
const [password, setPassword] = useState("");
@@ -116,4 +116,3 @@ function CrearCuenta() {
</section>
);
}
export default CrearCuenta;
+6 -374
View File
@@ -1,379 +1,11 @@
"use client";
import { useState, useEffect } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast";
import "../../styles/layout/agregarEquipo.scss";
import dynamic from "next/dynamic";
const AgregarEquipo = dynamic(() => import("@/components/AgregarEquipo"), {
ssr: false,
});
export default function Page() {
const [formData, setFormData] = useState({
serie: "",
marca: "",
modelo: "",
tipoEquipo: "",
estado: "",
sistemaOperativo: "",
procesador: "",
tipoUso: "",
observaciones: "",
adscripcion: "",
lugar: "",
responsable: "",
});
// Tipos de datos
interface TipoUso {
id_tipo_uso: number;
tipo_uso: string;
}
interface Estado {
id_estado: number;
estado: string;
}
interface TipoEquipo {
id_tipo_de_equipo: number;
tipo_equipo: string;
}
interface SistemaOperativo {
id_sistema_operativo: number;
sistema_operativo: string;
}
interface Procesador {
id_procesador: number;
procesador: string;
}
interface Marca {
id_marca: number;
tipo_marca: string;
}
interface Adscripcion {
id_adscripcion: number;
adscripcion: string;
}
// Estados
const [tiposUso, setTiposUso] = useState<TipoUso[]>([]);
const [marcas, setMarcas] = useState<Marca[]>([]);
const [estados, setEstados] = useState<Estado[]>([]);
const [adscripciones, setAdscripciones] = useState<Adscripcion[]>([]);
const [tiposEquipo, setTiposEquipo] = useState<TipoEquipo[]>([]);
const [sistemasOperativos, setSistemasOperativos] = useState<
SistemaOperativo[]
>([]);
const [procesadores, setProcesadores] = useState<Procesador[]>([]);
const [suggestions, setSuggestions] = useState({
adscripcion: [] as string[],
});
const mostrarCamposComputadora = formData.tipoEquipo !== "Impresora";
const api_url = process.env.NEXT_PUBLIC_API_URL;
useEffect(() => {
const fetchData = async () => {
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
try {
const [
usosRes,
marcasRes,
estadosRes,
adscripcionesRes,
tiposEquipoRes,
sistemasOperativosRes,
procesadoresRes,
] = await Promise.all([
axios.get(`${api_url}/equipos/usos`, { headers }),
axios.get(`${api_url}/equipos/marcas`, { headers }),
axios.get(`${api_url}/equipos/estados`, { headers }),
axios.get(`${api_url}/equipos/adscripciones`, { headers }),
axios.get(`${api_url}/equipos/tipos-equipo`, { headers }),
axios.get(`${api_url}/equipos/sistemas-operativos`, { headers }),
axios.get(`${api_url}/equipos/procesadores`, { headers }),
]);
setTiposUso(usosRes.data);
setMarcas(marcasRes.data);
setEstados(estadosRes.data);
setAdscripciones(adscripcionesRes.data);
setTiposEquipo(tiposEquipoRes.data);
setSistemasOperativos(sistemasOperativosRes.data);
setProcesadores(procesadoresRes.data);
console.log(adscripciones);
} catch (err) {
if (axios.isAxiosError(err)) {
if (err.response) {
toast.error(
err.response.data?.message || "No se pudo conectar con el API"
);
} else if (err.request) {
toast.error("No se pudo conectar con el servidor");
} else {
toast.error("Ocurrió un error inesperado");
}
} else {
toast.error("Ocurrió un error inesperado");
}
}
};
fetchData();
}, [api_url]);
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleSelectSuggestion = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setSuggestions((prev) => ({ ...prev, [field]: [] }));
};
const handleGuardar = async () => {
try {
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
await axios.post(`${api_url}/equipos/crear`, formData, { headers });
toast.success("Equipo guardado correctamente");
setFormData({
serie: "",
marca: "",
modelo: "",
tipoEquipo: "",
estado: "",
sistemaOperativo: "",
procesador: "",
tipoUso: "",
observaciones: "",
adscripcion: "",
lugar: "",
responsable: "",
});
} catch (err) {
toast.error("Error al guardar el equipo");
}
};
const handleCancelar = () => {
toast.error("Acción cancelada");
};
return (
<div className="agregarEquipoContainer">
<div className="innerContainer">
<h2 className="information">Agregar Nuevo Equipo</h2>
<form className="equipoForm">
{/* Columna Izquierda */}
<div className="column">
<div className="formGroup">
<label>Serie</label>
<input
required
type="text"
placeholder="Ingresa serie"
value={formData.serie}
onChange={(e) => handleInputChange("serie", e.target.value)}
/>
</div>
<div className="formGroup">
<label>Marca</label>
<select
required
value={formData.marca}
onChange={(e) => handleInputChange("marca", e.target.value)}
>
<option value="">Selecciona una marca</option>
{marcas.map((m) => (
<option key={m.id_marca} value={m.tipo_marca}>
{m.tipo_marca}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Modelo</label>
<input
type="text"
placeholder="Ingresa modelo"
value={formData.modelo}
onChange={(e) => handleInputChange("modelo", e.target.value)}
/>
</div>
<div className="formGroup">
<label>Tipo de equipo</label>
<select
required
value={formData.tipoEquipo}
onChange={(e) =>
handleInputChange("tipoEquipo", e.target.value)
}
>
<option value="">Selecciona tipo de equipo</option>
{tiposEquipo.map((t) => (
<option key={t.id_tipo_de_equipo} value={t.tipo_equipo}>
{t.tipo_equipo}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Estado</label>
<select
required
value={formData.estado}
onChange={(e) => handleInputChange("estado", e.target.value)}
>
<option value="">Selecciona estado</option>
{estados.map((e) => (
<option key={e.id_estado} value={e.estado}>
{e.estado}
</option>
))}
</select>
</div>
</div>
{/* Columna Centro */}
<div className="column">
<div className="formGroup">
<label>Tipo de uso</label>
<select
required
value={formData.tipoUso}
onChange={(e) => handleInputChange("tipoUso", e.target.value)}
>
<option value="">Selecciona tipo de uso</option>
{tiposUso.map((t) => (
<option key={t.id_tipo_uso} value={t.tipo_uso}>
{t.tipo_uso}
</option>
))}
</select>
</div>
{mostrarCamposComputadora && (
<>
<div className="formGroup">
<label>Procesador</label>
<select
value={formData.procesador}
onChange={(e) =>
handleInputChange("procesador", e.target.value)
}
>
<option value="">Selecciona procesador</option>
{procesadores.map((p) => (
<option key={p.id_procesador} value={p.procesador}>
{p.procesador}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Sistema operativo</label>
<select
value={formData.sistemaOperativo}
onChange={(e) =>
handleInputChange("sistemaOperativo", e.target.value)
}
>
<option value="">Selecciona sistema operativo</option>
{sistemasOperativos.map((so) => (
<option
key={so.id_sistema_operativo}
value={so.sistema_operativo}
>
{so.sistema_operativo}
</option>
))}
</select>
</div>
</>
)}
<div className="formGroup">
<label>Adscripción</label>
<input
required
type="text"
placeholder="Selecciona adscripción"
value={formData.adscripcion}
onChange={(e) =>
handleInputChange("adscripcion", e.target.value)
}
/>
{suggestions.adscripcion.length > 0 && (
<ul className="suggestions">
{suggestions.adscripcion.map((s) => (
<li
key={s}
onClick={() => handleSelectSuggestion("adscripcion", s)}
>
{s}
</li>
))}
</ul>
)}
</div>
</div>
<div className="column">
<div className="formGroup">
<label>Lugar</label>
<textarea
placeholder="Ingresa lugar"
value={formData.lugar}
onChange={(e) => handleInputChange("lugar", e.target.value)}
rows={5}
className="textAreaLarge"
/>
</div>
<div className="formGroup">
<label>Observaciones</label>
<textarea
placeholder="Ingresa observaciones"
value={formData.observaciones}
onChange={(e) =>
handleInputChange("observaciones", e.target.value)
}
rows={5}
className="textAreaLarge"
/>
</div>
<div className="formActions">
<button
type="button"
className="btnGuardar"
onClick={handleGuardar}
>
Guardar
</button>
<button
type="button"
className="btnCancelar"
onClick={handleCancelar}
>
Cancelar
</button>
</div>
</div>
</form>
</div>
</div>
);
return <AgregarEquipo />;
}
+1
View File
@@ -46,6 +46,7 @@ export default function Dashboard() {
}
} catch (error) {
toast.error("Error No se encontro el equipo");
router.push(`/AgregarEquipo?equipoId=${code}`);
}
toast(`${code}`);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import Header from "../components/header";
import { Inter, Montserrat } from "next/font/google";
import "../app/styles/base/globales.scss";
import { Toaster } from "react-hot-toast";
import "../app/styles/base/globales.scss";
const inter = Inter({
subsets: ["latin"],
@@ -21,7 +21,7 @@ export default function RootLayout({
}) {
return (
<html lang="es" className={`${inter.variable} ${montserrat.variable}`}>
<body suppressHydrationWarning>
<body>
<Header />
<Toaster position="top-left" reverseOrder={false} />
{children}
+81 -4
View File
@@ -1,3 +1,10 @@
.pestaña {
position: absolute;
top: 50%;
left: 62%;
transform: translate(-50%, -50%);
}
/* Contenedor principal del reporte */
.reporte-layout {
display: flex;
@@ -28,7 +35,61 @@
color: #0056b3;
}
/* Buscador */
.pestaña span:hover {
color: #b8870b; /* cambia de color al pasar el cursor */
}
.titulo-reporte {
font-weight: bold; /* negrita */
font-size: 24px; /* tamaño de texto */
font-family: Arial, sans-serif;
}
/*BUSCADOR*/
input[type="text"] {
width: 500px; /* más ancho, más largo */
padding: 7px; /* mantiene espacio cómodo dentro */
font-size: 18px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 8px 12px;
font-size: 16px;
border: none;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.contenedor {
margin-left: 80px;
}
.filtro-rec {
border: 2px solid #003366; /* Color del borde */
border-radius: 10px; /* Bordes redondeados */
padding: 10px; /* Espacio interno */
margin: 80px 550px; /* Separación del resto del contenido */
background-color: #f9f9f9;
position: relative;
left: 380px;
font-family: Arial, sans-serif;
top: -570px; /* sube 50px respecto a su posición original */
}
select:focus {
border-color: #003366;
box-shadow: 0 0 4px rgba(0, 51, 102, 0.3);
outline: none;
}
form[action="/buscar"] {
display: flex;
gap: 10px;
@@ -108,8 +169,9 @@ form[action="/buscar"] button:hover {
/* Botones del filtro */
.botones-filtro {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: flex-end; /* botones a la derecha */
gap: 10px; /* espacio entre botones */
margin-top: 15px; /* separación del resto del formulario */
}
.botones-filtro .aplicar,
@@ -123,6 +185,19 @@ form[action="/buscar"] button:hover {
cursor: pointer;
}
.tabla-rec {
border: 2px solid #003366; /* Color del borde */
border-radius: 10px; /* Bordes redondeados */
padding: 10px; /* Espacio interno */
margin: 50px 700px 0 80px; /* superior, derecha, inferior, izquierda */
background-color: #f9f9f9;
font-family: Arial, sans-serif;
height: 400px;
}
.boton-descargar {
padding: 8px 12px;
}
.botones-filtro .aplicar {
background-color: #0056b3;
}
@@ -143,6 +218,8 @@ form[action="/buscar"] button:hover {
color: white;
border-radius: 4px;
cursor: pointer;
right: 30px;
margin-left: 700px;
width: 100%;
}
@@ -166,4 +243,4 @@ form[action="/buscar"] button:hover {
.boton-descargar {
max-width: 500px;
}
}
}
+17 -5
View File
@@ -69,16 +69,28 @@ label {
display: block;
text-align: start;
width: 100%;
margin-bottom: 5px;
font-weight: bold;
color: #333;
font-size: 14px;
}
img {
object-fit: cover;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
select {
color: #333;
font-size: 14px;
width: 100%;
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: white;
background-position: right 10px center;
background-repeat: no-repeat;
background-size: 12px;
}
+5 -7
View File
@@ -19,7 +19,6 @@
.barNavigation li {
transition: all 0.3s ease;
cursor: pointer;
color: white;
width: auto;
transition: background-color 0.3s ease;
font-size: 1.5rem;
@@ -96,7 +95,6 @@
display: flex;
justify-content: center;
align-items: center;
color: white;
width: 100%;
height: 100%;
font-size: 1.5rem;
@@ -111,7 +109,7 @@
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 1.5rem;
font-size: 2rem;
display: flex;
align-items: center;
justify-content: center;
@@ -140,7 +138,7 @@
.barNavigation ul {
position: absolute;
background-color: #003e79;
background-color: #f4f5f7;
flex-direction: column;
padding: 10px 0;
align-items: center;
@@ -158,7 +156,7 @@
.barNavigation li:hover {
background-color: #d59f0f;
color: white;
color: black;
border-radius: 0;
border: none;
}
@@ -172,7 +170,7 @@
.logout-button {
margin-top: 10px;
font-size: 1rem;
font-size: 1.75rem;
padding: 6px 12px;
max-width: 180px;
}
@@ -183,6 +181,6 @@
}
.subMenu a span {
color: #fff;
color: black;
}
}
+55 -6
View File
@@ -9,13 +9,12 @@
.innerContainer {
max-width: 1000px;
width: 1000px;
width: 100%;
margin: 0 auto;
padding: 20px;
padding: 1rem 2rem;
}
h2 {
margin-top: 1rem;
text-align: center;
color: #0056b3;
margin-bottom: 20px;
@@ -25,7 +24,7 @@
.equipoForm {
display: flex;
gap: 30px;
gap: 1.5rem;
.column {
flex: 1;
@@ -88,7 +87,7 @@ textarea {
height: 100%;
.btnGuardar {
background-color: #ffb700;
color: #0056b3;
color: #ffffff;
border: none;
padding: 10px 20px;
border-radius: 6px;
@@ -117,8 +116,58 @@ textarea {
}
}
.formGroup {
position: relative;
}
.suggestions {
position: absolute; // mantiene fuera del flujo
top: 100%; // justo debajo del input
left: 0;
width: 100%;
background: #fff;
border: 1px solid #ccc;
border-radius: 6px;
list-style: none;
margin: 4px 0 0 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
z-index: 10;
font-size: 12px;
}
.suggestions li {
padding: 8px;
cursor: pointer;
&:hover {
background: #f2f2f2;
}
}
@media (max-width: 1075px) {
.equipoForm {
flex-wrap: wrap;
max-width: 100%;
.column {
flex: 1 1 calc(50% - 15px);
min-width: 250px;
.formGroup {
input,
select {
width: 100%;
font-size: 16px;
}
}
}
}
}
/* Responsive: en móvil, una columna */
@media (max-width: 768px) {
@media (max-width: 600px) {
.agregarEquipoContainer .innerContainer {
padding: 15px;
max-width: 90vw;
+493
View File
@@ -0,0 +1,493 @@
"use client";
import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast";
import "../app/styles/layout/agregarEquipo.scss";
import { AREAS } from "@/data/areas";
import { SO_POR_EQUIPO } from "@/data/so_por_equipo";
import { PROCESADORES_POR_EQUIPO } from "@/data/procesadores";
import { useRouter } from "next/navigation";
export default function Page() {
const searchParams = useSearchParams();
const inventario = searchParams.get("equipoId");
const router = useRouter();
const [formData, setFormData] = useState({
inventario: "",
serie: "",
marca: "",
modelo: "",
tipoEquipo: "",
estado: "",
sistemaOperativo: "",
procesador: "",
tipoUso: "",
observaciones: "",
adscripcion: "",
lugar: "",
responsable: "",
tipoPeriferico: "",
});
useEffect(() => {
if (inventario) {
setFormData((prev) => ({ ...prev, inventario }));
}
}, [inventario]);
// Tipos de datos
interface TipoUso {
id_tipo_uso: number;
tipo_uso: string;
}
interface Estado {
id_estado: number;
estado: string;
}
interface TipoEquipo {
id_tipo_de_equipo: number;
tipo_equipo: string;
}
interface SistemaOperativo {
id_sistema_operativo: number;
sistema_operativo: string;
}
interface Procesador {
id_procesador: number;
procesador: string;
}
interface Marca {
id_marca: number;
marca: string;
}
interface Adscripcion {
id_adscripcion: number;
adscripcion: string;
}
// Estados
const [tiposUso, setTiposUso] = useState<TipoUso[]>([]);
const [marcas, setMarcas] = useState<Marca[]>([]);
const [estados, setEstados] = useState<Estado[]>([]);
const [adscripciones, setAdscripciones] = useState<Adscripcion[]>([]);
const [tiposEquipo, setTiposEquipo] = useState<TipoEquipo[]>([]);
const [sistemasOperativos, setSistemasOperativos] = useState<
SistemaOperativo[]
>([]);
const [procesadores, setProcesadores] = useState<Procesador[]>([]);
const [perifericos, setPerifericos] = useState([]);
const [suggestions, setSuggestions] = useState({
adscripcion: [] as string[],
});
const mostrarCamposComputadora = formData.tipoEquipo !== "PERIFÉRICO";
const api_url = process.env.NEXT_PUBLIC_API_URL;
useEffect(() => {
const fetchData = async () => {
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
try {
const [
usosRes,
marcasRes,
estadosRes,
adscripcionesRes,
tiposEquipoRes,
sistemasOperativosRes,
] = await Promise.all([
axios.get(`${api_url}/equipos/usos`, { headers }),
axios.get(`${api_url}/equipos/marcas`, { headers }),
axios.get(`${api_url}/equipos/estados`, { headers }),
axios.get(`${api_url}/equipos/adscripciones`, { headers }),
axios.get(`${api_url}/equipos/tipos-equipo`, { headers }),
axios.get(`${api_url}/equipos/sistemas-operativos`, { headers }),
]);
setTiposUso(usosRes.data);
setMarcas(marcasRes.data);
setEstados(estadosRes.data);
setAdscripciones(adscripcionesRes.data);
setTiposEquipo(tiposEquipoRes.data);
setSistemasOperativos(sistemasOperativosRes.data);
console.log(adscripciones);
} catch (err) {
if (axios.isAxiosError(err)) {
if (err.response) {
toast.error(
err.response.data?.message || "No se pudo conectar con el API"
);
} else if (err.request) {
toast.error("No se pudo conectar con el servidor");
} else {
toast.error("Ocurrió un error inesperado");
}
} else {
toast.error("Ocurrió un error inesperado");
}
}
};
fetchData();
}, [api_url]);
useEffect(() => {
const fetchProcesador = async () => {
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
const response = await axios.get(
`${api_url}/equipos/procesador-tipo-equipos/${formData.tipoEquipo}`,
{
headers,
}
);
setProcesadores(response.data);
};
fetchProcesador();
}, [tiposEquipo]);
useEffect(() => {
const fetchPerifericos = async () => {
if (formData.tipoEquipo !== "PERIFÉRICO") return;
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
try {
const response = await axios.get(`${api_url}/equipos/perifericos`, {
headers,
});
setPerifericos(response.data);
} catch (err) {
console.error(err);
toast.error("No se pudieron cargar los periféricos");
}
};
fetchPerifericos();
}, [formData.tipoEquipo]);
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (field === "adscripcion") {
if (value.length < 3) {
// Si no hay al menos 3 letras, no mostrar sugerencias
setSuggestions((prev) => ({ ...prev, adscripcion: [] }));
return;
}
// Función para normalizar: minúsculas y quitar acentos
const normalize = (str: string) =>
str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim();
const searchValue = normalize(value);
const matches = AREAS.map((a) => a.label)
.filter((label) => normalize(label).includes(searchValue))
.slice(0, 5); // máximo 10 sugerencias
setSuggestions((prev) => ({ ...prev, adscripcion: matches }));
}
};
const handleSelectSuggestion = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setSuggestions((prev) => ({ ...prev, [field]: [] }));
};
const handleGuardar = async () => {
const adscripcionesValidas = AREAS.map((a) => a.label);
if (!adscripcionesValidas.includes(formData.adscripcion)) {
toast.error("Debes seleccionar una adscripción válida.");
return;
}
try {
const token = Cookies.get("token");
const headers = { Authorization: `Bearer ${token}` };
await axios.post(`${api_url}/equipos/crear`, formData, { headers });
toast.success("Equipo guardado correctamente");
setFormData({
inventario: "",
serie: "",
marca: "",
modelo: "",
tipoEquipo: "",
estado: "",
sistemaOperativo: "",
procesador: "",
tipoUso: "",
observaciones: "",
adscripcion: "",
lugar: "",
responsable: "",
tipoPeriferico: "",
});
} catch (err) {
toast.error("Error al guardar el equipo");
}
};
const handleCancelar = () => {
router.push("/Escaner");
};
return (
<div className="agregarEquipoContainer">
<div className="innerContainer">
<h2 className="information">Agregar Nuevo Equipo</h2>
<form className="equipoForm">
{/* Columna Izquierda */}
<div className="column">
<div className="formGroup">
<label>Numero de Inventario</label>
<input
required
type="text"
placeholder="Ingresa Inventario"
value={formData.inventario}
onChange={(e) =>
handleInputChange("inventario", e.target.value)
}
/>
</div>
<div className="formGroup">
<label>Serie</label>
<input
required
type="text"
placeholder="Ingresa serie"
value={formData.serie}
onChange={(e) => handleInputChange("serie", e.target.value)}
/>
</div>
<div className="formGroup">
<label>Marca</label>
<select
required
value={formData.marca}
onChange={(e) => handleInputChange("marca", e.target.value)}
>
<option value="">Selecciona una marca</option>
{marcas.map((m) => (
<option key={m.id_marca} value={m.marca}>
{m.marca}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Modelo</label>
<input
type="text"
placeholder="Ingresa modelo"
value={formData.modelo}
onChange={(e) => handleInputChange("modelo", e.target.value)}
/>
</div>
<div className="formGroup">
<label>Tipo de equipo</label>
<select
required
value={formData.tipoEquipo}
onChange={(e) =>
handleInputChange("tipoEquipo", e.target.value)
}
>
<option value="">Selecciona tipo de equipo</option>
{tiposEquipo.map((t) => (
<option key={t.id_tipo_de_equipo} value={t.tipo_equipo}>
{t.tipo_equipo}
</option>
))}
</select>
</div>
</div>
<div className="column">
<div className="formGroup">
<label>Estado</label>
<select
required
value={formData.estado}
onChange={(e) => handleInputChange("estado", e.target.value)}
>
<option value="">Selecciona estado</option>
{estados.map((e) => (
<option key={e.id_estado} value={e.estado}>
{e.estado}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Tipo de uso</label>
<select
required
value={formData.tipoUso}
onChange={(e) => handleInputChange("tipoUso", e.target.value)}
>
<option value="">Selecciona tipo de uso</option>
{tiposUso.map((t) => (
<option key={t.id_tipo_uso} value={t.tipo_uso}>
{t.tipo_uso}
</option>
))}
</select>
</div>
{mostrarCamposComputadora && (
<>
<div className="formGroup">
<label>Procesador</label>
<select
value={formData.procesador}
onChange={(e) =>
handleInputChange("procesador", e.target.value)
}
>
<option value="">Selecciona procesador</option>
{PROCESADORES_POR_EQUIPO[formData.tipoEquipo]?.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
<div className="formGroup">
<label>Sistema operativo</label>
<select
value={formData.sistemaOperativo}
onChange={(e) =>
handleInputChange("sistemaOperativo", e.target.value)
}
>
<option value="">Selecciona sistema operativo</option>
{SO_POR_EQUIPO[formData.tipoEquipo]?.map((so) => (
<option key={so} value={so}>
{so}
</option>
))}
</select>
</div>
</>
)}
{!mostrarCamposComputadora && (
<div className="formGroup">
<label>Tipos de Periféricos</label>
<select
value={formData.tipoPeriferico || ""}
onChange={(e) =>
handleInputChange("tipoPeriferico", e.target.value)
}
>
<option value="">Selecciona periférico</option>
{perifericos.map((p: any) => (
<option key={p.id_periferico} value={p.nombre}>
{p.nombre}
</option>
))}
</select>
</div>
)}
<div className="formGroup" style={{ position: "relative" }}>
<label>Adscripción</label>
<input
required
type="text"
placeholder="Ingresa adscripcion"
value={formData.adscripcion}
onChange={(e) =>
handleInputChange("adscripcion", e.target.value)
}
/>
{suggestions.adscripcion.length > 0 && (
<ul className="suggestions">
{suggestions.adscripcion.map((s) => (
<li
key={s}
onClick={() => handleSelectSuggestion("adscripcion", s)}
>
{s}
</li>
))}
</ul>
)}
</div>
</div>
<div className="column">
<div className="formGroup">
<label>Lugar</label>
<textarea
placeholder="Ingresa lugar"
value={formData.lugar}
onChange={(e) => handleInputChange("lugar", e.target.value)}
rows={5}
className="textAreaLarge"
/>
</div>
<div className="formGroup">
<label>Observaciones</label>
<textarea
placeholder="Ingresa observaciones"
value={formData.observaciones}
onChange={(e) =>
handleInputChange("observaciones", e.target.value)
}
rows={5}
className="textAreaLarge"
/>
</div>
<div className="formActions">
<button
type="button"
className="btnGuardar"
onClick={handleGuardar}
>
Guardar
</button>
<button
type="button"
className="btnCancelar"
onClick={handleCancelar}
>
Cancelar
</button>
</div>
</div>
</form>
</div>
</div>
);
}
+2 -2
View File
@@ -33,8 +33,8 @@ export default function BarcodeScanner({ onScan }: BarcodeScannerProps) {
type: "LiveStream",
target: scannerRef.current,
constraints: {
width: 640,
height: 370,
width: 300,
height: 500,
facingMode: "environment",
},
},
+97 -34
View File
@@ -1,17 +1,24 @@
"use client";
import { useSearchParams } from "next/navigation";
import "../app/styles/layout/agregarEquipo.scss";
import "./editar.css";
import { useEffect, useState } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import toast from "react-hot-toast";
import { SO_POR_EQUIPO } from "@/data/so_por_equipo";
import { AREAS } from "@/data/areas";
import "../app/styles/layout/agregarEquipo.scss";
import "./editar.css";
import { PROCESADORES_POR_EQUIPO } from "@/data/procesadores";
import { useRouter } from "next/navigation";
export default function Editar() {
const searchParams = useSearchParams();
const inventario = searchParams.get("equipoId");
const router = useRouter();
const [formData, setFormData] = useState({
inventario: "",
serie: "",
@@ -55,7 +62,7 @@ export default function Editar() {
interface Marca {
id_marca: number;
tipo_marca: string;
marca: string;
}
interface Adscripcion {
@@ -73,8 +80,12 @@ export default function Editar() {
>([]);
const [procesadores, setProcesadores] = useState<Procesador[]>([]);
const [suggestions, setSuggestions] = useState({
adscripcion: [] as string[],
});
const api_url = process.env.NEXT_PUBLIC_API_URL;
const mostrarCamposComputadora = formData.tipoEquipo !== "Impresora";
const mostrarCamposComputadora = formData.tipoEquipo !== "PERIFÉRICO";
// 🔹 Cargar catálogos
useEffect(() => {
@@ -175,30 +186,62 @@ export default function Editar() {
}
};
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (field === "adscripcion") {
if (value.length < 3) {
// Si no hay al menos 3 letras, no mostrar sugerencias
setSuggestions((prev) => ({ ...prev, adscripcion: [] }));
return;
}
// Normaliza para quitar acentos y poner en minúsculas
const normalize = (str: string) =>
str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.trim();
const searchValue = normalize(value);
// Busca coincidencias
const matches = AREAS.map((a) => a.label)
.filter((label) => normalize(label).includes(searchValue))
.slice(0, 10); // máximo 10 sugerencias
setSuggestions((prev) => ({ ...prev, adscripcion: matches }));
}
};
const handleSelectSuggestion = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
setSuggestions((prev) => ({ ...prev, [field]: [] }));
};
const handleCancelar = () => {
toast("Acción cancelada");
router.push("/Escaner");
};
return (
<div className="agregarEquipoContainer">
<div className="innerContainer">
<h2 className="information">
{inventario ? `Editar Equipo (${inventario})` : "Agregar Equipo"}
<span>{formData.tipoEquipo}</span>
<span>Inventario:{formData.inventario}</span>
<span>Fecha de censo:</span>
</h2>
<form className="equipoForm">
{/* Columna 1 */}
<div className="column">
<div className="formGroup">
<label>Inventario</label>
<input type="text" value={formData.inventario} disabled />
</div>
<div className="formGroup">
<label>Serie</label>
<input
type="text"
value={formData.serie}
placeholder="Ingresa serie"
onChange={(e) =>
setFormData({ ...formData, serie: e.target.value })
}
@@ -215,8 +258,8 @@ export default function Editar() {
>
<option value="">Selecciona marca</option>
{marcas.map((m) => (
<option key={m.id_marca} value={m.tipo_marca}>
{m.tipo_marca}
<option key={m.id_marca} value={m.marca}>
{m.marca}
</option>
))}
</select>
@@ -227,6 +270,7 @@ export default function Editar() {
<input
type="text"
value={formData.modelo}
placeholder="Ingresa modelo"
onChange={(e) =>
setFormData({ ...formData, modelo: e.target.value })
}
@@ -298,9 +342,9 @@ export default function Editar() {
}
>
<option value="">Selecciona procesador</option>
{procesadores.map((p) => (
<option key={p.id_procesador} value={p.procesador}>
{p.procesador}
{PROCESADORES_POR_EQUIPO[formData.tipoEquipo]?.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
@@ -318,12 +362,9 @@ export default function Editar() {
}
>
<option value="">Selecciona sistema operativo</option>
{sistemasOperativos.map((so) => (
<option
key={so.id_sistema_operativo}
value={so.sistema_operativo}
>
{so.sistema_operativo}
{SO_POR_EQUIPO[formData.tipoEquipo]?.map((so) => (
<option key={so} value={so}>
{so}
</option>
))}
</select>
@@ -331,26 +372,48 @@ export default function Editar() {
</>
)}
<div className="formGroup">
{!mostrarCamposComputadora && (
<div className="formGroup">
<label>Tipos de Perifericos</label>
<select>
<option value="">Selecciona periferico</option>
</select>
</div>
)}
<div className="formGroup" style={{ position: "relative" }}>
<label>Adscripción</label>
<select
<input
required
type="text"
placeholder="Ingresa adscripción"
value={formData.adscripcion}
onChange={(e) =>
setFormData({ ...formData, adscripcion: e.target.value })
handleInputChange("adscripcion", e.target.value)
}
>
<option value="">Selecciona adscripción</option>
{adscripciones.map((a) => (
<option key={a.id_adscripcion} value={a.adscripcion}>
{a.adscripcion}
</option>
))}
</select>
/>
{suggestions.adscripcion.length > 0 && (
<ul className="suggestions">
{suggestions.adscripcion.map((s) => (
<li
key={s}
onClick={() => handleSelectSuggestion("adscripcion", s)}
>
{s}
</li>
))}
</ul>
)}
</div>
<div className="formGroup">
<label>Responsable</label>
<input type="text" value={formData.responsable} disabled />
<input
type="text"
value={formData.responsable}
disabled
placeholder="Selecciona una Adscripcion"
/>
</div>
</div>
+4 -2
View File
@@ -8,7 +8,8 @@ import { usePathname } from "next/navigation";
function Header() {
const pathname = usePathname();
const rutasConBar = ["/Escaner", "/AgregarEquipo","/reporte"];
const publicNav = ["/Escaner", "/AgregarEquipo", "/Editar"];
const privateNav = ["/Reporte", "/CrearCuenta", "/CambiarPass"];
return (
<header className={header.header}>
@@ -40,7 +41,8 @@ function Header() {
alignItems: "end",
}}
>
{rutasConBar.includes(pathname) && <BarNavigation />}
{publicNav.includes(pathname) && <BarNavigation />}
{privateNav.includes(pathname) && <BarNavigation />}
</div>
</header>
);
+114
View File
@@ -0,0 +1,114 @@
//Esto es culpa de los webservices y de santi(wizard) attentamente IO
export const AREAS: Array<{ id: number; label: string }> = [
{ id: 128, label: "Coordinación de Actividades Deportivas y Recreativas" },
{ id: 122, label: "Coordinación de Comunicación Social" },
{ id: 9, label: "Coordinación de Cuerpos Colegiados" },
{
id: 126,
label:
"Coordinación de Derecho del Sistema de Universidad Abierta y Educación a Distancia",
},
{ id: 206, label: "Coordinación de Especializaciones" },
{ id: 120, label: "Coordinación de Estudios de Posgrado" },
{
id: 182,
label:
"Coordinación de la Unidad de Investigación Multidisciplinaria Aplicada",
},
{
id: 174,
label:
"Coordinación de LICEL Sistema de Universidad Abierta y Educación a Distancia",
},
{ id: 221, label: "Coordinación de Maestrías y Doctorados" },
{
id: 152,
label:
"Coordinación de Relaciones Internacionales del Sistema de Universidad Abierta y Educación a Distancia",
},
{ id: 121, label: "Coordinación de Servicios Académicos" },
{ id: 125, label: "Coordinación del Centro de Difusión Cultural" },
{ id: 124, label: "Coordinación del Centro de Educación Continua" },
{ id: 129, label: "Coordinación del Centro de Enseñanza de Idiomas" },
{
id: 214,
label: "Coordinación del Centro de Estudios Municipales y Metropolitanos",
},
{
id: 176,
label: "Coordinación del Centro Tecnológico para la Educación a Distancia",
},
{ id: 58, label: "Coordinación del Programa de Actuaría" },
{ id: 57, label: "Coordinación del Programa de Arquitectura" },
{ id: 46, label: "Coordinación del Programa de Comunicación" },
{ id: 56, label: "Coordinación del Programa de Derecho" },
{ id: 55, label: "Coordinación del Programa de Diseño Gráfico" },
{ id: 54, label: "Coordinación del Programa de Economía" },
{ id: 49, label: "Coordinación del Programa de Enseñanza de Inglés" },
{ id: 51, label: "Coordinación del Programa de Ingeniería Civil" },
{
id: 48,
label: "Coordinación del Programa de Matemáticas Aplicadas y Computación",
},
{ id: 47, label: "Coordinación del Programa de Pedagogía" },
{ id: 44, label: "Coordinación del Programa de Sociología" },
{ id: 43, label: "Coordinación del Programa Político" },
{ id: 194, label: "Departamento de Intendencia" },
{
id: 193,
label:
"Departamento de Investigación en Comunicación y Estudios Culturales",
},
{
id: 222,
label:
"Departamento de Orientación Académica del Sistema de Universidad Abierta y Educación a Distancia",
},
{ id: 204, label: "Departamento de Proyectos Administrativos" },
{ id: 86, label: "Departamento de Talleres y Laboratorios" },
{ id: 72, label: "Dirección" },
{ id: 80, label: "División de Ciencias Jurídicas" },
{ id: 79, label: "División de Ciencias Socioeconómicas" },
{ id: 78, label: "División de Diseño y Edificación" },
{ id: 77, label: "División de Humanidades" },
{ id: 76, label: "División de Matemáticas e Ingeniería" },
{
id: 74,
label:
"División del Sistema de Universidad Abierta y Educación a Distancia",
},
{ id: 70, label: "Especialización en Costos en la Construcción" },
{ id: 69, label: "Especialización en Finanzas Públicas" },
{ id: 68, label: "Especialización en Geotecnia" },
{ id: 71, label: "Especialización en Sistemas de Calidad" },
{
id: 220,
label:
"Especialización en Tecnología Digital para la Enseñanza de Matemáticas",
},
{ id: 207, label: "Grupo de Trabajo de Educación para Adultos (GTEA)" },
{ id: 173, label: "Maestría en Docencia para la Educación Media Superior" },
{ id: 224, label: "Posgrado en Ciencias Políticas y Sociales" },
{ id: 62, label: "Posgrado en Derecho" },
{ id: 164, label: "Posgrado en Economía" },
{ id: 197, label: "Posgrado en Pedagogía" },
{ id: 63, label: "Posgrado en Urbanismo" },
{ id: 226, label: "Programa Único de Especializaciones en Derecho" },
{ id: 127, label: "Secretaría Administrativa" },
{ id: 181, label: "Secretaría de Asuntos Académicos Estudiantiles" },
{
id: 177,
label: "Secretaría de Extensión Universitaria y Vinculación Institucional",
},
{ id: 119, label: "Secretaría de Posgrado e Investigación" },
{ id: 8, label: "Secretaría General" },
{ id: 6, label: "Unidad de Administración Escolar" },
{ id: 5, label: "Unidad de Administración y Recursos" },
{ id: 4, label: "Unidad de Planeación" },
{ id: 3, label: "Unidad de Servicios a la Comunidad" },
{ id: 2, label: "Unidad de Servicios Editoriales" },
{ id: 1, label: "Unidad de Talleres, Laboratorios y Audiovisuales" },
{ id: 130, label: "Unidad del Centro de Desarrollo Tecnológico" },
{ id: 123, label: "Unidad del Centro de Información y Documentación" },
];
+97
View File
@@ -0,0 +1,97 @@
export const PROCESADORES_POR_EQUIPO: Record<string, string[]> = {
"ESCRITORIO PC": [
"i7 o equivalentes (13a generación en adelante)",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (5a - 8a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (13a generación en adelante)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (13a generación en adelante)",
"i3 o equivalentes (9a - 12a generación)",
"i3 o equivalentes (5a - 8a generación)",
"i3 o equivalentes (1a - 4a generación)",
"Pentium Silver O Gold",
"Pentium Dual Core o anteriores",
"Celeron (adquirido en 2018 hasta la fecha)",
"Celeron Dual Core o anteriores",
"Core2 Duo, Quad Core, Pentium Dual Core o anteriores",
"Pentium Series J, G",
"Celeron Serie J, G",
],
"ESCRITORIO LINUX": [
"i7 o equivalentes (13a generación en adelante)",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (5a - 8a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (13a generación en adelante)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (13a generación en adelante)",
"i3 o equivalentes (9a - 12a generación)",
"i3 o equivalentes (5a - 8a generación)",
"i3 o equivalentes (1a - 4a generación)",
"Pentium Silver O Gold",
"Pentium Dual Core o anteriores",
],
"PORTÁTILES WINDOWS": [
"i7 o equivalentes (13a generación en adelante)",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (5a - 8a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (13a generación en adelante)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (13a generación en adelante)",
"i3 o equivalentes (9a - 12a generación)",
"i3 o equivalentes (5a - 8a generación)",
"i3 o equivalentes (1a - 4a generación)",
"Pentium Silver O Gold",
"Pentium Dual Core o anteriores",
],
"PORTÁTILES CHROMEBOOK": [
"i7 o equivalentes (13a generación en adelante)",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (5a - 8a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (13a generación en adelante)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (13a generación en adelante)",
"i3 o equivalentes (9a - 12a generación)",
"i3 o equivalentes (5a - 8a generación)",
"i3 o equivalentes (1a - 4a generación)",
],
"ESCRITORIO MAC OS": [
"Familia M2",
"Familia M1",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (1a - 4a generación)",
"Core2 Mac, Quad Core o anteriores",
],
"PORTÁTILES MAC OS": [
"Familia M2",
"Familia M1",
"i7 o equivalentes (9a - 12a generación)",
"i7 o equivalentes (1a - 4a generación)",
"i5 o equivalentes (9a - 12a generación)",
"i5 o equivalentes (5a - 8a generación)",
"i5 o equivalentes (1a - 4a generación)",
"i3 o equivalentes (1a - 4a generación)",
"Core2 Mac, Quad Core o anteriores",
],
SERVIDOR: [
"Xeon Bronce, Plata, Oro 1a generación",
"Xeon E3, E5, E7",
"Xeon Phi",
"Xeon o anteriores",
],
};
+36
View File
@@ -0,0 +1,36 @@
export const SO_POR_EQUIPO: Record<string, string[]> = {
"ESCRITORIO PC": [
"Windows 11",
"Windows 10",
"Windows 7/8",
"Windows XP/Vista",
"Linux",
],
"ESCRITORIO APPLE": [
"Mac OS (13 - Ventura, 14 - Sonoma)",
"Mac OS X (Mojave, Catalina, 11 - Big Sur, 12 - Monterrey)",
"Mac OS X (Yosemite, El Capitan, Sierra, High Sierra)",
"Mac OS X (Snow Leopard, Lion, Mountain Lion, Mavericks)",
],
"TABLETA ANDROID": [
"Windows 11",
"Windows 10",
"Windows 7/8",
"Windows XP/Vista",
"Chrome OS",
],
"TABLETA iPAD OS": [
"Mac OS (13 - Ventura, 14 - Sonoma)",
"Mac OS X (Mojave, Catalina, 11 - Big Sur, 12 - Monterrey)",
"Mac OS X (Yosemite, El Capitan, Sierra, High Sierra)",
"Mac OS X (Snow Leopard, Lion, Mountain Lion, Mavericks)",
],
SERVIDOR: [
"Linux (CentOS, Fedora, Ubuntu, Red Hat Enterprise, entre otros)",
"Unix (AIX, MAC OS Server, Solaris, entre otros)",
"Windows Server 2022/2023",
"Windows Server 2016/2019",
"Windows Server 2008/2012",
"Windows Server 2000/2003",
],
};