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:
commit
ee849eb9a8
14 changed files with 3761 additions and 0 deletions
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal 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
72
README.md
Normal 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.
|
||||
342
docs/arquitectura-diagrama.html
Normal file
342
docs/arquitectura-diagrama.html
Normal 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
411
docs/arquitectura.md
Normal 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
184
docs/roadmap-iteracion-1.md
Normal 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
836
especificaciones.md
Normal 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.*
|
||||
18
sketches/001-dashboard/README.md
Normal file
18
sketches/001-dashboard/README.md
Normal 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.
|
||||
526
sketches/001-dashboard/index.html
Normal file
526
sketches/001-dashboard/index.html
Normal 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>
|
||||
18
sketches/002-inventario/README.md
Normal file
18
sketches/002-inventario/README.md
Normal 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.
|
||||
403
sketches/002-inventario/index.html
Normal file
403
sketches/002-inventario/index.html
Normal 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>
|
||||
19
sketches/003-formulario/README.md
Normal file
19
sketches/003-formulario/README.md
Normal 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.
|
||||
351
sketches/003-formulario/index.html
Normal file
351
sketches/003-formulario/index.html
Normal 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>
|
||||
20
sketches/004-configuracion/README.md
Normal file
20
sketches/004-configuracion/README.md
Normal 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.
|
||||
496
sketches/004-configuracion/index.html
Normal file
496
sketches/004-configuracion/index.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue