Vista Escaner y Agregar equipo agregados

This commit is contained in:
jls846
2025-10-25 20:05:16 -06:00
parent 11e253e405
commit 2ea93697f4
17 changed files with 647 additions and 215 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

+247
View File
@@ -0,0 +1,247 @@
"use client";
import { useState } from "react";
import "../styles/layout/agregarEquipo.scss"; // Asegúrate de crear este archivo
export default function page() {
const [formData, setFormData] = useState({
serie: "",
marca: "",
modelo: "",
tipoEquipo: "", // Nuevo campo
estado: "",
sistemaOperativo: "",
procesador: "",
tipoUso: "",
observaciones: "",
adscripcion: "",
lugar: "",
responsable: "",
});
// Estado para saber si mostramos los campos de SO y Procesador
const mostrarCamposComputadora = formData.tipoEquipo !== "Impresora";
const handleGuardar = () => {
console.log("Formulario enviado:", formData);
alert("Equipo guardado (vista solo)");
};
const handleCancelar = () => {
alert("Acción cancelada");
};
// Función para actualizar el estado y limpiar campos si es impresora
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => {
let updated = { ...prev, [name]: value };
// Si es impresora, limpiamos SO y Procesador
if (value === "Impresora") {
updated.sistemaOperativo = "";
updated.procesador = "";
}
return updated;
});
};
return (
<div className="agregarEquipoContainer">
<div className="innerContainer">
<h2>Agregar Nuevo Equipo</h2>
<form className="equipoForm">
{/* Columna Izquierda */}
<div className="column">
<div className="formGroup">
<label>Serie</label>
<input
type="text"
placeholder="Ingresa serie"
value={formData.serie}
onChange={(e) =>
setFormData({ ...formData, serie: e.target.value })
}
/>
</div>
<div className="formGroup">
<label>Marca</label>
<select
value={formData.marca}
onChange={(e) =>
setFormData({ ...formData, marca: e.target.value })
}
>
<option value="">Ingresa marca</option>
<option value="Dell">Dell</option>
<option value="HP">HP</option>
<option value="Lenovo">Lenovo</option>
</select>
</div>
<div className="formGroup">
<label>Modelo</label>
<select
value={formData.modelo}
onChange={(e) =>
setFormData({ ...formData, modelo: e.target.value })
}
>
<option value="">Ingresa modelo</option>
<option value="XPS 15">XPS 15</option>
<option value="EliteBook">EliteBook</option>
<option value="ThinkPad">ThinkPad</option>
</select>
</div>
<div className="formGroup">
<label>Tipo de equipo</label>
<select
name="tipoEquipo"
value={formData.tipoEquipo}
onChange={handleChange}
>
<option value="">Selecciona tipo</option>
<option value="PC">PC</option>
<option value="Laptop">Laptop</option>
<option value="Impresora">Impresora</option>
</select>
</div>
<div className="formGroup">
<label>Estado</label>
<select
value={formData.estado}
onChange={(e) =>
setFormData({ ...formData, estado: e.target.value })
}
>
<option value="">Selecciona estado</option>
<option value="Activado">Activado</option>
<option value="Desactivado">Desactivado</option>
<option value="En reparación">En reparación</option>
</select>
</div>
<div className="formGroup">
<label>Tipo uso</label>
<select
value={formData.tipoUso}
onChange={(e) =>
setFormData({ ...formData, tipoUso: e.target.value })
}
>
<option value="">Ingresa tipo de uso</option>
<option value="administrativo">administrativo</option>
<option value="educacional">educacional</option>
<option value="laboratorio">laboratorio</option>
</select>
</div>
</div>
{/* Columna Centro */}
<div className="column">
{/* Mostrar SO y Procesador solo si NO es impresora */}
{mostrarCamposComputadora && (
<>
<div className="formGroup">
<label>Procesador</label>
<select
value={formData.procesador}
onChange={(e) =>
setFormData({ ...formData, procesador: e.target.value })
}
>
<option value="">Selecciona procesador</option>
<option value="Ryzen 3">Ryzen 3</option>
<option value="Intel i5">Intel i5</option>
<option value="AMD Ryzen 5">AMD Ryzen 5</option>
</select>
</div>
<div className="formGroup">
<label>Sistema operativo</label>
<select
value={formData.sistemaOperativo}
onChange={(e) =>
setFormData({ ...formData, sistemaOperativo: e.target.value })
}
>
<option value="">Selecciona sistema operativo</option>
<option value="Windows 10">Windows 10</option>
<option value="Windows 11">Windows 11</option>
<option value="Linux">Linux</option>
<option value="macOS">macOS</option>
</select>
</div>
</>
)}
<div className="formGroup">
<label>Adscripción</label>
<select
value={formData.adscripcion}
onChange={(e) =>
setFormData({ ...formData, adscripcion: e.target.value })
}
>
<option value="">Selecciona adscripción</option>
<option value="Cedetec">Cedetec</option>
<option value="Laboratorio">Laboratorio</option>
<option value="Administración">Administración</option>
</select>
</div>
<div className="formGroup">
<label>Responsable</label>
<select
value={formData.responsable}
onChange={(e) =>
setFormData({ ...formData, responsable: e.target.value })
}
>
<option value="">Selecciona responsable</option>
<option value="Lino">Lino</option>
<option value="Ana">Ana</option>
<option value="Carlos">Carlos</option>
</select>
</div>
</div>
{/* Columna Derecha */}
<div className="column">
<div className="formGroup">
<label>Lugar</label>
<textarea
placeholder="Ingresa lugar"
value={formData.lugar}
onChange={(e) =>
setFormData({ ...formData, lugar: e.target.value })
}
rows={4}
className="textAreaLarge"
/>
</div>
<div className="formGroup">
<label>Observaciones</label>
<textarea
placeholder="Ingresa observaciones"
value={formData.observaciones}
onChange={(e) =>
setFormData({ ...formData, observaciones: e.target.value })
}
rows={4}
className="textAreaLarge"
/>
</div>
</div>
</form>
{/* Botones */}
<div className="formActions">
<button type="button" className="btnGuardar" onClick={handleGuardar}>
Guardar
</button>
<button type="button" className="btnCancelar" onClick={handleCancelar}>
Cancelar
</button>
</div>
</div>
</div>
);
}
+30 -51
View File
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import BarcodeScanner from "../../components/BarcodeScanner";
import "../styles/layout/dashboard.scss";
import "../styles/layout/escaner.scss"; // Importación global
// Tipos
type Equipo = {
id: string;
nombre: string;
@@ -38,70 +38,49 @@ export default function Dashboard() {
const handleScan = (code: string) => {
setLastScan(code);
const encontrado = equipos.find((e) => e.id === code);
if (encontrado) {
alert(`Equipo encontrado: ${encontrado.nombre} (${encontrado.estado})`);
} else {
const nombre = prompt("Equipo no registrado. Ingresa el nombre:");
if (nombre) {
setEquipos([...equipos, { id: code, nombre, estado: "Disponible" }]);
const codigo = prompt("Equipo no registrado. Ingresa el codigo:");
if (codigo) {
setEquipos([...equipos, { id: code, nombre: codigo, estado: "Disponible" }]);
alert("Equipo agregado al inventario");
}
}
};
return (
<div className="dashboard-container">
<div className="header">
<h2>Dashboard Inventario Escolar</h2>
<button onClick={cerrarSesion}>Cerrar sesión</button>
</div>
<div className="dashboardContainer">
<div className="search-section">
<h3>Buscar equipo</h3>
<div style={{ display: "flex", marginTop: "10px" }}>
{/* VISTA DE ESCANEO */}
<main className="scanView">
<h3>Escanear Inventario</h3>
<div className="scannerFrame">
<img src="/barcode_scanner.png" alt="Marco de escaneo" />
</div>
<p className="instructions">
Enfoca el código de barras dentro del marco
</p>
<button className="scanButton">
<img src="/photo_camera.png" alt="camara" /> Escanear
</button>
<div className="searchBox">
<input
type="text"
placeholder="ID o nombre"
placeholder="Buscar por inventario..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button onClick={buscarEquipo}>Buscar</button>
<button onClick={buscarEquipo} className="searchButton">
<img src="/search.png" alt="Buscar" />
</button>
</div>
</div>
<div className="scanner-section">
<h3>Escanear código de barras</h3>
<BarcodeScanner onDetected={handleScan} />
{lastScan && (
<p>
Último código escaneado: <strong>{lastScan}</strong>
</p>
)}
</div>
<div className="equipos-section">
<h3>Equipos</h3>
<ul>
{equipos.map((e) => (
<li key={e.id}>
<span>
{e.id} - {e.nombre}
</span>
<span
className={
e.estado === "Disponible"
? "estado-disponible"
: "estado-reparacion"
}
>
{e.estado}
</span>
</li>
))}
</ul>
</div>
</main>
</div>
);
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
import Header from "../components/header";
import { Inter, Montserrat } from "next/font/google";
import "../app/styles/base/_globales.scss"; // si tienes estilos globales
import "../app/styles/base/globales.scss"; // si tienes estilos globales
// Carga optimizada de fuentes
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "700"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
});
const montserrat = Montserrat({
+1 -1
View File
@@ -1,5 +1,5 @@
import Login from "@/components/Login";
import "../app/styles/base/_globales.scss";
import "../app/styles/base/globales.scss";
export default function page() {
return <Login />;
+10
View File
@@ -0,0 +1,10 @@
// Colores
$color-primary: #003e79;
$color-secondary: #fab403;
$color-background: #f5f5f7;
$color-text: #000000;
// Tipografía
$font-primary: "Inter", sans-serif;
$font-titles: "Montserrat", sans-serif;
@@ -1,3 +1,5 @@
@import "variables";
html {
font-size: 62.5%;
box-sizing: border-box;
@@ -16,13 +18,14 @@ html {
body {
display: grid;
grid-template-rows: auto 1fr;
background-color: #f5f5f7;
font-family: "Inter", sans-serif;
background-color: $color-background;
font-family: $font-primary;
font-style: normal;
font-size: 2rem;
font-optical-sizing: auto;
min-height: 100dvh;
width: 100vw;
color: $color-text;
}
p {
@@ -31,10 +34,14 @@ p {
}
a {
color: #003e79;
color: $color-primary;
display: block;
text-decoration: none;
margin: 10px 5px;
&:hover {
color: $color-secondary;
}
}
h1,
@@ -42,7 +49,7 @@ h2,
h3 {
margin: 0 0 5rem 0;
font-weight: 600;
font-family: "Montserrat", sans-serif; /* 👈 Títulos con Montserrat */
font-family: $font-titles;
}
h1 {
+34 -29
View File
@@ -63,10 +63,9 @@
position: absolute;
flex-direction: column;
min-width: 200px;
max-width: 300px; /* Ajusta según tu diseño */
max-width: 300px;
left: 0;
background-color: #003e79;
transition: color 0.3s ease;
transition: opacity 0.4s ease, max-height 0.4s ease;
}
@@ -74,15 +73,6 @@
display: none;
}
.subMenu.open ul {
display: flex;
color: rgba(0, 61, 121, 1);
}
.subMenu ul:hover {
color: #5b8cc9;
}
.subMenu:hover ul {
opacity: 1;
max-height: 500px;
@@ -106,11 +96,6 @@
font-size: 1.5rem;
}
.containerLinks {
padding: 0 !important;
border-radius: 0 0 4px 4px;
}
.links {
display: flex;
justify-content: center;
@@ -121,11 +106,28 @@
font-size: 1.5rem;
}
thead,
tbody {
font-size: 1.25rem;
/* Estilo del botón de cierre de sesión */
.logout-button {
background-color: #ff4d4d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 200px;
&:hover {
background-color: #e63939;
}
}
/* ✅ Ajustes para móvil */
@media (max-width: 800px) {
.menuToggle {
display: flex;
@@ -133,18 +135,12 @@ tbody {
align-items: end;
transition: transform 0.3s ease;
}
.barNavigation {
width: 100%;
}
.menuToggle.center {
align-items: center;
margin: 0;
}
.barNavigation {
font-size: 15px;
display: block;
padding: 0;
width: 100%;
}
.barNavigation ul {
@@ -156,10 +152,11 @@ tbody {
display: none;
z-index: 1;
height: auto;
width: 100%;
}
.barNavigation {
padding: 0;
.barNavigation ul.active {
display: flex;
}
.barNavigation li:hover {
@@ -176,6 +173,14 @@ tbody {
background-color: rgb(1, 92, 184);
}
.logout-button {
margin-top: 10px;
font-size: 1rem;
padding: 6px 12px;
max-width: 180px;
}
thead,
tbody {
font-size: 1rem;
+159
View File
@@ -0,0 +1,159 @@
.agregarEquipoContainer {
background-color: #ffffff;
padding: 0;
margin: 0;
width: 100%;
max-width: 100%;
.innerContainer {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
h2 {
margin-top: 1rem;
text-align: center;
color: #0056b3;
margin-bottom: 20px;
font-size: 1.8rem;
}
}
.equipoForm {
display: flex;
gap: 30px;
margin-bottom: 20px;
.column {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
.formGroup {
display: flex;
flex-direction: column;
min-width: 0;
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
font-size: 14px;
}
input,
select,
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc; /* Borde azul como en la imagen */
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s ease;
height: 40px;
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 86, 179, 0.2);
}
}
textarea.textAreaLarge {
height: auto;
min-height: 80px;
max-height: 150px;
resize: none;
padding: 12px;
font-family: inherit;
}
select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 30px;
}
}
}
}
.formActions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
.btnGuardar {
background-color: #ffb700;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: #ffcc33;
}
}
.btnCancelar {
background-color: #0056b3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: #004494;
}
}
}
/* Responsive: en móvil, una columna */
@media (max-width: 768px) {
.agregarEquipoContainer .innerContainer {
padding: 15px;
}
.equipoForm {
flex-direction: column;
gap: 20px;
.column {
flex: 1 1 100%;
min-width: auto;
.formGroup {
input,
select {
width: 100%;
font-size: 16px;
}
}
}
}
.formActions {
flex-direction: column;
align-items: stretch;
button {
width: 100%;
font-size: 18px;
padding: 12px;
}
}
}
View File
-83
View File
@@ -1,83 +0,0 @@
.dashboard-container {
max-width: 900px;
margin: 50px auto;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
font-family: Arial, sans-serif;
}
h2 {
color: #004aad; // azul marino
font-size: 2rem;
margin-bottom: 15px;
}
button {
background-color: #d4af37; // dorado
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s;
}
button:hover {
background-color: #b5952e;
}
input {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid #ccc;
flex: 1;
font-size: 1rem;
}
.search-section, .scanner-section, .equipos-section {
margin-top: 30px;
}
.search-section input, .search-section button {
margin-right: 10px;
}
.scanner-section video {
border: 2px solid #004aad;
border-radius: 10px;
max-width: 400px;
width: 100%;
}
.equipos-section ul {
list-style: none;
padding: 0;
}
.equipos-section li {
display: flex;
justify-content: space-between;
padding: 10px 15px;
border-radius: 8px;
border: 1px solid #ccc;
margin-bottom: 10px;
background: white;
transition: background 0.3s;
}
.equipos-section li:hover {
background: #e6f0ff;
}
.estado-disponible {
color: green;
font-weight: bold;
}
.estado-reparacion {
color: red;
font-weight: bold;
}
+133
View File
@@ -0,0 +1,133 @@
.dashboardContainer {
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
background-color: #ffffff;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: #ffffff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 10;
h2 {
margin: 0;
font-size: 1.5rem;
color: #0056b3;
}
}
.scanView {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
gap: 20px;
h3 {
font-size: 16px;
font-weight:500;
margin: 0;
}
}
.scannerFrame {
width: 90%;
max-width: 500px;
height: 300px;
border: 2px solid #0056b3;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
font-size: 80px;
color: #000;
img{
width: 150px;
height: auto;
}
}
.instructions {
text-align: center;
font-size: 14px;
color: #666;
margin: 0;
}
.scanButton {
background-color: #ffb700;
color: #000;
border: none;
padding: 10px 24px;
border-radius: 20px;
font-weight: bold;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
color:#003E79 ;
&:hover {
background-color: #ffcc33;
}
}
.searchBox {
display: flex;
align-items: center;
gap: 8px;
width: 80%;
max-width: 400px;
border: 2px solid #0056b3;
border-radius: 20px;
overflow: hidden;
input {
flex: 1;
padding: 12px 16px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
}
.searchButton {
background-color: #0056b3;
color: white;
border-radius: 10px;
border: none;
padding: 0 5px;
cursor: pointer;
font-size: 18px;
margin-right: 10px;
display: flex; // ← Activa Flexbox
align-items: center; // ← Centra verticalmente
justify-content: center; // ← Centra horizontalmente
width: 36px; // ← Opcional: da un ancho fijo para que sea cuadrado
height: 36px; // ← Opcional: altura igual al ancho
&:hover {
background-color: #004494;
}
img {
display: block;
width: 20px; // ← Ajusta el tamaño del icono
height: auto; // ← Mantiene proporción
}
}
}
+19 -2
View File
@@ -1,9 +1,12 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation"; // ✅ Importa useRouter
import "../app/styles/layout/BarNavigation.scss";
import Link from "next/link";
function BarNavigation() {
const router = useRouter(); // ✅ Para redirigir
const [openMenu, setOpenMenu] = useState(false);
const [openSubMenu, setOpenSubMenu] = useState<number | null>(null);
@@ -14,9 +17,17 @@ function BarNavigation() {
}
};
const cerrarSesion = () => {
if (typeof window !== "undefined") {
localStorage.removeItem("loggedIn");
router.push("/"); // ✅ Redirige al inicio
}
setOpenMenu(false); // Cierra menú móvil
};
return (
<nav className="barNavigation">
<div className={`menuToggle ${openMenu ? "" : ""}`} onClick={toggleMenu}>
<div className="menuToggle" onClick={toggleMenu}>
<div></div>
<div></div>
<div></div>
@@ -33,9 +44,15 @@ function BarNavigation() {
<span>Agregar equipo</span>
</Link>
</li>
{/* ✅ Botón directo, sin onClick en el <li> */}
<li>
<button className="logout-button" onClick={cerrarSesion}>
Cerrar sesión
</button>
</li>
</ul>
</nav>
);
}
export default BarNavigation;
export default BarNavigation;
-42
View File
@@ -1,42 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import { BrowserMultiFormatReader } from "@zxing/library";
type Props = { onDetected: (code: string) => void; };
export default function BarcodeScanner({ onDetected }: Props) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const codeReader = new BrowserMultiFormatReader();
if (!videoRef.current) return;
// Obtener la cámara trasera si existe
codeReader.listVideoInputDevices().then((devices) => {
const backDevice = devices.find(d => d.label.toLowerCase().includes("back"));
const deviceId = backDevice ? backDevice.deviceId : devices[0].deviceId;
// Inicia la cámara inmediatamente
codeReader.decodeFromVideoDevice(deviceId, videoRef.current!, (result) => {
if (result) onDetected(result.getText());
});
}).catch(console.error);
return () => { codeReader.reset(); };
}, [onDetected]);
return (
<video
ref={videoRef}
style={{
width: "100%",
maxWidth: "600px", // Más grande
border: "2px solid #004aad",
borderRadius: "10px"
}}
autoPlay
muted
/>
);
}
+1 -1
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import "../app/styles/layout/login.scss";
import "../app/styles/base/_globales.scss"; // si tienes estilos globales
import "../app/styles/base/globales.scss";
import Image from "next/image";
import Link from "next/link";