feat: roadmap y diseño inicial — iteración 1 antes de feedback del usuario

Inventario de dispositivos electrónicos no prestables para administración
pública. Esta primera iteración contiene el diseño y la planificación
antes de contactar con el usuario para su validación.

Contenido:
- especificaciones.md: requisitos detallados del programa
- docs/arquitectura.md: propuesta de arquitectura con alternativas
- docs/roadmap-iteracion-1.md: plan de desarrollo iteración 1
- docs/arquitectura-diagrama.html: diagrama visual de arquitectura
- sketches/: mockups de las 4 pantallas principales
  - 001-dashboard: vista resumen
  - 002-inventario: tabla de dispositivos
  - 003-formulario: alta/edición de dispositivos
  - 004-configuracion: ajustes generales

Stack propuesto: Python + Flask + SQLite + Alpine.js
Distribución: PyInstaller → un único .exe autocontenido
Entorno: Windows 11 Pro unida a dominio, sin privilegios de admin
This commit is contained in:
pikaos 2026-08-03 01:34:56 +01:00
commit ee849eb9a8
14 changed files with 3761 additions and 0 deletions

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
.venv/
.env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
desktop.ini
# SQLite
*.db
*.db-journal
*.db-wal
*.db-shm
# Backups
backups/
# PyInstaller
*.spec
*.exe
*.dll
# Distribution
dist/
build/
# Logs
*.log
logs/
# Environment variables
.env
.env.local

72
README.md Normal file
View file

@ -0,0 +1,72 @@
# Inventschario
Aplicación de inventario de dispositivos electrónicos no prestables para
entornos de administración pública.
## Descripción
Inventschario es una aplicación web local que se ejecuta en el navegador
por defecto del usuario. Gestiona el ciclo de vida completo de dispositivos
electrónicos: ordenadores, monitores, impresoras, dispositivos de red, etc.
### Características principales
- **Gestión de dispositivos** — Alta, edición, baja y enajenación
- **Campos renombrables** — Etiquetas personalizables en la interfaz
- **Ciclo de vida** — Baja por obsolescencia, avería, robo; enajenación por venta, donación, transferencia
- **Copias de seguridad** — Automáticas diarias con recordatorio de limpieza
- **Importación / Exportación** — CSV con mapeo modular de campos
- **Historial** — Registro de todos los cambios realizados
- **Accesibilidad** — Navegación por teclado, contraste WCAG 2.1 AA
## Requisitos
- Windows 11 Pro (unida a dominio)
- Privilegios de instalador (NO se requiere administrador)
- Navegador Edge (incluido en Windows)
## Arquitectura
```
[Browser Edge] ──HTTP──► [Flask API] ──► [SQLite]
│ │
│ HTML/CSS/JS │ Python services
│ (Alpine.js) │ (DeviceService, etc.)
│ │
◄──── JSON ─────────────►│
```
- **Frontend:** HTML + CSS + Alpine.js (sin framework pesado)
- **Backend:** Python 3.11+ con Flask
- **Base de datos:** SQLite con WAL mode
- **Empaquetado:** PyInstaller → un único `.exe`
## Documentación
- [Especificaciones](especificaciones.md) — Requisitos detallados del programa
- [Arquitectura](docs/arquitectura.md) — Documento de arquitectura
- [Roadmap Iteración 1](docs/roadmap-iteracion-1.md) — Plan de desarrollo
- [Diagrama de Arquitectura](docs/arquitectura-diagrama.html) — Diagrama visual (abrir en navegador)
## Mockups de Interfaz
Los bocetos de diseño están en `sketches/`:
- [Dashboard](sketches/001-dashboard/index.html) — Vista resumen
- [Inventario](sketches/002-inventario/index.html) — Tabla de dispositivos
- [Formulario](sketches/003-formulario/index.html) — Alta / edición
- [Configuración](sketches/004-configuracion/index.html) — Ajustes generales
Abrir los archivos `.html` directamente en el navegador para ver los mockups.
## Estado
**Iteración 1** — Roadmap definido, pendiente de feedback del usuario.
Este commit contiene el diseño y la planificación antes de contactar con
el usuario para su validación. El trabajo corresponde a la primera iteración
del proyecto.
## Licencia
Por definir.

View file

@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Diagrama de Arquitectura</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'JetBrains Mono', monospace;
background: #020617;
min-height: 100vh;
padding: 2rem;
color: white;
}
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.header-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
.pulse-dot {
width: 12px; height: 12px; background: #22d3ee;
border-radius: 50%; animation: pulse 2s infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.025em; }
.subtitle { color: #94a3b8; font-size: 0.875rem; margin-left: 1.75rem; }
.diagram-container {
background: rgba(15, 23, 42, 0.5);
border-radius: 1rem;
border: 1px solid #1e293b;
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 900px; display: block; }
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.card {
background: rgba(15, 23, 42, 0.5);
border-radius: 0.75rem;
border: 1px solid #1e293b;
padding: 1.25rem;
}
.card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.card-dot { width: 8px; height: 8px; border-radius: 50%; }
.card-dot.cyan { background: #22d3ee; }
.card-dot.emerald { background: #34d399; }
.card-dot.violet { background: #a78bfa; }
.card-dot.amber { background: #fbbf24; }
.card-dot.rose { background: #fb7185; }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card ul { list-style: none; color: #94a3b8; font-size: 0.75rem; }
.card li { margin-bottom: 0.375rem; }
.footer { text-align: center; margin-top: 1.5rem; color: #475569; font-size: 0.75rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-row">
<div class="pulse-dot"></div>
<h1>Inventschario — Arquitectura del Sistema</h1>
</div>
<p class="subtitle">Aplicación de inventario de dispositivos electrónicos • Python + Flask + SQLite • Local-first</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 700">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
</marker>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
</pattern>
</defs>
<!-- Background Grid -->
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- ===== MACHINE BOUNDARY ===== -->
<rect x="20" y="20" width="960" height="660" rx="12" fill="rgba(251, 191, 36, 0.03)" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4"/>
<text x="32" y="38" fill="#fbbf24" font-size="10" font-weight="600">Windows 11 Pro — Máquina Local (127.0.0.1)</text>
<!-- ===== BROWSER (FRONTEND) ===== -->
<rect x="40" y="55" width="920" height="180" rx="8" fill="rgba(8, 51, 68, 0.15)" stroke="#22d3ee" stroke-width="1" stroke-dasharray="4,4"/>
<text x="52" y="73" fill="#22d3ee" font-size="10" font-weight="600">NAVEGADOR (Edge)</text>
<!-- SPA Shell -->
<rect x="60" y="85" width="140" height="55" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="130" y="108" fill="white" font-size="11" font-weight="600" text-anchor="middle">SPA Shell</text>
<text x="130" y="124" fill="#94a3b8" font-size="8" text-anchor="middle">index.html + Router</text>
<!-- Components -->
<rect x="220" y="85" width="140" height="55" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="290" y="108" fill="white" font-size="11" font-weight="600" text-anchor="middle">Componentes</text>
<text x="290" y="124" fill="#94a3b8" font-size="8" text-anchor="middle">Table | Form | Modal</text>
<!-- API Client -->
<rect x="380" y="85" width="140" height="55" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="450" y="108" fill="white" font-size="11" font-weight="600" text-anchor="middle">API Client</text>
<text x="450" y="124" fill="#94a3b8" font-size="8" text-anchor="middle">fetch wrapper + errors</text>
<!-- Pages -->
<rect x="540" y="85" width="140" height="55" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="610" y="108" fill="white" font-size="11" font-weight="600" text-anchor="middle">Páginas</text>
<text x="610" y="124" fill="#94a3b8" font-size="8" text-anchor="middle">Dashboard | Inventory</text>
<!-- Setup Wizard -->
<rect x="700" y="85" width="140" height="55" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
<text x="770" y="108" fill="white" font-size="11" font-weight="600" text-anchor="middle">Setup Wizard</text>
<text x="770" y="124" fill="#94a3b8" font-size="8" text-anchor="middle">6 pasos de config</text>
<!-- Frontend labels -->
<text x="60" y="170" fill="#22d3ee" font-size="9">HTML + CSS + Alpine.js</text>
<text x="60" y="185" fill="#94a3b8" font-size="8">Sin build step • System fonts • WCAG 2.1 AA</text>
<text x="60" y="200" fill="#94a3b8" font-size="8">Router hash-based • Componentes vanilla JS</text>
<!-- ===== HTTP ARROW ===== -->
<line x1="450" y1="145" x2="450" y2="260" stroke="#22d3ee" stroke-width="2" marker-end="url(#arrowhead)"/>
<rect x="395" y="190" width="110" height="22" rx="4" fill="#020617" stroke="#1e293b" stroke-width="1"/>
<text x="450" y="205" fill="#22d3ee" font-size="9" text-anchor="middle" font-weight="600">HTTP localhost</text>
<!-- ===== API GATEWAY ===== -->
<rect x="40" y="255" width="920" height="130" rx="8" fill="rgba(6, 78, 59, 0.15)" stroke="#34d399" stroke-width="1" stroke-dasharray="4,4"/>
<text x="52" y="273" fill="#34d399" font-size="10" font-weight="600">API GATEWAY — Flask REST/JSON</text>
<!-- Device Routes -->
<rect x="60" y="285" width="150" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="135" y="305" fill="white" font-size="10" font-weight="600" text-anchor="middle">/api/v1/devices</text>
<text x="135" y="319" fill="#94a3b8" font-size="8" text-anchor="middle">CRUD + History</text>
<!-- Import/Export Routes -->
<rect x="230" y="285" width="150" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="305" y="305" fill="white" font-size="10" font-weight="600" text-anchor="middle">/api/v1/import|export</text>
<text x="305" y="319" fill="#94a3b8" font-size="8" text-anchor="middle">CSV operations</text>
<!-- Backup Routes -->
<rect x="400" y="285" width="150" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="475" y="305" fill="white" font-size="10" font-weight="600" text-anchor="middle">/api/v1/backups</text>
<text x="475" y="319" fill="#94a3b8" font-size="8" text-anchor="middle">Create | Restore | List</text>
<!-- Config Routes -->
<rect x="570" y="285" width="150" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="645" y="305" fill="white" font-size="10" font-weight="600" text-anchor="middle">/api/v1/config</text>
<text x="645" y="319" fill="#94a3b8" font-size="8" text-anchor="middle">Settings + Labels</text>
<!-- Middleware -->
<rect x="740" y="285" width="200" height="45" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
<text x="840" y="305" fill="white" font-size="10" font-weight="600" text-anchor="middle">Middleware</text>
<text x="840" y="319" fill="#94a3b8" font-size="8" text-anchor="middle">CORS • JSON parse • Errors</text>
<!-- Gateway details -->
<text x="60" y="355" fill="#34d399" font-size="9">Blueprint pattern • Solo escucha 127.0.0.1 • Sin exposición a red</text>
<!-- ===== SERVICE LAYER ===== -->
<rect x="40" y="400" width="920" height="100" rx="8" fill="rgba(251, 146, 60, 0.1)" stroke="#fb923c" stroke-width="1" stroke-dasharray="4,4"/>
<text x="52" y="418" fill="#fb923c" font-size="10" font-weight="600">CAPA DE SERVICIOS — Lógica de Negocio</text>
<rect x="60" y="430" width="140" height="45" rx="6" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1.5"/>
<text x="130" y="450" fill="white" font-size="10" font-weight="600" text-anchor="middle">DeviceService</text>
<text x="130" y="464" fill="#94a3b8" font-size="8" text-anchor="middle">CRUD + Lifecycle</text>
<rect x="220" y="430" width="140" height="45" rx="6" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1.5"/>
<text x="290" y="450" fill="white" font-size="10" font-weight="600" text-anchor="middle">BackupService</text>
<text x="290" y="464" fill="#94a3b8" font-size="8" text-anchor="middle">Schedule + Gzip</text>
<rect x="380" y="430" width="140" height="45" rx="6" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1.5"/>
<text x="450" y="450" fill="white" font-size="10" font-weight="600" text-anchor="middle">ImportService</text>
<text x="450" y="464" fill="#94a3b8" font-size="8" text-anchor="middle">CSV + Mapper</text>
<rect x="540" y="430" width="140" height="45" rx="6" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1.5"/>
<text x="610" y="450" fill="white" font-size="10" font-weight="600" text-anchor="middle">ExportService</text>
<text x="610" y="464" fill="#94a3b8" font-size="8" text-anchor="middle">CSV + Labels</text>
<rect x="700" y="430" width="140" height="45" rx="6" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1.5"/>
<text x="770" y="450" fill="white" font-size="10" font-weight="600" text-anchor="middle">ConfigService</text>
<text x="770" y="464" fill="#94a3b8" font-size="8" text-anchor="middle">KV Store + Labels</text>
<!-- Arrows from Gateway to Services -->
<line x1="130" y1="335" x2="130" y2="428" stroke="#fb923c" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="305" y1="335" x2="290" y2="428" stroke="#fb923c" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="475" y1="335" x2="450" y2="428" stroke="#fb923c" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="645" y1="335" x2="610" y2="428" stroke="#fb923c" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="840" y1="335" x2="770" y2="428" stroke="#fb923c" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<!-- ===== DATA LAYER ===== -->
<rect x="40" y="515" width="920" height="140" rx="8" fill="rgba(76, 29, 149, 0.15)" stroke="#a78bfa" stroke-width="1" stroke-dasharray="4,4"/>
<text x="52" y="533" fill="#a78bfa" font-size="10" font-weight="600">CAPA DE DATOS — SQLite (WAL mode)</text>
<rect x="60" y="545" width="130" height="45" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="125" y="565" fill="white" font-size="10" font-weight="600" text-anchor="middle">DeviceRepo</text>
<text x="125" y="579" fill="#94a3b8" font-size="8" text-anchor="middle">devices table</text>
<rect x="210" y="545" width="130" height="45" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="275" y="565" fill="white" font-size="10" font-weight="600" text-anchor="middle">HistoryRepo</text>
<text x="275" y="579" fill="#94a3b8" font-size="8" text-anchor="middle">device_history</text>
<rect x="360" y="545" width="130" height="45" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="425" y="565" fill="white" font-size="10" font-weight="600" text-anchor="middle">ConfigRepo</text>
<text x="425" y="579" fill="#94a3b8" font-size="8" text-anchor="middle">app_config + labels</text>
<rect x="510" y="545" width="130" height="45" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="575" y="565" fill="white" font-size="10" font-weight="600" text-anchor="middle">BackupRepo</text>
<text x="575" y="579" fill="#94a3b8" font-size="8" text-anchor="middle">backup_log</text>
<!-- SQLite Database -->
<rect x="680" y="545" width="260" height="90" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
<text x="810" y="565" fill="white" font-size="11" font-weight="600" text-anchor="middle">SQLite</text>
<text x="810" y="582" fill="#94a3b8" font-size="8" text-anchor="middle">inventschario.db</text>
<text x="810" y="598" fill="#a78bfa" font-size="8" text-anchor="middle">WAL mode • Foreign Keys</text>
<text x="810" y="614" fill="#94a3b8" font-size="8" text-anchor="middle">6 tablas • 4 índices únicos</text>
<text x="810" y="628" fill="#94a3b8" font-size="8" text-anchor="middle">%APPDATA%/inventschario/</text>
<!-- Arrows from Services to Repos -->
<line x1="130" y1="478" x2="125" y2="543" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="290" y1="478" x2="275" y2="543" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="450" y1="478" x2="425" y2="543" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<line x1="610" y1="478" x2="575" y2="543" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arrowhead)"/>
<!-- Arrows from Repos to SQLite -->
<line x1="190" y1="567" x2="678" y2="580" stroke="#a78bfa" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="340" y1="567" x2="678" y2="585" stroke="#a78bfa" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="490" y1="567" x2="678" y2="590" stroke="#a78bfa" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="640" y1="567" x2="678" y2="595" stroke="#a78bfa" stroke-width="1" stroke-dasharray="3,3"/>
<!-- ===== LEGEND ===== -->
<text x="60" y="680" fill="white" font-size="10" font-weight="600">Leyenda:</text>
<rect x="130" y="672" width="14" height="8" rx="2" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1"/>
<text x="150" y="680" fill="#94a3b8" font-size="8">Frontend</text>
<rect x="210" y="672" width="14" height="8" rx="2" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1"/>
<text x="230" y="680" fill="#94a3b8" font-size="8">API Gateway</text>
<rect x="310" y="672" width="14" height="8" rx="2" fill="rgba(251, 146, 60, 0.3)" stroke="#fb923c" stroke-width="1"/>
<text x="330" y="680" fill="#94a3b8" font-size="8">Servicios</text>
<rect x="400" y="672" width="14" height="8" rx="2" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1"/>
<text x="420" y="680" fill="#94a3b8" font-size="8">Datos</text>
<rect x="480" y="672" width="14" height="8" rx="2" fill="rgba(251, 191, 36, 0.05)" stroke="#fbbf24" stroke-width="1" stroke-dasharray="4,4"/>
<text x="500" y="680" fill="#94a3b8" font-size="8">Límite de máquina</text>
</svg>
</div>
<!-- Info Cards -->
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot cyan"></div>
<h3>Frontend</h3>
</div>
<ul>
<li>• HTML + CSS + Alpine.js (sin framework pesado)</li>
<li>• Router hash-based para SPA</li>
<li>• Componentes vanilla JS reutilizables</li>
<li>• WCAG 2.1 AA — contraste, teclado, labels</li>
<li>• CSS custom properties para theming</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot emerald"></div>
<h3>Backend</h3>
</div>
<ul>
<li>• Python 3.11+ con Flask</li>
<li>• API REST JSON sobre localhost</li>
<li>• Blueprint pattern por dominio</li>
<li>• Service layer separada de HTTP</li>
<li>• Schedule library para backups</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot violet"></div>
<h3>Datos</h3>
</div>
<ul>
<li>• SQLite con WAL mode</li>
<li>• Repository pattern por tabla</li>
<li>• Foreign keys + índices</li>
<li>• Migraciones versionadas</li>
<li>• Backups gzip en %APPDATA%</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot amber"></div>
<h3>Empaquetado</h3>
</div>
<ul>
<li>• PyInstaller → un único .exe (~30MB)</li>
<li>• Frontend empaquetado en el bundle</li>
<li>• Instalación en perfil de usuario</li>
<li>• Acceso directo en Menú Inicio</li>
<li>• Sin dependencias de runtime</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot rose"></div>
<h3>Seguridad</h3>
</div>
<ul>
<li>• Servidor solo en 127.0.0.1</li>
<li>• Sin exposición a la red</li>
<li>• Validación en backend (no confiar en frontend)</li>
<li>• Datos en %APPDATA% (aceso restringido)</li>
<li>• Foreign keys para integridad</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot cyan"></div>
<h3>Modularidad</h3>
</div>
<ul>
<li>• API REST como contrato frontend↔backend</li>
<li>• Frontend reemplazable sin tocar backend</li>
<li>• Plugin architecture para import/export</li>
<li>• Migración a .NET 8 o Go documentada</li>
<li>• Cada service es independiente</li>
</ul>
</div>
</div>
<p class="footer">
Inventschario v1.0.0 — Iteración 1 — 2026-08-03 — Python + Flask + SQLite
</p>
</div>
</body>
</html>

411
docs/arquitectura.md Normal file
View file

@ -0,0 +1,411 @@
# Inventschario — Documento de Arquitectura
**Versión:** 1.0.0-iteracion1
**Fecha:** 2026-08-03
**Estado:** Propuesta para revisión
---
## 1. Visión General
Inventschario采用 arquitectura de tres capas con separación clara entre
presentación, lógica de negocio y persistencia. La capa de presentación se
ejecuta en el navegador del usuario; la capa de negocio y persistencia corren
como un servidor local HTTP que solo escucha en `127.0.0.1`.
La pasarela entre frontend y backend es una API REST documentada, de forma que
el frontend puede sustituirse completamente sin modificar ninguna línea del
backend, y viceversa.
---
## 2. Diagrama de Componentes
```
┌─────────────────────────────────────────────────────────────────┐
│ NAVEGADOR (Edge) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ FRONTEND │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │Dashboard │ │Inventario│ │ Formular │ │ Config │ │ │
│ │ │ Page │ │ Page │ │ ios │ │ Page │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────┴──────────────┴──────────────┴──────────────┴────┐ │
│ │ │ Router (SPA) │ │
│ │ └────────────────────┬─────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────────────────┴─────────────────────────────────┐ │
│ │ │ API Client (fetch wrapper) │ │
│ │ └────────────────────┬─────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────────────────┴─────────────────────────────────┐ │
│ │ │ Component Library │ │
│ │ │ Table | Form | Modal | Toast | Sidebar | Charts │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────┬───────────────────────────────┘ │
│ │ HTTP localhost:PORT │
├──────────────────────────────┼──────────────────────────────────┤
│ │ │
│ ┌───────────────────────────┴───────────────────────────────┐ │
│ │ API GATEWAY │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ /devices │ │ /import │ │ /backups │ │ /config │ │ │
│ │ │ /history │ │ /export │ │ │ │ /labels │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────┴──────────────┴──────────────┴──────────────┴────┐ │ │
│ │ │ Middleware Layer │ │ │
│ │ │ CORS (localhost) | JSON parsing | Error handling │ │ │
│ │ └────────────────────┬─────────────────────────────────┘ │ │
│ └───────────────────────┼───────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┴───────────────────────────────────┐ │
│ │ SERVICE LAYER │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │DeviceService │ │BackupService │ │ImportService │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │- CRUD │ │- crear │ │- parse CSV │ │ │
│ │ │- buscar │ │- restaurar │ │- mapear │ │ │
│ │ │- filtrar │ │- eliminar │ │- validar │ │ │
│ │ │- baja │ │- verificar │ │- duplicados │ │ │
│ │ │- enajenar │ │ antigüedad │ │ │ │ │
│ │ │- historial │ │ │ │ │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────┴──────────────────┴──────────────────┴───────┐ │ │
│ │ │ ExportService | ConfigService │ │ │
│ │ └──────────────────────┬─────────────────────────────┘ │ │
│ └─────────────────────────┼─────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────┴─────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ DeviceRepo │ │ ConfigRepo │ │ BackupRepo │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ SQLite CRUD │ │ KV Store │ │ Log CRUD │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────┴──────────────────┴──────────────────┴───────┐ │ │
│ │ │ SQLite (WAL mode) │ │ │
│ │ │ inventschario.db │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 3. Alternativas de Arquitectura
### Alternativa A: Python + Flask (Recomendada)
```
[Browser] ──HTTP──► [Flask server] ──► [SQLite]
│ │
│ HTML/CSS/JS │ Python services
│ (Alpine.js) │ (DeviceService, etc.)
│ │
◄──── JSON ──────────►│
```
**Por qué se recomienda:**
- Python stdlib incluye sqlite3, http.server, json, gzip, csv, shutil
- Flask es ligero (~500KB) y no necesita build step
- PyInstaller empaqueta todo en un .exe autocontenido
- El ecosistema Python tiene librerías para todo lo necesario
- Desarrollo rápido, fácil de mantener
**Componentes:**
- `flask` — Servidor web minimalista
- `schedule` — Programador de tareas (backups diarios)
- `pyinstaller` — Empaquetado en .exe
### Alternativa B: .NET 8 Minimal API
```
[Browser] ──HTTP──► [Kestrel server] ──► [SQLite via EF Core]
│ │
│ HTML/CSS/JS │ C# services
│ (embebido) │ (DeviceService, etc.)
│ │
◄──── JSON ──────────►│
```
**Por qué es alternativa válida:**
- `dotnet publish -r win-x64 --self-contained` produce .exe sin runtime
- Rendimiento superior a Python en operaciones intensivas
- Integración nativa con Windows
- Entity Framework Core para ORM robusto
**Componentes:**
- ASP.NET Core Minimal API
- Microsoft.Data.Sqlite
- Hangfire (tareas programadas)
### Alternativa C: Go + net/http
```
[Browser] ──HTTP──► [Go HTTP server] ──► [SQLite via CGO]
│ │
│ HTML/CSS/JS │ Go handlers
│ (embed.FS) │ (DeviceService, etc.)
│ │
◄──── JSON ──────────►│
```
**Por qué es alternativa válida:**
- Binario más pequeño (10-15 MB)
- Rendimiento excepcional
- Sin dependencias en runtime
- `embed.FS` embebe archivos estáticos directamente en el binario
**Componentes:**
- `net/http` (stdlib)
- `mattn/go-sqlite3` (CGO)
- `embed.FS` (archivos estáticos)
### Decisión: Python + Flask
Para la primera iteración, Python + Flask es la opción que mejor equilibra:
- Velocidad de desarrollo
- Facilidad de distribución (PyInstaller)
- Modularidad del frontend (API REST pura)
- Ecosistema de herramientas
Si en iteraciones futuras el tamaño del .exe o la detección por antivirus
se convierten en problemas, se documentará la migración a .NET 8.
---
## 4. Patrones de Diseño
### 4.1 Repository Pattern (Capa de Datos)
Cada tabla tiene su repository que encapsula las consultas SQL:
```python
class DeviceRepository:
def __init__(self, db_path):
self.db_path = db_path
def get_all(self, filters=None): ...
def get_by_id(self, device_id): ...
def create(self, device_data): ...
def update(self, device_id, device_data): ...
def delete(self, device_id): ...
def search(self, query): ...
```
### 4.2 Service Layer (Lógica de Negocio)
Los services orquestan la lógica sin conocer la API HTTP:
```python
class DeviceService:
def __init__(self, device_repo, history_repo):
self.device_repo = device_repo
self.history_repo = history_repo
def create_device(self, data, user):
device = self.device_repo.create(data)
self.history_repo.log(device.id, 'created', user=user)
return device
def decommission(self, device_id, reason, user):
device = self.device_repo.get_by_id(device_id)
self.device_repo.update_status(device_id, 'decommissioned')
self.history_repo.log(device_id, 'decommissioned',
notes=reason, user=user)
```
### 4.3 API Gateway (Capa HTTP)
Las rutas API son delgadas — solo parsean la request, llaman al service,
y formatean la response:
```python
@bp.route('/api/v1/devices', methods=['POST'])
def create_device():
data = request.get_json()
device = device_service.create_device(data, current_user)
return jsonify(device.to_dict()), 201
```
### 4.4 Frontend Component Architecture
El frontend usa un patrón de componentes simples sin framework pesado:
```javascript
// Cada componente es un objeto con render() y bind()
const DeviceTable = {
render(devices) { return `<table>...</table>`; },
bind(container) { /* event listeners */ }
};
// El router cambia de página
const Router = {
routes: { '/': Dashboard, '/inventory': Inventory },
navigate(path) { /* render the page */ }
};
```
### 4.5 Import/Export Plugin Architecture
El sistema de importación exportación es un plugin architecture:
```python
class ImporterBase:
"""Clase abstracta para todos los importadores"""
def parse(self, file_path) -> list[dict]: ...
def validate(self, records) -> ValidationResult: ...
def import_records(self, records, strategy) -> ImportResult: ...
class CSVImporter(ImporterBase):
"""Implementación CSV"""
def parse(self, file_path):
# Lee CSV, mapea columnas, retorna lista de dicts
...
class FieldMapper:
"""Motor de mapeo de campos"""
def __init__(self, mapping_config):
self.mapping = mapping_config
def map_record(self, raw_record) -> dict:
# Aplica el mapeo configurado
...
```
Para añadir un nuevo formato (ej: Excel), se crea una nueva clase que
herede de `ImporterBase` sin tocar el resto del sistema.
---
## 5. Seguridad
### 5.1 Red
- El servidor Flask escucha SOLO en `127.0.0.1:PORT`
- No hay exposición a la red local o internet
- El puerto se asigna dinámicamente o se configura en el wizard
### 5.2 Datos
- SQLite WAL mode para integridad
- Foreign keys habilitadas
- Validación de datos en backend (no confiar en el frontend)
- Backups almacenados en `%APPDATA%` (acceso restringido al usuario)
### 5.3 Archivos
- No se ejecutan comandos del sistema
- No se escriben archivos fuera de `%APPDATA%/inventschario/`
- La BD se almacena en `%APPDATA%/inventschario/inventschario.db`
- Los logs se almacenan en `%APPDATA%/inventschario/logs/`
---
## 6. Rendimiento
### 6.1 Consideraciones
- Todo es local: latencia de red ~0ms
- SQLite es suficiente para inventarios de hasta 100,000 dispositivos
- WAL mode permite lecturas concurrentes
- Índices en campos de búsqueda frecuentes
### 6.2 Métricas Objetivo
| Operación | Tiempo objetivo |
|--------------------------------|-----------------|
| Carga de página inicial | < 2s |
| Búsqueda en inventario | < 500ms |
| Crear dispositivo | < 100ms |
| Exportar 1,000 dispositivos | < 3s |
| Importar 1,000 dispositivos | < 5s |
| Backup de BD de 10MB | < 2s |
---
## 7. Estrategia de Testing
### 7.1 Niveles
1. **Unit Tests** — Services, repositories, utilidades
2. **Integration Tests** — API endpoints con BD en memoria
3. **E2E Tests** — Flujo completo (futuro, iteración 2)
### 7.2 Herramientas
- `pytest` — Framework de testing
- `pytest-flask` — Fixtures para Flask
- `sqlite3` en memoria — BD de test aislada
### 7.3 Cobertura
Objetivo iteración 1: > 80% cobertura en services y repositories.
---
## 8. Despliegue
### 8.1 Distribución
```
inventschario/
├── inventschario.exe # Ejecutable empaquetado (PyInstaller)
├── inventschario.ico # Icono de la aplicación
└── README.md # Instrucciones de uso
```
### 8.2 Instalación (Usuario)
1. El usuario ejecuta `inventschario.exe`
2. PyInstaller extrae a un directorio temporal
3. La aplicación se instala en `%LOCALAPPDATA%/inventschario/`
4. Se crea un acceso directo en el Menú Inicio del usuario
5. Se lanza el navegador con la aplicación
### 8.3 Start Menu
El acceso directo se crea en:
```
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Inventschario/Inventschario.lnk
```
Esto requiere privilegios de instalador (que el usuario tiene).
---
## 9. Plan de Migración (Si es necesario)
Si Python + Flask no cumple los requisitos en producción:
### De Python a .NET 8
1. Mantener la misma API REST (los endpoints no cambian)
2. Reescribir los services en C#
3. Usar Entity Framework Core con SQLite
4. Publicar como self-contained: `dotnet publish -r win-x64 --self-contained`
5. El frontend NO cambia (solo se sirve desde wwwroot)
### De Python a Go
1. Mantener la misma API REST
2. Reescribir los services en Go
3. Usar `mattn/go-sqlite3`
4. Compilar como binario estático
5. Embeber frontend con `embed.FS`
En ambos casos, la API REST como contrato permite el cambio de backend
sin modificar el frontend.
---
*Documento generado como parte de la primera iteración del proyecto Inventschario.*

184
docs/roadmap-iteracion-1.md Normal file
View file

@ -0,0 +1,184 @@
# Inventschario — Roadmap Iteración 1
**Fecha:** 2026-08-03
**Objetivo:** MVP funcional con gestión de dispositivos, copias de seguridad
e importación/exportación CSV.
---
## Fase 0: Infraestructura del Proyecto
- [ ] Inicializar repositorio git
- [ ] Crear estructura de directorios
- [ ] Configurar `pyproject.toml` con dependencias
- [ ] Crear `requirements.txt`
- [ ] Configurar `.gitignore` (Python, SQLite, backups, __pycache__)
- [ ] Escribir `README.md` con instrucciones de desarrollo
## Fase 1: Capa de Datos
- [ ] Implementar `schema.sql` con el esquema completo
- [ ] Implementar `connection.py` (conexión SQLite, WAL mode, foreign keys)
- [ ] Implementar `migrations.py` (creación de tablas, upgrades futuros)
- [ ] Implementar `DeviceRepository` (CRUD completo)
- [ ] Implementar `HistoryRepository` (log de cambios)
- [ ] Implementar `ConfigRepository` (key-value store)
- [ ] Implementar `BackupRepository` (log de backups)
- [ ] Tests unitarios para todos los repositories
## Fase 2: Capa de Servicios
- [ ] Implementar `DeviceService` (CRUD, baja, enajenación, historial)
- [ ] Implementar `BackupService` (crear, restaurar, eliminar, verificar antigüedad)
- [ ] Implementar `ConfigService` (leer/actualizar config, labels, setup)
- [ ] Implementar `ImportService` (parse CSV, mapeo, validación, duplicados)
- [ ] Implementar `ExportService` (generar CSV con etiquetas renombradas)
- [ ] Tests unitarios para todos los services
## Fase 3: API Gateway
- [ ] Configurar Flask app con Blueprint pattern
- [ ] Implementar CORS para localhost
- [ ] Implementar middleware de errores
- [ ] Rutas: `/api/v1/devices` (CRUD, búsqueda, filtros)
- [ ] Rutas: `/api/v1/devices/:id/decommission`, `/dispose`
- [ ] Rutas: `/api/v1/devices/:id/history`
- [ ] Rutas: `/api/v1/config`, `/api/v1/config/labels`
- [ ] Rutas: `/api/v1/config/initialize`
- [ ] Rutas: `/api/v1/backups` (listar, crear, restaurar, eliminar)
- [ ] Rutas: `/api/v1/import/csv`, `/api/v1/export/csv`
- [ ] Rutas: `/api/v1/locations`, `/api/v1/departments`
- [ ] Tests de integración para todos los endpoints
## Fase 4: Frontend — Estructura Base
- [ ] Crear `index.html` con estructura SPA
- [ ] Implementar `app.js` (router hash-based)
- [ ] Implementar `api.js` (cliente HTTP con manejo de errores)
- [ ] Implementar sistema de componentes (table, form, modal, toast, sidebar)
- [ ] Crear estilos globales con CSS custom properties
- [ ] Implementar responsive layout (sidebar collapse)
## Fase 5: Frontend — Páginas
### 5.1 Dashboard
- [ ] Estadísticas: total dispositivos, por estado, por tipo
- [ ] Últimos dispositivos añadidos
- [ ] Alertas de garantía próxima (futuro, placeholder)
- [ ] Recordatorio de backups antiguos
### 5.2 Inventario (Tabla)
- [ ] Tabla paginada con sorting por columnas
- [ ] Barra de búsqueda full-text
- [ ] Filtros: tipo, estado, departamento, ubicación
- [ ] Acciones por fila: ver, editar, baja, enajenar
- [ ] Selección múltiple para exportación
### 5.3 Formulario de Dispositivo
- [ ] Campos obligatorios marcados
- [ ] Validación en tiempo real
- [ ] Autocompletado de campos de registro
- [ ] Selector de tipo de dispositivo
- [ ] Selector de ubicación / departamento
- [ ] Modo creación y modo edición
### 5.4 Detalle del Dispositivo
- [ ] Ficha completa con todos los campos
- [ ] Historial de cambios cronológico
- [ ] Botones de acción: editar, baja, enajenar
- [ ] Foto del dispositivo (futuro)
### 5.5 Baja / Enajenación
- [ ] Modal con tipo de baja/enajenación
- [ ] Campo de fecha efectiva
- [ ] Campo de motivo / observaciones
- [ ] Campo de responsable autorizador
- [ ] Confirmación antes de ejecutar
### 5.6 Importar / Exportar
- [ ] Selector de archivo CSV para importar
- [ ] Vista previa de los primeros 10 registros
- [ ] Mapeo de columnas (automático por nombre/posición)
- [ ] Opciones ante duplicados
- [ ] Registro de errores por fila
- [ ] Botón de exportación con filtros actuales
- [ ] Selector de separador CSV
### 5.7 Configuración
- [ ] Nombre de institución
- [ ] Edición de etiquetas de campos de registro
- [ ] Configuración de backups (hora, retención)
- [ ] Gestión de ubicaciones
- [ ] Gestión de departamentos
### 5.8 Wizard de Configuración Inicial
- [ ] Detección de primera ejecución
- [ ] 6 pasos del wizard
- [ ] Validación en cada paso
- [ ] Creación de la BD al finalizar
## Fase 6: Copias de Seguridad
- [ ] Implementar scheduler con `schedule` library
- [ ] Backup automático a la hora configurada
- [ ] Compresión gzip del archivo .db
- [ ] Registro en `backup_log`
- [ ] Verificación de backups antiguos al iniciar
- [ ] Toast de recordatorio al usuario
- [ ] Función de restaurar desde backup
## Fase 7: Empaquetado
- [ ] Configurar PyInstaller spec file
- [ ] Incluir frontend estático en el bundle
- [ ] Crear icono de la aplicación (.ico)
- [ ] Script de instalación (crear directorio + acceso directo Menú Inicio)
- [ ] Test de distribución: ejecutar en máquina limpia de Windows
## Fase 8: Testing y Calidad
- [ ] Tests unitarios: > 80% cobertura en services
- [ ] Tests de integración: todos los endpoints API
- [ ] Prueba manual de flujo completo
- [ ] Revisión de accesibilidad (WCAG 2.1 AA)
- [ ] Prueba de rendimiento con 1,000 dispositivos
---
## Estimación de Esfuerzo
| Fase | Días estimados | Dependencias |
|-------|:--------------:|-----------------|
| 0 | 0.5 | — |
| 1 | 2 | Fase 0 |
| 2 | 2 | Fase 1 |
| 3 | 2 | Fase 2 |
| 4 | 2 | Fase 0 |
| 5 | 5 | Fases 3, 4 |
| 6 | 1 | Fase 1 |
| 7 | 1 | Fases 3-6 |
| 8 | 1 | Fases 3-7 |
| **Total** | **~16 días** | |
---
## Criterios de Salida de la Iteración 1
1. El usuario puede instalar la app en su perfil de Windows
2. El wizard de configuración crea la BD correctamente
3. Se pueden crear, editar, ver y eliminar dispositivos
4. Se pueden dar de baja y enajenar dispositivos
5. La tabla de inventario tiene búsqueda, filtros y ordenación
6. Las etiquetas de campos son renombrables
7. Los backups se ejecutan diariamente
8. Se muestra recordatorio de backups antiguos
9. Se puede exportar a CSV
10. Se puede importar desde CSV
11. El historial de cambios se registra
12. La interfaz es navegable por teclado
13. Los colores cumplen WCAG 2.1 AA
---
*Roadmap generado como parte de la primera iteración del proyecto Inventschario.*
*Este documento es la base para el feedback del usuario antes de comenzar la implementación.*

836
especificaciones.md Normal file
View file

@ -0,0 +1,836 @@
# Inventschario — Especificaciones del Programa
**Versión:** 1.0.0-iteracion1
**Fecha:** 2026-08-03
**Estado:** Borrador para feedback del usuario
---
## 1. Resumen del Producto
**Inventschario** es una aplicación de inventario de dispositivos electrónicos no
prestables (ordenadores, monitores, periféricos, equipos de red, etc.) diseñada
para ejecutarse en una única máquina Windows 11 Pro unida a un dominio. La
aplicación se distribuye como un único paquete, se ejecuta en el navegador por
defecto del usuario y almacena sus datos en una base de datos SQLite local.
El usuario tiene privilegios de instalador pero NO de administrador. La
aplicación se instala únicamente en el directorio del usuario y aparece solo en
el Menú Inicio de dicho usuario.
---
## 2. Restricciones del Entorno
| Restricción | Detalle |
|------------------------------|-------------------------------------------------------------------------|
| SO | Windows 11 Pro |
| Dominio | Unida a dominio corporativo |
| Privilegios del usuario | Instalador (puede instalar software en su perfil), NO administrador |
| Distribución | Un único paquete autocontenido (sin dependencias externas en runtime) |
| Interfaz | Navegador por defecto del sistema (Edge), se abre al doble clic |
| Instalación | Solo en el directorio del usuario, Menú Inicio del usuario |
| Red | Mínima latencia (frontend y backend en la misma máquina) |
| Copias de seguridad | Automáticas diarias, recordatorio para limpieza de antiguas |
---
## 3. Funcionalidades Principales
### 3.1 Gestión de Dispositivos
#### 3.1.1 Campos de Identificación (Renombrables)
La aplicación debe soportar múltiples campos de registro para cada dispositivo.
El usuario puede renombrar la etiqueta visible de cada campo en la interfaz,
pero el nombre interno en la base de datos permanece constante.
| Campo BD (interno) | Etiqueta por defecto | Tipo | Obligatorio |
|------------------------|-------------------------------|------------|-------------|
| `serial_number` | Número de Serie | Texto | Sí |
| `service_tag` | Service Tag | Texto | No |
| `catalog_number` | Número de Catálogo Interno | Texto | Sí |
| `additional_registry` | Registro Adicional | Texto | No |
El sistema de renombrado se almacena en una tabla de configuración:
```sql
CREATE TABLE field_labels (
field_key TEXT PRIMARY KEY, -- e.g. 'serial_number'
label TEXT NOT NULL, -- e.g. 'Número de Serie'
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
#### 3.1.2 Campos Descriptivos del Dispositivo
| Campo BD | Etiqueta por defecto | Tipo | Obligatorio |
|-------------------------|----------------------------|------------|-------------|
| `device_name` | Nombre / Descripción | Texto | Sí |
| `device_type` | Tipo de Dispositivo | Enum | Sí |
| `brand` | Marca | Texto | No |
| `model` | Modelo | Texto | No |
| `serial_internal` | Número de Serie Interno | Texto | No |
| `purchase_date` | Fecha de Compra | Fecha | No |
| `warranty_expiry` | Fin de Garantía | Fecha | No |
| `purchase_price` | Precio de Compra (€) | Decimal | No |
| `assigned_location` | Ubicación Asignada | Texto | No |
| `assigned_user` | Usuario Asignado | Texto | No |
| `department` | Departamento | Texto | No |
| `status` | Estado | Enum | Sí |
| `condition` | Estado Físico | Enum | No |
| `notes` | Notas | Texto | No |
| `photo_path` | Ruta Foto | Texto | No |
#### 3.1.3 Tipos de Dispositivo (Enum)
- `computer` — Ordenador de sobremesa
- `laptop` — Portátil
- `monitor` — Monitor / Pantalla
- `printer` — Impresora
- `scanner` — Escáner
- `network_device` — Dispositivo de red (switch, router, AP)
- `peripheral` — Periférico (teclado, ratón, auriculares)
- `server` — Servidor
- `ups` — SAI / Regulador
- `storage` — Almacenamiento externo
- `other` — Otro
#### 3.1.4 Estados del Dispositivo (Enum)
| Estado | Descripción |
|-----------------|------------------------------------------------|
| `active` | En uso, asignado y operativo |
| `available` | Disponible, sin asignar |
| `maintenance` | En mantenimiento / reparación |
| `decommissioned`| Dado de baja |
| `disposed` | Enajenado / desechado |
| `lost` | Extraviado / no localizado |
| `reserved` | Reservado para asignación futura |
#### 3.1.5 Estado Físico (Enum)
- `excellent` — Excelente
- `good` — Bueno
- `fair` — Regular
- `poor` — Malo
- `damaged` — Dañado
### 3.2 Ciclo de Vida del Dispositivo (Administración Pública)
La aplicación debe gestionar el ciclo de vida completo de cada dispositivo,
incluyendo trámites de administración pública:
#### 3.2.1 Altas
- Registro manual de un nuevo dispositivo
- Importación masiva desde CSV
- Asignación automática de número de inventario interno
#### 3.2.2 Modificaciones
- Edición de cualquier campo del dispositivo
- Historial de cambios (quién, cuándo, qué cambió)
- Reasignación de ubicación / usuario / departamento
#### 3.2.3 Bajas
- Baja por obsolescencia
- Baja por avería irreversible
- Baja por robo / extravío
- Baja por fin de garantía sin renovación
- Registro del motivo de baja
- Fecha efectiva de baja
- Responsable que autoriza la baja
#### 3.2.4 Enajenación
- Enajenación por venta
- Enajenación por donación
- Enajenación por transferencia a otro organismo
- Registro del destinatario / adquirente
- Documento de enajenación (referencia)
- Importe de enajenación si aplica
- Fecha efectiva de enajenación
#### 3.2.5 Historial de Cambios
```sql
CREATE TABLE device_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id INTEGER NOT NULL,
action TEXT NOT NULL, -- 'created', 'modified', 'decommissioned', 'disposed', ...
field_changed TEXT, -- NULL para acciones de ciclo completo
old_value TEXT,
new_value TEXT,
performed_by TEXT NOT NULL,
performed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notes TEXT,
FOREIGN KEY (device_id) REFERENCES devices(id)
);
```
### 3.3 Copias de Seguridad
#### 3.3.1 Backup Automático Diario
- Se ejecuta una vez al día (configurable la hora)
- Copia completa de la base de datos SQLite
- Almacena en `%APPDATA%/inventschario/backups/`
- Formato de nombre: `inventschario_YYYYMMDD_HHMMSS.db`
- Comprimido en formato `.gz` para ahorrar espacio
#### 3.3.2 Gestión de Backups
- Al iniciar la aplicación, comprueba si hay backups con más de N días (configurable)
- Muestra un recordatorio al usuario indicando cuántos backups antiguos existen
- El usuario decide eliminar los antiguos o mantenerlos
- Opción para crear un backup manual en cualquier momento
- Opción para restaurar desde un backup específico
#### 3.3.3 Configuración de Backups
| Parámetro | Valor por defecto | Descripción |
|------------------------------|-------------------|---------------------------------------|
| `backup_enabled` | `true` | Habilitar backups automáticos |
| `backup_hour` | `02:00` | Hora diaria del backup |
| `backup_retention_days` | `30` | Días antes de sugerir eliminación |
| `backup_max_keep` | `90` | Máximo de backups a conservar |
| `backup_path` | `%APPDATA%/inventschario/backups/` | Ruta de almacenamiento |
### 3.4 Importación y Exportación
#### 3.4.1 Exportación a CSV
- Exporta todos los dispositivos o una selección filtrada
- Usa las etiquetas renombradas como cabeceras del CSV
- Separador configurable (coma, punto y coma, tabulador)
- Codificación UTF-8 con BOM para compatibilidad con Excel
- Opción de incluir solo columnas visibles
#### 3.4.2 Importación desde CSV
Sistema modular diseñado para ser extensible:
**Fase 1 (Implementación inicial):**
- Selección del archivo CSV
- Mapeo automático de columnas por posición o nombre
- Vista previa de los primeros 10 registros
- Detección de duplicados (por serial_number o catalog_number)
- Opciones ante duplicados: saltar, sobrescribir, crear como nuevo
- Registro de errores por fila
**Fase 2 (Extensibilidad futura):**
- Interfaz de mapeo visual de campos (drag & drop)
- Guardar perfiles de importación (plantillas)
- Transformaciones de datos durante la importación (regex, lookup)
- Importación desde Excel (.xlsx) directamente
#### 3.4.3 Arquitectura Modular de Importación/Exportación
```
import_export/
├── __init__.py
├── base.py # Clases abstractas ImporterBase, ExporterBase
├── csv_importer.py # Implementación CSV
├── csv_exporter.py # Implementación CSV
├── field_mapper.py # Motor de mapeo de campos
├── validator.py # Validación de datos importados
└── profiles/ # Perfiles de importación guardados
```
### 3.5 Interfaz de Usuario
#### 3.5.1 Requisitos de Usabilidad y Accesibilidad
- Navegación por teclado completa (Tab, Enter, Escape, atajos)
- Contraste de color WCAG 2.1 AA mínimo (4.5:1 texto, 3:1 elementos UI)
- Tamaño mínimo de objetivo de clic: 44x44 px
- Labels asociados a todos los inputs
- Mensajes de error claros y posicionados junto al campo
- Feedback visual en hover, focus, y active states
- Responsive (aunque el uso principal es escritorio)
- Modo de alto contraste soportado
- Texto alternativo en iconos decorativos
#### 3.5.2 Pantallas / Pestañas
1. **Dashboard** — Vista resumen con estadísticas clave
2. **Inventario** — Tabla de dispositivos con búsqueda, filtros, ordenación
3. **Agregar Dispositivo** — Formulario de alta
4. **Detalle / Edición** — Ficha completa del dispositivo con historial
5. **Baja / Enajenación** — Formulario de ciclo de vida
6. **Importar / Exportar** — Interfaz de importación y exportación CSV
7. **Configuración** — Parámetros generales, labels de campos, backups
8. **Configuración Inicial** — Wizard de primera ejecución
#### 3.5.3 Configuración Inicial (Primera Ejecución)
Al detectar que no existe base de datos, se muestra un wizard:
1. **Paso 1:** Bienvenida y nombre de la institución / organismo
2. **Paso 2:** Configuración de campos de registro (renombrar etiquetas)
3. **Paso 3:** Tipos de dispositivo a gestionar (seleccionar cuáles activar)
4. **Paso 4:** Configuración de copias de seguridad
5. **Paso 5:** Ubicaciones / Departamentos predefinidos (opcional)
6. **Paso 6:** Confirmación y creación de la base de datos
---
## 4. Modelo de Datos (SQLite)
### 4.1 Diagrama Entidad-Relación
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ devices │ │ device_history │ │ field_labels │
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
│ id (PK) │◄────│ device_id (FK) │ │ field_key (PK) │
│ serial_number │ │ id (PK) │ │ label │
│ service_tag │ │ action │ │ updated_at │
│ catalog_number │ │ field_changed │ └─────────────────┘
│ additional_reg │ │ old_value │
│ device_name │ │ new_value │ ┌──────────────────┐
│ device_type │ │ performed_by │ │ app_config │
│ brand │ │ performed_at │ ├──────────────────┤
│ model │ │ notes │ │ key (PK) │
│ serial_internal │ └──────────────────┘ │ value │
│ purchase_date │ │ updated_at │
│ warranty_expiry │ ┌──────────────────┐ └──────────────────┘
│ purchase_price │ │ locations │
│ assigned_loc │ ├──────────────────┤ ┌──────────────────┐
│ assigned_user │ │ id (PK) │ │ departments │
│ department │ │ name │ ├──────────────────┤
│ status │ │ building │ │ id (PK) │
│ condition │ │ floor │ │ name │
│ notes │ │ room │ └──────────────────┘
│ photo_path │ └──────────────────┘
│ created_at │
│ updated_at │ ┌──────────────────┐
└─────────────────┘ │ backup_log │
├──────────────────┤
│ id (PK) │
│ filename │
│ size_bytes │
│ created_at │
│ is自動 │
└──────────────────┘
```
### 4.2 Script de Creación de la Base de Datos
```sql
-- Inventschario v1.0.0
-- Base de datos de inventario de dispositivos electrónicos
PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;
CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
serial_number TEXT NOT NULL,
service_tag TEXT,
catalog_number TEXT NOT NULL,
additional_registry TEXT,
device_name TEXT NOT NULL,
device_type TEXT NOT NULL DEFAULT 'other',
brand TEXT,
model TEXT,
serial_internal TEXT,
purchase_date DATE,
warranty_expiry DATE,
purchase_price DECIMAL(10,2),
assigned_location TEXT,
assigned_user TEXT,
department TEXT,
status TEXT NOT NULL DEFAULT 'active',
condition TEXT DEFAULT 'good',
notes TEXT,
photo_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX idx_devices_catalog ON devices(catalog_number);
CREATE INDEX idx_devices_serial ON devices(serial_number);
CREATE INDEX idx_devices_status ON devices(status);
CREATE INDEX idx_devices_type ON devices(device_type);
CREATE TABLE device_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id INTEGER NOT NULL,
action TEXT NOT NULL,
field_changed TEXT,
old_value TEXT,
new_value TEXT,
performed_by TEXT NOT NULL,
performed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
notes TEXT,
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
);
CREATE INDEX idx_history_device ON device_history(device_id);
CREATE INDEX idx_history_date ON device_history(performed_at);
CREATE TABLE field_labels (
field_key TEXT PRIMARY KEY,
label TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE app_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
building TEXT,
floor TEXT,
room TEXT
);
CREATE TABLE departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE backup_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
size_bytes INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_auto BOOLEAN DEFAULT 1
);
-- Valores por defecto para field_labels
INSERT INTO field_labels (field_key, label) VALUES
('serial_number', 'Número de Serie'),
('service_tag', 'Service Tag'),
('catalog_number', 'Número de Catálogo Interno'),
('additional_registry', 'Registro Adicional'),
('device_name', 'Nombre / Descripción'),
('device_type', 'Tipo de Dispositivo'),
('brand', 'Marca'),
('model', 'Modelo'),
('purchase_date', 'Fecha de Compra'),
('warranty_expiry', 'Fin de Garantía'),
('purchase_price', 'Precio de Compra (€)'),
('assigned_location', 'Ubicación Asignada'),
('assigned_user', 'Usuario Asignado'),
('department', 'Departamento'),
('status', 'Estado'),
('condition', 'Estado Físico');
-- Valores por defecto para app_config
INSERT INTO app_config (key, value) VALUES
('institution_name', ''),
('backup_enabled', 'true'),
('backup_hour', '02:00'),
('backup_retention_days', '30'),
('backup_max_keep', '90'),
('csv_separator', ';'),
('csv_encoding', 'utf-8-sig'),
('setup_complete', 'false');
```
---
## 5. Arquitectura del Sistema
### 5.1 Requisitos Arquitectónicos
- **Modularidad total:** Separación frontend / backend con pasarela (API)
- **Independencia de interfaz:** Cambiar el frontend no afecta al backend
- **Autocontenido:** Todas las dependencias empaquetadas en un solo ejecutable
- **Sin privilegios de admin:** Instalación y ejecución en perfil de usuario
- **Baja latencia:** Frontend y backend en la misma máquina
### 5.2 Capas
```
┌─────────────────────────────────────────────┐
│ NAVEGADOR (Edge) │
│ ┌───────────────────────────────────────┐ │
│ │ FRONTEND (HTML/CSS/JS) │ │
│ │ Framework ligero: Alpine.js o Vanilla│ │
│ └──────────────────┬────────────────────┘ │
│ │ HTTP/localhost │
├─────────────────────┼───────────────────────┤
│ SERVIDOR LOCAL │
│ ┌──────────────────┴────────────────────┐ │
│ │ API GATEWAY (REST/JSON) │ │
│ │ Endpoints: /api/devices, /api/... │ │
│ └──────────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────────┴────────────────────┐ │
│ │ CAPA DE SERVICIOS │ │
│ │ DeviceService, BackupService, etc. │ │
│ └──────────────────┬────────────────────┘ │
│ │ │
│ ┌──────────────────┴────────────────────┐ │
│ │ CAPA DE DATOS (SQLite) │ │
│ │ Repository pattern, migraciones │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
### 5.3 API REST (Especificación)
Todas las rutas comienzan con `/api/v1/`.
#### Dispositivos
| Método | Ruta | Descripción |
|--------|------------------------------|-----------------------------------|
| GET | `/api/v1/devices` | Listar dispositivos (con filtros) |
| GET | `/api/v1/devices/:id` | Obtener dispositivo por ID |
| POST | `/api/v1/devices` | Crear dispositivo |
| PUT | `/api/v1/devices/:id` | Actualizar dispositivo |
| DELETE | `/api/v1/devices/:id` | Eliminar dispositivo (soft delete)|
| POST | `/api/v1/devices/:id/decommission` | Dar de baja |
| POST | `/api/v1/devices/:id/dispose` | Enajenar |
| GET | `/api/v1/devices/:id/history` | Historial de cambios |
#### Importación / Exportación
| Método | Ruta | Descripción |
|--------|------------------------------|-----------------------------------|
| GET | `/api/v1/export/csv` | Exportar a CSV |
| POST | `/api/v1/import/csv` | Importar desde CSV |
| GET | `/api/v1/import/preview` | Vista previa de importación |
#### Configuración
| Método | Ruta | Descripción |
|--------|------------------------------|-----------------------------------|
| GET | `/api/v1/config` | Obtener configuración |
| PUT | `/api/v1/config` | Actualizar configuración |
| GET | `/api/v1/config/labels` | Obtener etiquetas de campos |
| PUT | `/api/v1/config/labels` | Actualizar etiquetas de campos |
| POST | `/api/v1/config/initialize` | Ejecutar wizard de configuración |
#### Backups
| Método | Ruta | Descripción |
|--------|------------------------------|-----------------------------------|
| GET | `/api/v1/backups` | Listar backups |
| POST | `/api/v1/backups/create` | Crear backup manual |
| POST | `/api/v1/backups/:id/restore`| Restaurar desde backup |
| DELETE | `/api/v1/backups/:id` | Eliminar backup |
| GET | `/api/v1/backups/check-aging`| Verificar backups antiguos |
#### Ubicaciones y Departamentos
| Método | Ruta | Descripción |
|--------|------------------------------|-----------------------------------|
| GET | `/api/v1/locations` | Listar ubicaciones |
| POST | `/api/v1/locations` | Crear ubicación |
| GET | `/api/v1/departments` | Listar departamentos |
| POST | `/api/v1/departments` | Crear departamento |
---
## 6. Alternativas Tecnológicas
### Opción A: Python + Flask + SQLite (Recomendada)
| Aspecto | Detalle |
|------------------|-----------------------------------------------------------|
| Backend | Python 3.11+ con Flask |
| Base de datos | SQLite3 (incluido en Python stdlib) |
| Frontend | HTML/CSS/JS + Alpine.js (ligero, sin build step) |
| Empaquetado | PyInstaller → un único `.exe` |
| Tamaño aprox. | 25-40 MB |
| Ventajas | Ecosistema rico, fácil de desarrollar, SQLite nativo |
| Desventajas | PyInstaller a veces tiene problemas con antimalware |
| Dependencias | Flask, schedule (para backups) — todo empaquetado |
### Opción B: .NET 8 Self-Contained
| Aspecto | Detalle |
|------------------|-----------------------------------------------------------|
| Backend | ASP.NET Core minimal API |
| Base de datos | Microsoft.Data.Sqlite |
| Frontend | HTML/CSS/JS embebido en wwwroot |
| Empaquetado | `dotnet publish -r win-x64 --self-contained` |
| Tamaño aprox. | 60-80 MB |
| Ventajas | Nativo Windows, sin runtime externo, excelente rendimiento|
| Desventajas | Más verboso que Python, curva de aprendizaje mayor |
| Dependencias | Ninguna en runtime (self-contained) |
### Opción C: Go + SQLite
| Aspecto | Detalle |
|------------------|-----------------------------------------------------------|
| Backend | Go stdlib `net/http` + `mattn/go-sqlite3` |
| Base de datos | SQLite via CGO |
| Frontend | HTML/CSS/JS embebido via `embed` |
| Empaquetado | Binario compilado estático |
| Tamaño aprox. | 10-15 MB |
| Ventajas | Binario más pequeño, rendimiento excepcional |
| Desventajas | CGO complicationa el build cross-platform, menos ecosistema web |
| Dependencias | Ninguna en runtime |
### Opción D: Electron + SQLite
| Aspecto | Detalle |
|------------------|-----------------------------------------------------------|
| Backend | Node.js (embebido en Electron) |
| Base de datos | better-sqlite3 |
| Frontend | React/Vue/Svelte dentro de Electron |
| Empaquetado | electron-builder → instalador .exe |
| Tamaño aprox. | 150-200 MB |
| Ventajas | UI rica, familiar para devs frontend |
| Desventajas | Muy pesado para una app de inventario, innecesario |
| Dependencias | Chromium embebido |
### Comparativa Resumen
| Criterio | Python+Flask | .NET 8 | Go | Electron |
|------------------------|:------------:|:---------:|:---------:|:---------:|
| Tamaño del paquete | ★★★★ | ★★★ | ★★★★★ | ★★ |
| Facilidad de desarrollo| ★★★★★ | ★★★ | ★★★ | ★★★★ |
| Rendimiento | ★★★ | ★★★★★ | ★★★★★ | ★★★ |
| Sin dependencias RT | ★★★ (PyInst)| ★★★★★ | ★★★★★ | ★★★ |
| Ecosistema web | ★★★★★ | ★★★★ | ★★★ | ★★★★★ |
| Compatibilidad AV | ★★★ | ★★★★★ | ★★★★★ | ★★★★ |
| Modularidad frontend | ★★★★★ | ★★★★★ | ★★★★ | ★★★★★ |
**Recomendación:** Python + Flask (Opción A) para la iteración inicial. Si el
tamaño o la detección por antivirus se convierten en problemas, migrar a .NET 8.
---
## 7. Estructura de Directorios
```
inventschario/
├── especificaciones.md # Este documento
├── docs/
│ ├── arquitectura.md # Documento de arquitectura detallada
│ └── roadmap-iteracion-1.md # Roadmap de la primera iteración
├── sketches/ # Mockups de diseño de interfaz
│ ├── 001-dashboard/
│ ├── 002-inventario/
│ ├── 003-formulario/
│ └── 004-configuracion/
├── src/
│ ├── __init__.py
│ ├── main.py # Punto de entrada
│ ├── server.py # Servidor Flask
│ ├── config.py # Configuración de la aplicación
│ ├── database/
│ │ ├── __init__.py
│ │ ├── connection.py # Conexión SQLite
│ │ ├── models.py # Modelos de datos
│ │ ├── migrations.py # Migraciones de esquema
│ │ └── schema.sql # Esquema inicial
│ ├── services/
│ │ ├── __init__.py
│ │ ├── device_service.py # Lógica de dispositivos
│ │ ├── backup_service.py # Gestión de backups
│ │ ├── import_service.py # Importación CSV
│ │ ├── export_service.py # Exportación CSV
│ │ └── config_service.py # Configuración general
│ ├── api/
│ │ ├── __init__.py
│ │ ├── devices.py # Rutas API de dispositivos
│ │ ├── imports.py # Rutas API de importación
│ │ ├── exports.py # Rutas API de exportación
│ │ ├── backups.py # Rutas API de backups
│ │ └── config.py # Rutas API de configuración
│ ├── import_export/
│ │ ├── __init__.py
│ │ ├── base.py # Clases abstractas
│ │ ├── csv_importer.py # Importador CSV
│ │ ├── csv_exporter.py # Exportador CSV
│ │ ├── field_mapper.py # Motor de mapeo
│ │ └── validator.py # Validación de datos
│ └── utils/
│ ├── __init__.py
│ ├── scheduler.py # Programador de backups
│ └── paths.py # Rutas de archivos
├── frontend/
│ ├── index.html # Página principal (SPA)
│ ├── css/
│ │ └── styles.css # Estilos globales
│ ├── js/
│ │ ├── app.js # Router y estado global
│ │ ├── api.js # Cliente HTTP para la API
│ │ ├── components/ # Componentes reutilizables
│ │ │ ├── table.js
│ │ │ ├── form.js
│ │ │ ├── modal.js
│ │ │ ├── toast.js
│ │ │ └── sidebar.js
│ │ └── pages/ # Páginas / vistas
│ │ ├── dashboard.js
│ │ ├── inventory.js
│ │ ├── device-form.js
│ │ ├── device-detail.js
│ │ ├── lifecycle.js
│ │ ├── import-export.js
│ │ ├── settings.js
│ │ └── setup-wizard.js
│ └── assets/
│ └── icons/ # Iconos SVG
├── tests/
│ ├── test_devices.py
│ ├── test_import.py
│ ├── test_backup.py
│ └── test_config.py
├── pyproject.toml
├── requirements.txt
└── README.md
```
---
## 8. Requisitos No Funcionales
### 8.1 Rendimiento
- Tiempo de respuesta API: < 100ms (local)
- Carga de página inicial: < 2 segundos
- Búsqueda en inventario: < 500ms con 10,000 dispositivos
- Importación CSV: < 5 segundos para 1,000 registros
### 8.2 Seguridad
- El servidor solo escucha en `127.0.0.1` (localhost)
- No se exponen puertos a la red
- No se almacenan credenciales en texto plano
- Backups cifrados (futuro, iteración 2)
- Logs de auditoría para acciones críticas
### 8.3 Fiabilidad
- WAL mode en SQLite para concurrencia de lectura
- Backups automáticos diarios
- Integridad referencial con foreign keys
- Validación de datos tanto en backend como frontend
### 8.4 Mantenibilidad
- Código modular con separation of concerns
- API REST bien documentada
- Tests unitarios y de integración
- Migraciones de esquema versionadas
---
## 9. Flujo de Usuario — Escenarios Principales
### 9.1 Primera Ejecución
```
Doble clic en icono del Menú Inicio
→ Se abre Edge en http://localhost:PORT
→ Detector: no hay DB → Wizard de configuración
→ Paso 1: Nombre de institución
→ Paso 2: Etiquetas de campos de registro
→ Paso 3: Tipos de dispositivo a gestionar
→ Paso 4: Configuración de backups
→ Paso 5: Ubicaciones y departamentos (opcional)
→ Paso 6: Confirmación → Crear DB
→ Redirige al Dashboard
```
### 9.2 Alta de Dispositivo
```
Click "Agregar Dispositivo" (sidebar o botón)
→ Formulario con campos obligatorios marcados
→ Autocompletado de campos numéricos de registro
→ Validación en tiempo real
→ Submit → Guarda en BD → Historial "created"
→ Toast de confirmación → permanece en formulario para siguiente alta
```
### 9.3 Búsqueda y Edición
```
En pestaña Inventario:
→ Barra de búsqueda (búsqueda full-text)
→ Filtros: tipo, estado, departamento, ubicación
→ Click en fila → Detalle del dispositivo
→ Click "Editar" → Modo edición inline
→ Cambios guardados → Historial "modified"
```
### 9.4 Baja / Enajenación
```
En detalle del dispositivo:
→ Click "Dar de Baja" o "Enajenar"
→ Modal con:
- Tipo de baja/enajenación
- Fecha efectiva
- Motivo / observaciones
- Responsable autorizador
→ Confirmar → Estado cambia → Historial actualizado
→ Dispositivo aparece como "Baja" / "Enajenado" en el inventario
```
### 9.5 Backup Diario
```
Servidor inicia → Scheduler programado a las 02:00
→ BackupService.crear_backup():
- Copia el archivo .db
- Comprime con gzip
- Registra en backup_log
→ Al iniciar la app, BackupService.verificar_antiguos():
- Si hay backups > retention_days → Toast informativo
- "Hay X backups con más de N días. ¿Desea eliminarlos?"
- Botones: "Eliminar antiguos" / "Mantener"
```
---
## 10. Criterios de Aceptación — Iteración 1
- [ ] El usuario puede instalar la aplicación en su perfil de Windows
- [ ] El icono aparece en el Menú Inicio del usuario
- [ ] Al doble clic, se abre Edge con la aplicación
- [ ] El wizard de configuración initial crea la BD correctamente
- [ ] Se pueden crear, editar, y dar de baja dispositivos
- [ ] La tabla de inventario muestra búsqueda, filtros y ordenación
- [ ] Se pueden renombrar las etiquetas de los campos de registro
- [ ] Las copias de seguridad se ejecutan diariamente
- [ ] Se muestra recordatorio de backups antiguos al iniciar
- [ ] Se pueden exportar dispositivos a CSV
- [ ] Se pueden importar dispositivos desde CSV
- [ ] El historial de cambios se registra correctamente
- [ ] La interfaz es navegable por teclado
- [ ] Los colores cumplen WCAG 2.1 AA
---
## 11. Futuras Iteraciones (Roadmap)
### Iteración 2
- Cifrado de backups
- Autenticación de usuario (si se necesita multi-usuario)
- Informes y gráficos (distribución por tipo, estado, departamento)
- Impresión de etiquetas de inventario
### Iteración 3
- Escáner de códigos de barras / QR
- Sincronización con Active Directory
- API para integración con otros sistemas
- Importación desde Excel (.xlsx)
### Iteración 4
- Multi-idioma (i18n)
- Modo oscuro
- Notificaciones de garantía próxima a vencer
- Dashboard con métricas avanzadas
---
*Documento generado como parte de la primera iteración del proyecto Inventschario.
Sujeto a revisión y feedback del usuario.*

View file

@ -0,0 +1,18 @@
# Variant: Dashboard — Monitor Surface
## Design stance
Data-first density: the user is watching state change. Stats cards at the top,
recent items table, and distribution chart — no hero, no marketing.
## Key choices
- **Layout:** Sidebar + main content, 4-column stats grid, 2-column lower section
- **Typography:** Inter, 32px stat values, 13px body
- **Color:** Neutral (#1a1a1a accent, #f7f7f5 background), status badges with semantic colors
- **Interaction:** Hover on table rows, clickable chart bars, alert dismiss
## Trade-offs
- Strong at: At-a-glance status, quick navigation, density without clutter
- Weak at: On first use with 0 devices (needs empty state design)
## Best for
Administrators who check the dashboard daily to monitor inventory status.

View file

@ -0,0 +1,526 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #1a1a1a;
background: #f7f7f5;
line-height: 1.5;
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: 240px;
background: #ffffff;
border-right: 1px solid #e5e5e3;
padding: 20px 0;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar-logo {
padding: 0 20px 20px;
border-bottom: 1px solid #e5e5e3;
margin-bottom: 8px;
}
.sidebar-logo h1 {
font-size: 16px;
font-weight: 700;
color: #1a1a1a;
letter-spacing: -0.3px;
}
.sidebar-logo span {
font-size: 11px;
color: #999;
display: block;
margin-top: 2px;
}
.nav-section {
padding: 8px 12px;
}
.nav-label {
font-size: 11px;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 8px 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
color: #666;
cursor: pointer;
text-decoration: none;
transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: #f0f0ee; color: #1a1a1a; }
.nav-item.active { background: #f0f0ee; color: #1a1a1a; font-weight: 500; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-spacer { flex: 1; }
.nav-footer {
padding: 12px 20px;
border-top: 1px solid #e5e5e3;
font-size: 12px;
color: #999;
}
/* Main content */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
background: #ffffff;
border-bottom: 1px solid #e5e5e3;
padding: 16px 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
.topbar h2 {
font-size: 18px;
font-weight: 600;
}
.topbar-actions { display: flex; gap: 10px; }
.btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid #e5e5e3;
background: #fff;
color: #1a1a1a;
transition: background 0.15s;
font-family: inherit;
}
.btn:hover { background: #f0f0ee; }
.btn-primary {
background: #1a1a1a;
color: #fff;
border-color: #1a1a1a;
}
.btn-primary:hover { background: #333; }
.content {
padding: 32px;
overflow-y: auto;
flex: 1;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border: 1px solid #e5e5e3;
border-radius: 8px;
padding: 20px;
}
.stat-label {
font-size: 12px;
color: #999;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
margin-top: 4px;
letter-spacing: -1px;
}
.stat-change {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.stat-change .up { color: #16a34a; }
.stat-change .down { color: #dc2626; }
/* Two-column layout */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* Card */
.card {
background: #ffffff;
border: 1px solid #e5e5e3;
border-radius: 8px;
overflow: hidden;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid #e5e5e3;
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header h3 {
font-size: 14px;
font-weight: 600;
}
.card-body { padding: 0; }
.card-body.padded { padding: 20px; }
/* Table */
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
padding: 10px 16px;
font-size: 11px;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.3px;
border-bottom: 1px solid #e5e5e3;
background: #fafafa;
}
td {
padding: 12px 16px;
font-size: 13px;
border-bottom: 1px solid #f0f0ee;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafaf8; }
/* Badges */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-available { background: #e0e7ff; color: #3730a3; }
.badge-maintenance { background: #fef3c7; color: #92400e; }
.badge-decommissioned { background: #f3f4f6; color: #6b7280; }
.badge-computer { background: #f0f0ee; color: #555; }
.badge-monitor { background: #eff6ff; color: #1d4ed8; }
.badge-laptop { background: #fdf4ff; color: #9333ea; }
/* Chart placeholder */
.chart-placeholder {
height: 160px;
display: flex;
align-items: flex-end;
gap: 8px;
padding: 20px;
}
.chart-bar {
flex: 1;
background: #e5e5e3;
border-radius: 4px 4px 0 0;
position: relative;
transition: background 0.2s;
}
.chart-bar:hover { background: #1a1a1a; }
.chart-bar-label {
position: absolute;
bottom: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
color: #999;
white-space: nowrap;
}
/* Alert banner */
.alert {
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 14px 20px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
}
.alert-icon { font-size: 18px; }
.alert-text { flex: 1; color: #92400e; }
.alert-action {
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: #fbbf24;
color: #92400e;
border: none;
cursor: pointer;
font-family: inherit;
}
.alert-action:hover { background: #f59e0b; }
/* Quick actions */
.quick-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.quick-action {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px 12px;
border: 1px solid #e5e5e3;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
text-decoration: none;
color: inherit;
}
.quick-action:hover { border-color: #ccc; background: #fafaf8; }
.quick-action-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: #f0f0ee;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.quick-action-label {
font-size: 12px;
font-weight: 500;
color: #666;
text-align: center;
}
/* Focus styles for accessibility */
*:focus-visible {
outline: 2px solid #1a1a1a;
outline-offset: 2px;
}
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" role="navigation" aria-label="Menú principal">
<div class="sidebar-logo">
<h1>Inventschario</h1>
<span>Inventario Electrónico</span>
</div>
<div class="nav-section">
<div class="nav-label">Principal</div>
<a class="nav-item active" href="#dashboard" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item" href="#inventory">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
Inventario
</a>
<a class="nav-item" href="#add">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Agregar Dispositivo
</a>
</div>
<div class="nav-section">
<div class="nav-label">Herramientas</div>
<a class="nav-item" href="#import">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar / Exportar
</a>
<a class="nav-item" href="#backups">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Copias de Seguridad
</a>
<a class="nav-item" href="#settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Configuración
</a>
</div>
<div class="nav-spacer"></div>
<div class="nav-footer">
v1.0.0 — 2026-08-03
</div>
</nav>
<!-- Main Content -->
<main class="main" role="main">
<header class="topbar">
<h2>Dashboard</h2>
<div class="topbar-actions">
<button class="btn" aria-label="Crear backup manual">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 4px;"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/></svg>
Backup
</button>
<button class="btn btn-primary" aria-label="Agregar nuevo dispositivo">+ Nuevo Dispositivo</button>
</div>
</header>
<div class="content">
<!-- Alert: old backups -->
<div class="alert" role="alert">
<span class="alert-icon" aria-hidden="true">⚠️</span>
<span class="alert-text">Hay <strong>3 copias de seguridad</strong> con más de 30 días. ¿Desea eliminar las antiguas?</span>
<button class="alert-action" aria-label="Eliminar backups antiguos">Revisar</button>
</div>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Dispositivos</div>
<div class="stat-value">247</div>
<div class="stat-change"><span class="up">↑ 12</span> este mes</div>
</div>
<div class="stat-card">
<div class="stat-label">En Uso</div>
<div class="stat-value">198</div>
<div class="stat-change">80% del total</div>
</div>
<div class="stat-card">
<div class="stat-label">Disponibles</div>
<div class="stat-value">31</div>
<div class="stat-change"><span class="up">↑ 5</span> desde el mes pasado</div>
</div>
<div class="stat-card">
<div class="stat-label">En Mantenimiento</div>
<div class="stat-value">8</div>
<div class="stat-change"><span class="down">↑ 3</span> esta semana</div>
</div>
</div>
<!-- Two column layout -->
<div class="two-col">
<!-- Recent devices -->
<div class="card">
<div class="card-header">
<h3>Últimos Dispositivos Registrados</h3>
<a href="#inventory" style="font-size: 12px; color: #666; text-decoration: none;">Ver todos →</a>
</div>
<div class="card-body">
<table>
<thead>
<tr>
<th>Catálogo</th>
<th>Nombre</th>
<th>Tipo</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>INV-2024-0247</strong></td>
<td>MacBook Pro 14"</td>
<td><span class="badge badge-laptop">Portátil</span></td>
<td><span class="badge badge-active">Activo</span></td>
</tr>
<tr>
<td><strong>INV-2024-0246</strong></td>
<td>Dell P2723QE</td>
<td><span class="badge badge-monitor">Monitor</span></td>
<td><span class="badge badge-available">Disponible</span></td>
</tr>
<tr>
<td><strong>INV-2024-0245</strong></td>
<td>Lenovo ThinkPad T14s</td>
<td><span class="badge badge-laptop">Portátil</span></td>
<td><span class="badge badge-active">Activo</span></td>
</tr>
<tr>
<td><strong>INV-2024-0244</strong></td>
<td>HP LaserJet Pro M404</td>
<td><span class="badge badge-computer">Impresora</span></td>
<td><span class="badge badge-maintenance">Mantenimiento</span></td>
</tr>
<tr>
<td><strong>INV-2024-0243</strong></td>
<td>Cisco Catalyst 2960</td>
<td><span class="badge badge-computer">Red</span></td>
<td><span class="badge badge-active">Activo</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Distribution by type -->
<div class="card">
<div class="card-header">
<h3>Distribución por Tipo</h3>
</div>
<div class="card-body padded">
<div class="chart-placeholder" style="padding-bottom: 36px;">
<div class="chart-bar" style="height: 85%;">
<span class="chart-bar-label">Portátiles</span>
</div>
<div class="chart-bar" style="height: 60%;">
<span class="chart-bar-label">Monitores</span>
</div>
<div class="chart-bar" style="height: 45%;">
<span class="chart-bar-label">Sobremesa</span>
</div>
<div class="chart-bar" style="height: 25%;">
<span class="chart-bar-label">Impresoras</span>
</div>
<div class="chart-bar" style="height: 20%;">
<span class="chart-bar-label">Red</span>
</div>
<div class="chart-bar" style="height: 15%;">
<span class="chart-bar-label">Perif.</span>
</div>
<div class="chart-bar" style="height: 10%;">
<span class="chart-bar-label">Otros</span>
</div>
</div>
</div>
</div>
</div>
<!-- Quick actions -->
<div style="margin-top: 32px;">
<h3 style="font-size: 14px; font-weight: 600; margin-bottom: 16px;">Acciones Rápidas</h3>
<div class="quick-actions">
<a class="quick-action" href="#add">
<div class="quick-action-icon" aria-hidden="true"></div>
<div class="quick-action-label">Agregar<br>Dispositivo</div>
</a>
<a class="quick-action" href="#import">
<div class="quick-action-icon" aria-hidden="true"></div>
<div class="quick-action-label">Importar<br>desde CSV</div>
</a>
<a class="quick-action" href="#export">
<div class="quick-action-icon" aria-hidden="true"></div>
<div class="quick-action-label">Exportar<br>a CSV</div>
</a>
</div>
</div>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,18 @@
# Variant: Inventario — Operate Surface
## Design stance
Action-oriented data table: the user is working with devices, not just viewing them.
Every row has inline actions, the toolbar has filters, and bulk selection is available.
## Key choices
- **Layout:** Full-width table with persistent toolbar, checkbox selection, pagination
- **Typography:** Monospace for catalog numbers, Inter for everything else
- **Color:** Status badges with semantic colors, row hover highlight
- **Interaction:** Sortable columns, filter dropdowns, row-level actions (view, edit, delete)
## Trade-offs
- Strong at: Scanning large lists, quick actions per device, bulk operations
- Weak at: Needs mobile responsive design for tablet use
## Best for
Daily inventory management — searching, filtering, and acting on specific devices.

View file

@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Inventario</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #1a1a1a;
background: #f7f7f5;
line-height: 1.5;
display: flex;
min-height: 100vh;
}
/* Sidebar — same as dashboard */
.sidebar {
width: 240px;
background: #ffffff;
border-right: 1px solid #e5e5e3;
padding: 20px 0;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar-logo { padding: 0 20px 20px; border-bottom: 1px solid #e5e5e3; margin-bottom: 8px; }
.sidebar-logo h1 { font-size: 16px; font-weight: 700; }
.sidebar-logo span { font-size: 11px; color: #999; display: block; margin-top: 2px; }
.nav-section { padding: 8px 12px; }
.nav-label { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 8px 4px; }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 6px; font-size: 14px; color: #666; cursor: pointer;
text-decoration: none; transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: #f0f0ee; color: #1a1a1a; }
.nav-item.active { background: #f0f0ee; color: #1a1a1a; font-weight: 500; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-spacer { flex: 1; }
.nav-footer { padding: 12px 20px; border-top: 1px solid #e5e5e3; font-size: 12px; color: #999; }
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
background: #ffffff; border-bottom: 1px solid #e5e5e3;
padding: 16px 32px; display: flex; align-items: center; justify-content: space-between;
}
.topbar h2 { font-size: 18px; font-weight: 600; }
.topbar-actions { display: flex; gap: 10px; }
.btn {
padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid #e5e5e3; background: #fff; color: #1a1a1a;
transition: background 0.15s; font-family: inherit;
}
.btn:hover { background: #f0f0ee; }
.btn-primary { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
.btn-primary:hover { background: #333; }
.btn-danger { background: #fff; color: #dc2626; border-color: #fca5a5; }
.btn-danger:hover { background: #fef2f2; }
.content { padding: 32px; overflow-y: auto; flex: 1; }
/* Toolbar */
.toolbar {
display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
}
.search-box {
flex: 1; min-width: 240px; position: relative;
}
.search-box input {
width: 100%; padding: 8px 12px 8px 36px; border: 1px solid #e5e5e3;
border-radius: 6px; font-size: 13px; font-family: inherit; background: #fff;
}
.search-box input:focus { outline: 2px solid #1a1a1a; outline-offset: -1px; }
.search-box svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
width: 16px; height: 16px; color: #999;
}
.filter-group { display: flex; gap: 8px; }
.filter-select {
padding: 8px 12px; border: 1px solid #e5e5e3; border-radius: 6px;
font-size: 13px; font-family: inherit; background: #fff; color: #1a1a1a;
cursor: pointer;
}
.filter-select:focus { outline: 2px solid #1a1a1a; outline-offset: -1px; }
/* Selection bar */
.selection-bar {
background: #1a1a1a; color: #fff; padding: 10px 20px; border-radius: 8px;
margin-bottom: 16px; display: flex; align-items: center; gap: 16px;
font-size: 13px;
}
.selection-bar .btn {
background: transparent; color: #fff; border-color: rgba(255,255,255,0.2);
padding: 4px 12px; font-size: 12px;
}
.selection-bar .btn:hover { background: rgba(255,255,255,0.1); }
/* Table */
.table-card {
background: #ffffff; border: 1px solid #e5e5e3; border-radius: 8px; overflow: hidden;
}
table { width: 100%; border-collapse: collapse; }
th {
text-align: left; padding: 10px 16px; font-size: 11px; font-weight: 600;
color: #999; text-transform: uppercase; letter-spacing: 0.3px;
border-bottom: 1px solid #e5e5e3; background: #fafafa; cursor: pointer;
user-select: none; white-space: nowrap;
}
th:hover { color: #1a1a1a; }
th .sort-icon { margin-left: 4px; opacity: 0.4; }
th.sorted .sort-icon { opacity: 1; color: #1a1a1a; }
td {
padding: 12px 16px; font-size: 13px; border-bottom: 1px solid #f0f0ee;
vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafaf8; }
tr.selected td { background: #f0f0ee; }
/* Checkbox */
.checkbox { width: 16px; height: 16px; cursor: pointer; accent-color: #1a1a1a; }
/* Badges */
.badge {
display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 11px; font-weight: 500;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-available { background: #e0e7ff; color: #3730a3; }
.badge-maintenance { background: #fef3c7; color: #92400e; }
.badge-decommissioned { background: #f3f4f6; color: #6b7280; }
.badge-disposed { background: #fee2e2; color: #991b1b; }
.badge-computer { background: #f0f0ee; color: #555; }
.badge-monitor { background: #eff6ff; color: #1d4ed8; }
.badge-laptop { background: #fdf4ff; color: #9333ea; }
.badge-printer { background: #fef3c7; color: #92400e; }
.badge-network { background: #ecfdf5; color: #065f46; }
/* Row actions */
.row-actions { display: flex; gap: 4px; }
.row-action {
width: 28px; height: 28px; border-radius: 4px; border: none;
background: transparent; cursor: pointer; display: flex;
align-items: center; justify-content: center; color: #999;
transition: background 0.15s, color 0.15s;
}
.row-action:hover { background: #f0f0ee; color: #1a1a1a; }
.row-action svg { width: 14px; height: 14px; }
/* Pagination */
.pagination {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; border-top: 1px solid #e5e5e3; font-size: 13px; color: #666;
}
.pagination-info { font-size: 12px; }
.pagination-controls { display: flex; gap: 4px; }
.page-btn {
padding: 4px 10px; border-radius: 4px; border: 1px solid #e5e5e3;
background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;
}
.page-btn:hover { background: #f0f0ee; }
.page-btn.active { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
/* Focus styles */
*:focus-visible { outline: 2px solid #1a1a1a; outline-offset: 2px; }
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" role="navigation" aria-label="Menú principal">
<div class="sidebar-logo">
<h1>Inventschario</h1>
<span>Inventario Electrónico</span>
</div>
<div class="nav-section">
<div class="nav-label">Principal</div>
<a class="nav-item" href="#dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item active" href="#inventory" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
Inventario
</a>
<a class="nav-item" href="#add">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Agregar Dispositivo
</a>
</div>
<div class="nav-section">
<div class="nav-label">Herramientas</div>
<a class="nav-item" href="#import">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar / Exportar
</a>
<a class="nav-item" href="#backups">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Copias de Seguridad
</a>
<a class="nav-item" href="#settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Configuración
</a>
</div>
<div class="nav-spacer"></div>
<div class="nav-footer">v1.0.0 — 2026-08-03</div>
</nav>
<!-- Main Content -->
<main class="main" role="main">
<header class="topbar">
<h2>Inventario <span style="font-weight: 400; color: #999; font-size: 14px;">(247 dispositivos)</span></h2>
<div class="topbar-actions">
<button class="btn" aria-label="Exportar selección a CSV">⬇ Exportar</button>
<button class="btn btn-primary" aria-label="Agregar nuevo dispositivo">+ Nuevo</button>
</div>
</header>
<div class="content">
<!-- Toolbar -->
<div class="toolbar" role="search">
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="search" placeholder="Buscar por nombre, número de serie, catálogo..." aria-label="Buscar dispositivos">
</div>
<div class="filter-group">
<select class="filter-select" aria-label="Filtrar por tipo">
<option value="">Todos los tipos</option>
<option>Portátil</option>
<option>Sobremesa</option>
<option>Monitor</option>
<option>Impresora</option>
<option>Red</option>
<option>Periférico</option>
</select>
<select class="filter-select" aria-label="Filtrar por estado">
<option value="">Todos los estados</option>
<option>Activo</option>
<option>Disponible</option>
<option>Mantenimiento</option>
<option>Baja</option>
</select>
<select class="filter-select" aria-label="Filtrar por departamento">
<option value="">Todos los departamentos</option>
<option>Informática</option>
<option>Administración</option>
<option>Recursos Humanos</option>
<option>Contabilidad</option>
</select>
</div>
</div>
<!-- Selection bar (hidden by default, shown when items selected) -->
<div class="selection-bar" role="status" aria-live="polite" style="display: none;">
<span>3 seleccionados</span>
<button class="btn">Exportar selección</button>
<button class="btn">Cambiar estado</button>
<button class="btn btn-danger">Eliminar</button>
<button class="btn" style="margin-left: auto;">Cancelar selección</button>
</div>
<!-- Table -->
<div class="table-card">
<table role="grid" aria-label="Lista de dispositivos">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" class="checkbox" aria-label="Seleccionar todos"></th>
<th class="sorted">Catálogo <span class="sort-icon"></span></th>
<th>Nombre / Descripción</th>
<th>Tipo</th>
<th>Marca / Modelo</th>
<th>Ubicación</th>
<th>Estado</th>
<th style="width: 100px;">Acciones</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0247"></td>
<td><strong>INV-2024-0247</strong></td>
<td>MacBook Pro 14" — Oficina Dirección</td>
<td><span class="badge badge-laptop">Portátil</span></td>
<td>Apple / MacBook Pro M3</td>
<td>Planta 2, Desp. 201</td>
<td><span class="badge badge-active">Activo</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle" aria-label="Ver detalle de INV-2024-0247">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button class="row-action" title="Editar" aria-label="Editar INV-2024-0247">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="row-action" title="Dar de baja" aria-label="Dar de baja INV-2024-0247" style="color: #dc2626;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0246"></td>
<td><strong>INV-2024-0246</strong></td>
<td>Dell P2723QE — Monitor 4K</td>
<td><span class="badge badge-monitor">Monitor</span></td>
<td>Dell / P2723QE</td>
<td>Almacén</td>
<td><span class="badge badge-available">Disponible</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="row-action" title="Editar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="row-action" title="Dar de baja" style="color: #dc2626;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0245"></td>
<td><strong>INV-2024-0245</strong></td>
<td>Lenovo ThinkPad T14s — Desarrollo</td>
<td><span class="badge badge-laptop">Portátil</span></td>
<td>Lenovo / ThinkPad T14s Gen 4</td>
<td>Planta 1, Open Space</td>
<td><span class="badge badge-active">Activo</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="row-action" title="Editar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="row-action" title="Dar de baja" style="color: #dc2626;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0244"></td>
<td><strong>INV-2024-0244</strong></td>
<td>HP LaserJet Pro M404dn</td>
<td><span class="badge badge-printer">Impresora</span></td>
<td>HP / LaserJet Pro M404dn</td>
<td>Planta 1, Reprografía</td>
<td><span class="badge badge-maintenance">Mantenimiento</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="row-action" title="Editar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="row-action" title="Dar de baja" style="color: #dc2626;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0243"></td>
<td><strong>INV-2024-0243</strong></td>
<td>Cisco Catalyst 2960 — Switch Planta 1</td>
<td><span class="badge badge-network">Red</span></td>
<td>Cisco / Catalyst 2960-24TC-L</td>
<td>Planta 1, Cuarto Redes</td>
<td><span class="badge badge-active">Activo</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="row-action" title="Editar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="row-action" title="Dar de baja" style="color: #dc2626;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>
</div>
</td>
</tr>
<tr>
<td><input type="checkbox" class="checkbox" aria-label="Seleccionar INV-2024-0242"></td>
<td><strong>INV-2024-0242</strong></td>
<td>Dell OptiPlex 7090 — Recepción</td>
<td><span class="badge badge-computer">Sobremesa</span></td>
<td>Dell / OptiPlex 7090</td>
<td>Planta 0, Recepción</td>
<td><span class="badge badge-active">Activo</span></td>
<td>
<div class="row-actions">
<button class="row-action" title="Ver detalle"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
<button class="row-action" title="Editar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="row-action" title="Dar de baja" style="color: #dc2626;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Pagination -->
<div class="pagination">
<div class="pagination-info">Mostrando 1-20 de 247 dispositivos</div>
<div class="pagination-controls">
<button class="page-btn" aria-label="Página anterior"></button>
<button class="page-btn active" aria-current="page">1</button>
<button class="page-btn">2</button>
<button class="page-btn">3</button>
<button class="page-btn"></button>
<button class="page-btn">13</button>
<button class="page-btn" aria-label="Página siguiente"></button>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,19 @@
# Variant: Formulario — Configure Surface
## Design stance
Progressive disclosure through logical sections: identification, device info,
purchase, assignment, notes. Required fields marked clearly, inline validation.
## Key choices
- **Layout:** Single-column form card, 2-column grid within sections
- **Typography:** Clear section headers, field labels with required markers
- **Color:** Minimal — white card on light gray background, red for required/errors
- **Interaction:** Real-time validation, "Save and Add Another" for batch entry
## Trade-offs
- Strong at: Clear data entry flow, reduced errors, batch-friendly
- Weak at: Long form on small screens (needs responsive stacking)
## Best for
Staff entering new devices — the form guides them through required fields
and offers shortcuts for batch entry.

View file

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Agregar Dispositivo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #1a1a1a; background: #f7f7f5; line-height: 1.5;
display: flex; min-height: 100vh;
}
.sidebar {
width: 240px; background: #fff; border-right: 1px solid #e5e5e3;
padding: 20px 0; flex-shrink: 0; display: flex; flex-direction: column;
}
.sidebar-logo { padding: 0 20px 20px; border-bottom: 1px solid #e5e5e3; margin-bottom: 8px; }
.sidebar-logo h1 { font-size: 16px; font-weight: 700; }
.sidebar-logo span { font-size: 11px; color: #999; display: block; margin-top: 2px; }
.nav-section { padding: 8px 12px; }
.nav-label { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 8px 4px; }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 6px; font-size: 14px; color: #666; cursor: pointer;
text-decoration: none; transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: #f0f0ee; color: #1a1a1a; }
.nav-item.active { background: #f0f0ee; color: #1a1a1a; font-weight: 500; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-spacer { flex: 1; }
.nav-footer { padding: 12px 20px; border-top: 1px solid #e5e5e3; font-size: 12px; color: #999; }
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
background: #fff; border-bottom: 1px solid #e5e5e3;
padding: 16px 32px; display: flex; align-items: center; justify-content: space-between;
}
.topbar h2 { font-size: 18px; font-weight: 600; }
.topbar-actions { display: flex; gap: 10px; }
.btn {
padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid #e5e5e3; background: #fff; color: #1a1a1a;
transition: background 0.15s; font-family: inherit;
}
.btn:hover { background: #f0f0ee; }
.btn-primary { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
.btn-primary:hover { background: #333; }
.btn-ghost { border: none; background: transparent; color: #666; }
.btn-ghost:hover { color: #1a1a1a; background: #f0f0ee; }
.content { padding: 32px; overflow-y: auto; flex: 1; max-width: 800px; }
/* Form */
.form-card {
background: #fff; border: 1px solid #e5e5e3; border-radius: 8px;
overflow: hidden;
}
.form-header {
padding: 20px 24px; border-bottom: 1px solid #e5e5e3;
}
.form-header h3 { font-size: 15px; font-weight: 600; }
.form-header p { font-size: 13px; color: #666; margin-top: 4px; }
.form-body { padding: 24px; }
.form-section {
margin-bottom: 28px;
}
.form-section:last-child { margin-bottom: 0; }
.form-section-title {
font-size: 12px; font-weight: 600; color: #999; text-transform: uppercase;
letter-spacing: 0.5px; margin-bottom: 16px; padding-bottom: 8px;
border-bottom: 1px solid #f0f0ee;
}
.form-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
.form-grid .full-width { grid-column: 1 / -1; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-label {
font-size: 13px; font-weight: 500; color: #1a1a1a;
}
.form-label .required { color: #dc2626; margin-left: 2px; }
.form-hint { font-size: 11px; color: #999; }
.form-input, .form-select, .form-textarea {
padding: 8px 12px; border: 1px solid #e5e5e3; border-radius: 6px;
font-size: 13px; font-family: inherit; background: #fff; color: #1a1a1a;
transition: border-color 0.15s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a;
}
.form-input.error { border-color: #dc2626; }
.form-error { font-size: 11px; color: #dc2626; margin-top: 2px; }
.form-textarea { resize: vertical; min-height: 80px; }
.form-select { cursor: pointer; }
/* Required fields note */
.required-note {
font-size: 12px; color: #999; margin-bottom: 20px;
}
.required-note .required { color: #dc2626; }
/* Form footer */
.form-footer {
padding: 16px 24px; border-top: 1px solid #e5e5e3;
display: flex; justify-content: space-between; align-items: center;
background: #fafafa;
}
.form-footer-info { font-size: 12px; color: #999; }
.form-footer-actions { display: flex; gap: 8px; }
/* Success toast */
.toast {
position: fixed; bottom: 24px; right: 24px; background: #166534;
color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 13px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center;
gap: 8px; z-index: 100;
}
.toast-icon { font-size: 16px; }
*:focus-visible { outline: 2px solid #1a1a1a; outline-offset: 2px; }
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" role="navigation" aria-label="Menú principal">
<div class="sidebar-logo"><h1>Inventschario</h1><span>Inventario Electrónico</span></div>
<div class="nav-section">
<div class="nav-label">Principal</div>
<a class="nav-item" href="#dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item" href="#inventory">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
Inventario
</a>
<a class="nav-item active" href="#add" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Agregar Dispositivo
</a>
</div>
<div class="nav-section">
<div class="nav-label">Herramientas</div>
<a class="nav-item" href="#import">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar / Exportar
</a>
<a class="nav-item" href="#backups">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Copias de Seguridad
</a>
<a class="nav-item" href="#settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Configuración
</a>
</div>
<div class="nav-spacer"></div>
<div class="nav-footer">v1.0.0 — 2026-08-03</div>
</nav>
<!-- Main Content -->
<main class="main" role="main">
<header class="topbar">
<div style="display: flex; align-items: center; gap: 12px;">
<a href="#inventory" class="btn-ghost" style="padding: 4px; border: none; background: none; cursor: pointer; color: #666; text-decoration: none;" aria-label="Volver al inventario">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</a>
<h2>Agregar Dispositivo</h2>
</div>
<div class="topbar-actions">
<button class="btn">Cancelar</button>
<button class="btn btn-primary">Guardar Dispositivo</button>
</div>
</header>
<div class="content">
<p class="required-note">Los campos marcados con <span class="required">*</span> son obligatorios.</p>
<form class="form-card" aria-label="Formulario de nuevo dispositivo">
<!-- Identification -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Identificación</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="catalog_number">Número de Catálogo Interno <span class="required">*</span></label>
<input class="form-input" type="text" id="catalog_number" placeholder="INV-2024-0248" required aria-required="true">
<span class="form-hint">Se genera automáticamente si se deja vacío</span>
</div>
<div class="form-group">
<label class="form-label" for="serial_number">Número de Serie <span class="required">*</span></label>
<input class="form-input" type="text" id="serial_number" placeholder="C02X1234JHD5" required aria-required="true">
</div>
<div class="form-group">
<label class="form-label" for="service_tag">Service Tag</label>
<input class="form-input" type="text" id="service_tag" placeholder="ABC1234">
<span class="form-hint">Número de servicio del fabricante</span>
</div>
<div class="form-group">
<label class="form-label" for="additional_reg">Registro Adicional</label>
<input class="form-input" type="text" id="additional_reg" placeholder="Cualquier otro número de registro">
</div>
</div>
</div>
<!-- Device info -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Información del Dispositivo</div>
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label" for="device_name">Nombre / Descripción <span class="required">*</span></label>
<input class="form-input" type="text" id="device_name" placeholder="MacBook Pro 14" Oficina Dirección" required aria-required="true">
</div>
<div class="form-group">
<label class="form-label" for="device_type">Tipo de Dispositivo <span class="required">*</span></label>
<select class="form-select" id="device_type" required aria-required="true">
<option value="">Seleccionar tipo...</option>
<option>Ordenador de sobremesa</option>
<option selected>Portátil</option>
<option>Monitor / Pantalla</option>
<option>Impresora</option>
<option>Escáner</option>
<option>Dispositivo de red</option>
<option>Periférico</option>
<option>Servidor</option>
<option>SAI / Regulador</option>
<option>Almacenamiento externo</option>
<option>Otro</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="brand">Marca</label>
<input class="form-input" type="text" id="brand" placeholder="Apple">
</div>
<div class="form-group">
<label class="form-label" for="model">Modelo</label>
<input class="form-input" type="text" id="model" placeholder="MacBook Pro M3">
</div>
<div class="form-group">
<label class="form-label" for="serial_internal">N.º Serie Interno</label>
<input class="form-input" type="text" id="serial_internal" placeholder="Número interno de control">
</div>
<div class="form-group">
<label class="form-label" for="condition">Estado Físico</label>
<select class="form-select" id="condition">
<option value="excellent">Excelente</option>
<option value="good" selected>Bueno</option>
<option value="fair">Regular</option>
<option value="poor">Malo</option>
<option value="damaged">Dañado</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="status">Estado</label>
<select class="form-select" id="status">
<option value="active" selected>Activo</option>
<option value="available">Disponible</option>
<option value="reserved">Reservado</option>
</select>
</div>
</div>
</div>
<!-- Purchase info -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Compra y Garantía</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="purchase_date">Fecha de Compra</label>
<input class="form-input" type="date" id="purchase_date">
</div>
<div class="form-group">
<label class="form-label" for="warranty_expiry">Fin de Garantía</label>
<input class="form-input" type="date" id="warranty_expiry">
</div>
<div class="form-group">
<label class="form-label" for="purchase_price">Precio de Compra (€)</label>
<input class="form-input" type="number" id="purchase_price" placeholder="0.00" step="0.01" min="0">
</div>
</div>
</div>
<!-- Assignment -->
<div class="form-section" style="padding: 24px; padding-bottom: 0;">
<div class="form-section-title">Asignación</div>
<div class="form-grid">
<div class="form-group">
<label class="form-label" for="location">Ubicación Asignada</label>
<select class="form-select" id="location">
<option value="">Seleccionar ubicación...</option>
<option>Planta 0 — Recepción</option>
<option>Planta 1 — Open Space</option>
<option>Planta 1 — Reprografía</option>
<option>Planta 1 — Cuarto Redes</option>
<option>Planta 2 — Desp. Dirección</option>
<option>Almacén</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="assigned_user">Usuario Asignado</label>
<input class="form-input" type="text" id="assigned_user" placeholder="Nombre del usuario">
</div>
<div class="form-group">
<label class="form-label" for="department">Departamento</label>
<select class="form-select" id="department">
<option value="">Seleccionar departamento...</option>
<option>Informática</option>
<option>Administración</option>
<option>Recursos Humanos</option>
<option>Contabilidad</option>
<option>Dirección</option>
</select>
</div>
</div>
</div>
<!-- Notes -->
<div class="form-section" style="padding: 24px;">
<div class="form-section-title">Notas</div>
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label" for="notes">Observaciones</label>
<textarea class="form-textarea" id="notes" placeholder="Notas adicionales sobre el dispositivo..." rows="3"></textarea>
</div>
</div>
</div>
<!-- Footer -->
<div class="form-footer">
<span class="form-footer-info">Los cambios se guardan localmente</span>
<div class="form-footer-actions">
<button type="button" class="btn">Cancelar</button>
<button type="button" class="btn" style="border-color: #e5e5e3;">Guardar y Agregar Otro</button>
<button type="submit" class="btn btn-primary">Guardar Dispositivo</button>
</div>
</div>
</form>
</div>
</main>
<!-- Toast demo -->
<div class="toast" role="alert" aria-live="polite">
<span class="toast-icon" aria-hidden="true"></span>
Dispositivo INV-2024-0247 guardado correctamente
</div>
</body>
</html>

View file

@ -0,0 +1,20 @@
# Variant: Configuración — Configure Surface
## Design stance
Tabbed settings with inline editing: each section is self-contained, changes
are saved explicitly, and the field label editor shows the internal key alongside
the user-facing label.
## Key choices
- **Layout:** Tabbed navigation, settings sections as cards, inline list editors
- **Typography:** Monospace for field keys, Inter for labels and values
- **Color:** Minimal — toggle switches for boolean settings, danger color for delete
- **Interaction:** Toggle switches, inline add/remove for locations/departments
## Trade-offs
- Strong at: Organized settings, clear field mapping, backup management
- Weak at: Many settings could overwhelm on first visit
## Best for
Initial setup and ongoing configuration — the admin sets up the system once
and revisits occasionally for adjustments.

View file

@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventschario — Configuración</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: #1a1a1a; background: #f7f7f5; line-height: 1.5;
display: flex; min-height: 100vh;
}
.sidebar {
width: 240px; background: #fff; border-right: 1px solid #e5e5e3;
padding: 20px 0; flex-shrink: 0; display: flex; flex-direction: column;
}
.sidebar-logo { padding: 0 20px 20px; border-bottom: 1px solid #e5e5e3; margin-bottom: 8px; }
.sidebar-logo h1 { font-size: 16px; font-weight: 700; }
.sidebar-logo span { font-size: 11px; color: #999; display: block; margin-top: 2px; }
.nav-section { padding: 8px 12px; }
.nav-label { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 8px 4px; }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 6px; font-size: 14px; color: #666; cursor: pointer;
text-decoration: none; transition: background 0.15s, color 0.15s;
}
.nav-item:hover { background: #f0f0ee; color: #1a1a1a; }
.nav-item.active { background: #f0f0ee; color: #1a1a1a; font-weight: 500; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-spacer { flex: 1; }
.nav-footer { padding: 12px 20px; border-top: 1px solid #e5e5e3; font-size: 12px; color: #999; }
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.topbar {
background: #fff; border-bottom: 1px solid #e5e5e3;
padding: 16px 32px; display: flex; align-items: center; justify-content: space-between;
}
.topbar h2 { font-size: 18px; font-weight: 600; }
.btn {
padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid #e5e5e3; background: #fff; color: #1a1a1a;
transition: background 0.15s; font-family: inherit;
}
.btn:hover { background: #f0f0ee; }
.btn-primary { background: #1a1a1a; color: #fff; border-color: #1a1a1a; }
.btn-primary:hover { background: #333; }
.btn-danger { color: #dc2626; border-color: #fca5a5; }
.btn-danger:hover { background: #fef2f2; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.content { padding: 32px; overflow-y: auto; flex: 1; max-width: 800px; }
/* Settings sections */
.settings-section {
background: #fff; border: 1px solid #e5e5e3; border-radius: 8px;
margin-bottom: 24px; overflow: hidden;
}
.settings-header {
padding: 16px 20px; border-bottom: 1px solid #e5e5e3;
display: flex; align-items: center; justify-content: space-between;
}
.settings-header h3 { font-size: 14px; font-weight: 600; }
.settings-header p { font-size: 12px; color: #999; }
.settings-body { padding: 20px; }
.setting-row {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 0; border-bottom: 1px solid #f0f0ee;
}
.setting-row:last-child { border-bottom: none; }
.setting-info { flex: 1; }
.setting-label { font-size: 13px; font-weight: 500; }
.setting-desc { font-size: 12px; color: #999; margin-top: 2px; }
.setting-control { margin-left: 20px; }
.form-input, .form-select {
padding: 6px 10px; border: 1px solid #e5e5e3; border-radius: 4px;
font-size: 13px; font-family: inherit; background: #fff;
}
.form-input:focus, .form-select:focus {
outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a;
}
/* Toggle switch */
.toggle {
width: 40px; height: 22px; background: #e5e5e3; border-radius: 11px;
position: relative; cursor: pointer; transition: background 0.2s;
border: none; padding: 0;
}
.toggle.active { background: #166534; }
.toggle::after {
content: ''; width: 18px; height: 18px; background: #fff;
border-radius: 50%; position: absolute; top: 2px; left: 2px;
transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.toggle.active::after { transform: translateX(18px); }
/* Field labels editor */
.label-editor { margin-top: 8px; }
.label-row {
display: flex; align-items: center; gap: 12px; padding: 8px 0;
border-bottom: 1px solid #f0f0ee;
}
.label-row:last-child { border-bottom: none; }
.label-key {
font-size: 12px; color: #999; font-family: monospace;
min-width: 160px;
}
.label-input {
flex: 1; padding: 6px 10px; border: 1px solid #e5e5e3; border-radius: 4px;
font-size: 13px; font-family: inherit;
}
.label-input:focus { outline: none; border-color: #1a1a1a; box-shadow: 0 0 0 1px #1a1a1a; }
/* Backup list */
.backup-list { margin-top: 12px; }
.backup-item {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 12px; border: 1px solid #f0f0ee; border-radius: 6px;
margin-bottom: 8px; font-size: 13px;
}
.backup-info { display: flex; align-items: center; gap: 12px; }
.backup-icon { font-size: 16px; }
.backup-name { font-weight: 500; }
.backup-meta { font-size: 11px; color: #999; }
.backup-actions { display: flex; gap: 4px; }
/* Tabs */
.tabs {
display: flex; gap: 0; border-bottom: 1px solid #e5e5e3; margin-bottom: 20px;
}
.tab {
padding: 10px 16px; font-size: 13px; font-weight: 500; color: #666;
cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s;
background: none; border-top: none; border-left: none; border-right: none;
font-family: inherit;
}
.tab:hover { color: #1a1a1a; }
.tab.active { color: #1a1a1a; border-bottom-color: #1a1a1a; }
*:focus-visible { outline: 2px solid #1a1a1a; outline-offset: 2px; }
</style>
</head>
<body>
<!-- Sidebar -->
<nav class="sidebar" role="navigation" aria-label="Menú principal">
<div class="sidebar-logo"><h1>Inventschario</h1><span>Inventario Electrónico</span></div>
<div class="nav-section">
<div class="nav-label">Principal</div>
<a class="nav-item" href="#dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
Dashboard
</a>
<a class="nav-item" href="#inventory">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
Inventario
</a>
<a class="nav-item" href="#add">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
Agregar Dispositivo
</a>
</div>
<div class="nav-section">
<div class="nav-label">Herramientas</div>
<a class="nav-item" href="#import">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
Importar / Exportar
</a>
<a class="nav-item" href="#backups">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Copias de Seguridad
</a>
<a class="nav-item active" href="#settings" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Configuración
</a>
</div>
<div class="nav-spacer"></div>
<div class="nav-footer">v1.0.0 — 2026-08-03</div>
</nav>
<!-- Main Content -->
<main class="main" role="main">
<header class="topbar">
<h2>Configuración</h2>
<div class="topbar-actions">
<button class="btn btn-primary">Guardar Cambios</button>
</div>
</header>
<div class="content">
<!-- Tabs -->
<div class="tabs" role="tablist">
<button class="tab active" role="tab" aria-selected="true">General</button>
<button class="tab" role="tab">Campos</button>
<button class="tab" role="tab">Copias de Seguridad</button>
<button class="tab" role="tab">Importar / Exportar</button>
</div>
<!-- General Settings -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Información General</h3>
<p>Datos de la institución y configuración básica</p>
</div>
</div>
<div class="settings-body">
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Nombre de la Institución</div>
<div class="setting-desc">Aparece en el encabezado de exportaciones e informes</div>
</div>
<div class="setting-control">
<input class="form-input" type="text" value="Ayuntamiento de Ejemplo" style="width: 280px;">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Prefijo de Número de Catálogo</div>
<div class="setting-desc">Prefijo para la generación automática de números de inventario</div>
</div>
<div class="setting-control">
<input class="form-input" type="text" value="INV-" style="width: 120px;">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Separador CSV por Defecto</div>
<div class="setting-desc">Separador utilizado al exportar e importar archivos CSV</div>
</div>
<div class="setting-control">
<select class="form-select">
<option value=";">Punto y coma (;)</option>
<option value=",">Coma (,)</option>
<option value="\t">Tabulador</option>
</select>
</div>
</div>
</div>
</div>
<!-- Field Labels -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Etiquetas de Campos</h3>
<p>Personaliza los nombres que aparecen en la interfaz. El nombre interno en la base de datos no cambia.</p>
</div>
</div>
<div class="settings-body">
<div class="label-editor">
<div class="label-row">
<span class="label-key">serial_number</span>
<input class="label-input" type="text" value="Número de Serie">
</div>
<div class="label-row">
<span class="label-key">service_tag</span>
<input class="label-input" type="text" value="Service Tag">
</div>
<div class="label-row">
<span class="label-key">catalog_number</span>
<input class="label-input" type="text" value="Número de Catálogo Interno">
</div>
<div class="label-row">
<span class="label-key">additional_registry</span>
<input class="label-input" type="text" value="Registro Adicional">
</div>
<div class="label-row">
<span class="label-key">device_name</span>
<input class="label-input" type="text" value="Nombre / Descripción">
</div>
<div class="label-row">
<span class="label-key">device_type</span>
<input class="label-input" type="text" value="Tipo de Dispositivo">
</div>
<div class="label-row">
<span class="label-key">brand</span>
<input class="label-input" type="text" value="Marca">
</div>
<div class="label-row">
<span class="label-key">model</span>
<input class="label-input" type="text" value="Modelo">
</div>
<div class="label-row">
<span class="label-key">purchase_date</span>
<input class="label-input" type="text" value="Fecha de Compra">
</div>
<div class="label-row">
<span class="label-key">warranty_expiry</span>
<input class="label-input" type="text" value="Fin de Garantía">
</div>
<div class="label-row">
<span class="label-key">purchase_price</span>
<input class="label-input" type="text" value="Precio de Compra (€)">
</div>
<div class="label-row">
<span class="label-key">assigned_location</span>
<input class="label-input" type="text" value="Ubicación Asignada">
</div>
<div class="label-row">
<span class="label-key">assigned_user</span>
<input class="label-input" type="text" value="Usuario Asignado">
</div>
<div class="label-row">
<span class="label-key">department</span>
<input class="label-input" type="text" value="Departamento">
</div>
<div class="label-row">
<span class="label-key">status</span>
<input class="label-input" type="text" value="Estado">
</div>
<div class="label-row">
<span class="label-key">condition</span>
<input class="label-input" type="text" value="Estado Físico">
</div>
</div>
</div>
</div>
<!-- Backup Settings -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Copias de Seguridad</h3>
<p>Configuración del backup automático y gestión de copias</p>
</div>
</div>
<div class="settings-body">
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Backup Automático</div>
<div class="setting-desc">Crear una copia de seguridad de la base de datos cada día</div>
</div>
<div class="setting-control">
<button class="toggle active" role="switch" aria-checked="true" aria-label="Activar backup automático"></button>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Hora del Backup</div>
<div class="setting-desc">Hora a la que se ejecuta el backup diario</div>
</div>
<div class="setting-control">
<input class="form-input" type="time" value="02:00">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Días de Retención</div>
<div class="setting-desc">Número de días antes de sugerir la eliminación de backups antiguos</div>
</div>
<div class="setting-control">
<input class="form-input" type="number" value="30" min="7" max="365" style="width: 80px;">
<span style="font-size: 12px; color: #999; margin-left: 4px;">días</span>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Máximo de Backups</div>
<div class="setting-desc">Número máximo de copias de seguridad a conservar</div>
</div>
<div class="setting-control">
<input class="form-input" type="number" value="90" min="10" max="365" style="width: 80px;">
</div>
</div>
<!-- Recent backups -->
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #e5e5e3;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Copias Recientes</span>
<button class="btn btn-sm">Crear Backup Manual</button>
</div>
<div class="backup-list">
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260803_020000.db.gz</div>
<div class="backup-meta">3 de agosto 2026, 02:00 — 2.4 MB — Automático</div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm" aria-label="Restaurar este backup">Restaurar</button>
<button class="btn btn-sm btn-danger" aria-label="Eliminar este backup">Eliminar</button>
</div>
</div>
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260802_020000.db.gz</div>
<div class="backup-meta">2 de agosto 2026, 02:00 — 2.3 MB — Automático</div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm">Restaurar</button>
<button class="btn btn-sm btn-danger">Eliminar</button>
</div>
</div>
<div class="backup-item">
<div class="backup-info">
<span class="backup-icon" aria-hidden="true">💾</span>
<div>
<div class="backup-name">inventschario_20260801_143022.db.gz</div>
<div class="backup-meta">1 de agosto 2026, 14:30 — 2.3 MB — <strong style="color: #92400e;">Manual</strong></div>
</div>
</div>
<div class="backup-actions">
<button class="btn btn-sm">Restaurar</button>
<button class="btn btn-sm btn-danger">Eliminar</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Locations & Departments -->
<div class="settings-section">
<div class="settings-header">
<div>
<h3>Ubicaciones y Departamentos</h3>
<p>Gestiona las ubicaciones físicas y departamentos de la institución</p>
</div>
</div>
<div class="settings-body">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
<!-- Locations -->
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Ubicaciones</span>
<button class="btn btn-sm">+ Añadir</button>
</div>
<div style="border: 1px solid #e5e5e3; border-radius: 6px; overflow: hidden;">
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 0 — Recepción</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 1 — Open Space</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 1 — Reprografía</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Planta 2 — Desp. Dirección</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; font-size: 13px; display: flex; justify-content: space-between;">
<span>Almacén</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
</div>
</div>
<!-- Departments -->
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-size: 13px; font-weight: 500;">Departamentos</span>
<button class="btn btn-sm">+ Añadir</button>
</div>
<div style="border: 1px solid #e5e5e3; border-radius: 6px; overflow: hidden;">
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Informática</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Administración</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Recursos Humanos</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; border-bottom: 1px solid #f0f0ee; font-size: 13px; display: flex; justify-content: space-between;">
<span>Contabilidad</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
<div style="padding: 8px 12px; font-size: 13px; display: flex; justify-content: space-between;">
<span>Dirección</span>
<button class="btn btn-sm btn-danger" style="padding: 2px 6px; font-size: 11px;">×</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>