diff --git a/README.md b/README.md
index 58fab68..83496e1 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,16 @@ A simple SQLite-based system for managing tablet lending and returns.
- **Return Management**: Record when tablets are returned
- **History Tracking**: Complete history of all loans and returns
- **User Loans View**: See all tablets loaned to each user (demonstrates one-to-many relationship)
+- **Non-Loanable Devices**: Track inventory devices that cannot be loaned (e.g., projectors, monitors)
- **Project Notes**: Markdown editor for development documentation
## Files
-- `minimal_app.py` - Interactive command-line application
+- `minimal_app.py` - Interactive command-line application (includes non-loanable device management)
- `test_app.py` - Test script that demonstrates functionality
-- `tablets.db` - SQLite database (created automatically)
-- `simple_app.py` - Web-based version (requires Flask)
-- `app.py` - Alternative web version (requires Flask)
+- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table)
+- `simple_app.py` - Web-based version (requires Flask, includes non-loanable device management)
+- `app.py` - Alternative web version (requires Flask, includes non-loanable device management)
## Quick Start
@@ -144,6 +145,7 @@ python3 app.py
- **Loan Tablet**: Loan a tablet to a user (with search for large datasets)
- **User Loans**: View all tablets loaned to each user - demonstrates the **one-to-many relationship** (one user, multiple tablets)
- **Loan History**: Complete history of all loan and return transactions
+- **Non-Loanable Devices**: Manage inventory of devices that cannot be loaned (add, edit, delete, view)
- **Project Management**: Markdown editor for development notes (saved to `notes_development/project_notes.md`)
## Database Backup
@@ -153,6 +155,48 @@ To backup your data:
cp tablets.db tablets_backup_$(date +%Y%m%d).db
```
+## Testing
+
+The project includes a comprehensive unit test suite with **46 tests** covering core functionality and edge cases.
+
+### Running Tests
+
+```bash
+# Install test dependencies (if not already installed)
+source .venv/bin/activate
+uv pip install pytest pytest-cov
+
+# Run all tests
+python3 -m pytest tests/ -v
+
+# Run with coverage report
+python3 -m pytest tests/ --cov=./ --cov-report=term-missing
+```
+
+### Test Structure
+
+| File | Tests | Coverage |
+|------|-------|----------|
+| `tests/test_core.py` | 21 | Core operations (tablet, user, loan CRUD) |
+| `tests/test_edge_cases.py` | 14 | Edge cases (duplicates, invalid IDs, etc.) |
+| `tests/test_minimal_app.py` | 11 | Application function tests |
+
+### Key Edge Cases Covered
+
+- Loan a device that's already loaned (prevents double-loaning)
+- Return a non-existent loan (handles gracefully)
+- Duplicate serial numbers (rejected at database level)
+- Duplicate user identifications (rejected at database level)
+- Invalid tablet/user IDs (validated before operations)
+- Multiple loans per user (one-to-many relationship)
+- Loan-return-loan sequence (device lifecycle)
+
+### Test Configuration
+
+Tests use isolated temporary databases and automatically clean up after each test run. No impact on production data.
+
+
+
## License
This is a simple educational project. Feel free to use and modify it as needed.
\ No newline at end of file
diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md
new file mode 100644
index 0000000..f0c6929
--- /dev/null
+++ b/REQUIREMENTS.md
@@ -0,0 +1,221 @@
+# Requisitos del Sistema de Gestión de Préstamos de Tablets
+
+## Descripción General
+
+El sistema debe permitir la gestión del préstamo y devolución de dispositivos tablets entre un inventario y un conjunto de usuarios. El enfoque principal es el seguimiento del estado de cada dispositivo y su relación con los usuarios que los han tomado prestados.
+
+---
+## Requisitos Faltantes
+
+### Gestión de dispositivos no prestables
+
+✅ **IMPLEMENTADO** - El sistema ahora permite:
+- Registrar nuevos dispositivos no prestables en un inventario separado (tabla `non_loanable_devices`)
+- Gestionar dispositivos no prestables a través de:
+ - Interfaz web (app.py): /non_loanable_devices, /add_non_loanable_device, /edit_non_loanable_device, /delete_non_loanable_device
+ - Interfaz CLI (minimal_app.py): Opciones 8, 9, 10 del menú
+- La implementación utiliza una tabla separada en la misma base de datos con los siguientes campos:
+ - brand, model, serial_number (único), device_type, location, status, notes, purchase_date, purchase_cost
+
+## Requisitos Funcionales
+
+### Gestión de Dispositivos
+
+El sistema debe permitir:
+- Registrar nuevos dispositivos en el inventario
+- Identificar cada dispositivo de manera única
+- Almacenar información descriptiva de cada dispositivo (marca, modelo, número de serie)
+- Consultar el estado actual de cada dispositivo (disponible, prestado)
+
+### Gestión de Usuarios
+
+El sistema debe permitir:
+- Registrar nuevos usuarios
+- Identificar cada usuario de manera única
+- Almacenar información de contacto básica (nombre, identificación)
+- Consultar la información de los usuarios registrados
+
+### Gestión de Préstamos
+
+El sistema debe permitir:
+- Asignar un dispositivo disponible a un usuario
+- Registrar la fecha y hora del préstamo
+- Registrar la fecha y hora de la devolución
+- Consultar el historial de préstamos de un dispositivo
+- Consultar el historial de préstamos de un usuario
+- Visualizar todos los dispositivos actualmente prestados
+
+### Consulta y Reportes
+
+El sistema debe permitir:
+- Buscar dispositivos por cualquier atributo (marca, modelo, número de serie)
+- Buscar usuarios por cualquier atributo (nombre, identificación)
+- Filtrar préstamos por estado (activos, finalizados)
+- Filtrar préstamos por dispositivo
+- Filtrar préstamos por usuario
+- Ver el historial completo de préstamos y devoluciones
+
+---
+
+## Modelo de Datos
+
+### Entidades Principales
+
+1. **Dispositivo**
+ - Identificador único
+ - Marca
+ - Modelo
+ - Número de serie (único)
+ - Estado (disponible, prestado)
+
+2. **Usuario**
+ - Identificador único
+ - Nombre
+ - Identificación (única)
+
+3. **Préstamo**
+ - Identificador único
+ - Dispositivo asociado
+ - Usuario asociado
+ - Fecha de préstamo
+ - Fecha de devolución (opcional)
+ - Estado (activo, finalizado)
+
+### Relaciones
+
+- Un **usuario** puede tener **múltiples préstamos** (relación uno a muchos)
+- Un **dispositivo** solo puede estar en **un préstamo activo** a la vez
+- Un **préstamo** relaciona exactamente un dispositivo con un usuario
+- El historial de préstamos permite rastrear todos los movimientos de cada dispositivo
+
+### Reglas de Negocio
+
+1. Un dispositivo en estado "prestado" no puede ser prestado a otro usuario
+2. Un préstamo finalizado debe registrar fecha de devolución
+3. La identificación de dispositivos y usuarios debe ser única
+4. Al devolver un dispositivo, este vuelve al estado "disponible"
+
+---
+
+## Flujos de Trabajo
+
+### Registro de un Nuevo Dispositivo
+
+1. El sistema solicita la información del dispositivo (marca, modelo, número de serie)
+2. El sistema valida que el número de serie no esté registrado
+3. El sistema registra el dispositivo con estado "disponible"
+4. El sistema confirma el registro
+
+### Registro de un Nuevo Usuario
+
+1. El sistema solicita la información del usuario (nombre, identificación)
+2. El sistema valida que la identificación no esté registrada
+3. El sistema registra el usuario
+4. El sistema confirma el registro
+
+### Préstamo de un Dispositivo
+
+1. El sistema muestra los dispositivos disponibles
+2. El usuario selecciona un dispositivo
+3. El sistema muestra los usuarios registrados
+4. El usuario selecciona un usuario
+5. El sistema registra el préstamo con:
+ - Dispositivo seleccionado
+ - Usuario seleccionado
+ - Fecha y hora actual
+ - Estado "activo"
+6. El sistema actualiza el estado del dispositivo a "prestado"
+7. El sistema confirma el préstamo
+
+### Devolución de un Dispositivo
+
+1. El sistema muestra los préstamos activos
+2. El usuario selecciona un préstamo
+3. El sistema registra la fecha y hora de devolución
+4. El sistema actualiza el estado del préstamo a "finalizado"
+5. El sistema actualiza el estado del dispositivo a "disponible"
+6. El sistema confirma la devolución
+
+### Consulta de Préstamos por Usuario
+
+1. El sistema muestra la lista de usuarios
+2. El usuario selecciona un usuario
+3. El sistema muestra:
+ - Dispositivos actualmente prestados (préstamos activos)
+ - Historial de préstamos pasados (opcional: expandible)
+4. El sistema permite buscar por cualquier campo
+
+---
+
+## Requisitos No Funcionales
+
+### Usabilidad
+
+- La interfaz debe permitir búsqueda eficiente en listas grandes (500+ elementos)
+- La visualización de información debe ser clara y organizada
+- Los estados deben ser fácilmente identificables
+- Las acciones principales deben ser accesibles
+
+### Escalabilidad
+
+- El diseño debe soportar un crecimiento en el número de dispositivos y usuarios
+- La búsqueda debe ser eficiente
+- El historial debe ser consultable sin afectar el rendimiento
+
+### Mantenibilidad
+
+- La estructura de datos debe ser clara
+- Las relaciones entre entidades deben estar bien definidas
+- Los flujos de trabajo deben estar documentados
+
+---
+
+## Diseño de Interfaz (Conceptual)
+
+### Vistas Principales
+
+1. **Vista de Inventario**
+ - Lista de dispositivos
+ - Filtros por estado
+ - Búsqueda
+
+2. **Vista de Usuarios**
+ - Lista de usuarios
+ - Búsqueda
+
+3. **Vista de Préstamos Activos**
+ - Lista de préstamos en curso
+ - Filtros por dispositivo/usuario
+ - Acciones: devolver dispositivo
+
+4. **Vista de Historial**
+ - Lista completa de préstamos
+ - Filtros por fecha, usuario, dispositivo, estado
+ - Detalles de cada préstamo
+
+5. **Vista de Préstamos por Usuario**
+ - Lista de usuarios
+ - Para cada usuario: dispositivos actualmente prestados
+ - Opcional: historial de préstamos pasados (expandible)
+
+### Elementos de Interacción
+
+- Campos de búsqueda en todas las listas
+- Botones de acción claros (Prestar, Devolver, Registrar)
+- Indicadores visuales de estado (colores, iconos)
+- Confirmación de acciones críticas
+
+---
+
+## Notas de Implementación
+
+Este documento describe los requisitos a nivel de diseño. La implementación específica (lenguaje de programación, base de datos, framework, arquitectura) queda a criterio del equipo de desarrollo y puede variar según las necesidades técnicas.
+
+Los detalles técnicos como:
+- Tecnologías específicas
+- Estructura de base de datos física
+- APIs
+- Autenticación
+- Despliegue
+
+Deberán ser definidos en la fase de diseño técnico y pueden evolucionar sin afectar los requisitos funcionales descritos aquí.
diff --git a/RUNNING.md b/RUNNING.md
index 9c37f3d..02c80c4 100644
--- a/RUNNING.md
+++ b/RUNNING.md
@@ -48,10 +48,22 @@ pkill -f "python app.py"
- **Port**: 5000 (configurable in app.py)
- **Virtual Environment**: `.venv/` (created with uv)
-## Troubleshooting
-If you have issues:
-1. Check if the server is running: `ps aux | grep app.py`
-2. Check the database: `sqlite3 tablets.db`
-3. Restart the server: `source .venv/bin/activate && python app.py`
+## Running Tests
-Enjoy managing your tablets! 📱💻
\ No newline at end of file
+The project includes 46 unit tests for verifying functionality:
+
+```bash
+# Run all tests
+python3 -m pytest tests/ -v
+
+# Run with coverage
+python3 -m pytest tests/ --cov=./ --cov-report=term-missing
+```
+
+Tests cover:
+- Core CRUD operations for tablets, users, and loans
+- Edge cases (duplicates, invalid IDs, already loaned devices)
+- Application function testing
+- Non-loanable device management
+
+All tests use isolated temporary databases and do not affect production data.
diff --git a/TECHNICAL_SPECIFICATIONS.md b/TECHNICAL_SPECIFICATIONS.md
new file mode 100644
index 0000000..a6995c2
--- /dev/null
+++ b/TECHNICAL_SPECIFICATIONS.md
@@ -0,0 +1,200 @@
+# Especificaciones Técnicas
+
+Documento complementario a `REQUIREMENTS.md` con propuestas concretas de implementación técnica.
+
+---
+
+## 1. Concurrencia en Base de Datos
+
+| Opción | Descripción | Ventajas | Desventajas |
+|--------|-------------|---------|-------------|
+| **SQLite + WAL** | Modo Write-Ahead Logging | Zero-config, embebido, buena para <100 conexiones | Limitado a 1 escritor simultáneo |
+| **PostgreSQL** | Base de datos relacional | Concurrencia completa, transacciones ACID | Requiere servidor |
+| **Transacciones** | Aislamiento READ COMMITTED | Previene lecturas sucias | Bloqueos temporales |
+| **Locking optimista** | Versión de fila en actualizaciones | Sin bloqueos | Conflictos requieren reintento |
+
+**Recomendación**: SQLite con WAL para prototipos, PostgreSQL para producción. Usar transacciones para operaciones críticas (préstamo/devolución).
+
+---
+
+## 2. Autenticación Básica
+
+| Opción | Descripción | Complejidad |
+|--------|-------------|-------------|
+| **Sesiones (Flask)** | Cookies firmadas | Baja |
+| **JWT** | Tokens sin estado | Media |
+| **OAuth 2.0** | Delegación a proveedores | Alta |
+| **Basic Auth** | HTTP Basic Authentication | Mínima |
+
+**Flujo mínimo**:
+```
+Login (user/pass) → Session/JWT → Middleware verifica rol → Acceso a endpoints
+```
+
+**Roles sugeridos**:
+- `admin`: CRUD completo, gestión de usuarios
+- `staff`: Préstamos/devoluciones
+- `viewer`: Solo consulta
+
+---
+
+## 3. Separación Frontend/Backend
+
+| Arquitectura | Descripción | Stack Ejemplo |
+|--------------|-------------|---------------|
+| **Monolítica + Templates** | Servidor renderiza HTML | Flask+Jinja2, Django |
+| **API + SPA** | Backend API, frontend dinámico | FastAPI + React/Vue |
+| **API + SSR** | Backend API + renderizado servidor | Next.js + API |
+| **Microservicios** | Servicios independientes | Backend API + Frontend estático |
+
+**Para este proyecto**:
+- **Opción A**: Flask + Jinja2 (simple, suficiente para requisitos)
+- **Opción B**: FastAPI + React (escalable, separable)
+
+---
+
+## 4. Búsqueda Eficiente (500+ elementos)
+
+| Opción | Descripción | Implementación |
+|--------|-------------|----------------|
+| **Índices DB** | B-tree en columnas buscadas | `CREATE INDEX idx_brand ON tablets(brand)` |
+| **Búsqueda full-text** | Búsqueda en texto | SQLite FTS5, PostgreSQL tsvector |
+| **Filtrado cliente** | JavaScript filtra datos | Ideal para <2000 elementos |
+| **Paginación** | Dividir resultados | `LIMIT 50 OFFSET 0` |
+
+**Recomendación**: Índices en `serial_number`, `brand`, `model`, `user.name`, `user.identification`.
+
+---
+
+## 5. Estructura de Proyecto Propuesta
+
+```
+project/
+├── backend/
+│ ├── models/ # Entidades (Device, User, Loan)
+│ ├── services/ # Lógica de negocio
+│ ├── repositories/ # Acceso a datos
+│ ├── routes/ # Endpoints/API
+│ └── app.py # Configuración
+├── frontend/ # Opcional para opción B
+│ ├── public/
+│ └── src/
+├── migrations/ # Control de cambios DB
+├── tests/
+└── docs/
+```
+
+---
+
+## 6. Decisiones Clave por Tomar
+
+| Decisión | Opciones | Impacto |
+|----------|----------|---------|
+| Base de datos | SQLite / PostgreSQL | Concurrencia, despliegue |
+| Autenticación | Sesiones / JWT / Ninguna | Seguridad, complejidad |
+| Frontend | Templates / SPA / Ninguno | Experiencia usuario |
+| Despliegue | Local / Docker / Cloud | Escalabilidad |
+
+---
+
+## 7. Ejemplo de Implementación Mínima (Pseudocódigo)
+
+```python
+# modelos.py
+class Device:
+ id: int (PK)
+ serial_number: str (unique)
+ status: str (available/loaned)
+ loans: List[Loan] (one-to-many)
+
+class User:
+ id: int (PK)
+ identification: str (unique)
+ loans: List[Loan] (one-to-many)
+
+class Loan:
+ id: int (PK)
+ device_id: int (FK)
+ user_id: int (FK)
+ loan_date: datetime
+ return_date: datetime (nullable)
+ status: str (active/returned)
+
+# servicios/loan_service.py
+def loan_device(device_id: int, user_id: int) -> Loan:
+ with transaction(): # Atomicidad
+ device = get_device_for_update(device_id)
+ if device.status != 'available':
+ raise DeviceNotAvailableError()
+
+ loan = Loan.create(
+ device_id=device_id,
+ user_id=user_id,
+ loan_date=now(),
+ status='active'
+ )
+ device.status = 'loaned'
+ return loan
+```
+
+---
+
+## 8. Consideraciones Adicionales
+
+- **Validaciones**: Únicas (serial_number, identification) a nivel de base de datos
+- **Auditoría**: Campos `created_at`, `updated_at` en todas las entidades
+- **Backups**: Script para exportar base de datos periódicamente
+- **Logging**: Registrar préstamos/devoluciones para auditoría
+- **Versionado API**: `/api/v1/...` para compatibilidad futura
+- **Pruebas**: Cubrir flujos críticos (préstamo con dispositivo no disponible, devolución de préstamo inexistente)
+
+---
+
+## 9. Migración a PostgreSQL (Futuro)
+
+### Contexto
+
+El sistema actualmente utiliza **SQLite** como base de datos embebida, ideal para prototipos y aplicaciones pequeñas. Sin embargo, SQLite tiene limitaciones para escalar.
+
+### Criterios para Migración
+
+Considerar migrar a **PostgreSQL** cuando:
+- Tamaño DB > 1GB
+- Más de 50 conexiones simultáneas
+- Más de 200 usuarios activos
+- Más de 500 transacciones por minuto
+- Necesidad de múltiples servidores
+
+### Arquitectura Propuesta
+
+Usar un patrón de **Repository** para abstraer la base de datos:
+```
+backend/
+├── repositories/
+│ ├── base_repository.py # Interfaz abstracta
+│ ├── sqlite_repository.py # Implementación SQLite
+│ └── postgres_repository.py # Implementación PostgreSQL
+└── config/
+ └── database.py # Fábrica de repositorios
+```
+
+### Schema PostgreSQL
+
+El schema es similar al de SQLite pero con tipos de datos más específicos y índices adicionales para rendimiento.
+
+### Pasos para Migración
+
+1. Instalar dependencias: `pip install psycopg2-binary sqlalchemy alembic`
+2. Configurar PostgreSQL y crear base de datos
+3. Ejecutar script de migración: `python scripts/migrate_to_postgres.py`
+4. Cambiar configuración: `DB_TYPE=postgres`
+5. Iniciar aplicación
+
+### Beneficios
+
+- Concurrencia ilimitada (múltiples escritores)
+- Escalabilidad a miles de conexiones
+- Mejor rendimiento con índices
+- Seguridad integrada (autenticación, roles)
+- Backups automáticos
+- Replicación y clustering
diff --git a/app.py b/app.py
index a5332b7..7dfba4d 100644
--- a/app.py
+++ b/app.py
@@ -63,6 +63,22 @@ def init_db():
)
''')
+ # Create non_loanable_devices table for devices that cannot be loaned
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS non_loanable_devices (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ brand TEXT NOT NULL,
+ model TEXT NOT NULL,
+ serial_number TEXT UNIQUE NOT NULL,
+ device_type TEXT NOT NULL,
+ location TEXT,
+ status TEXT DEFAULT 'available',
+ notes TEXT,
+ purchase_date TEXT,
+ purchase_cost REAL
+ )
+ ''')
+
conn.commit()
@app.route('/')
@@ -319,6 +335,99 @@ def user_loans():
return render_template('user_loans.html', users_with_loans=user_loans_list)
+@app.route('/non_loanable_devices')
+def non_loanable_devices():
+ """Show all non-loanable devices"""
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM non_loanable_devices ORDER BY device_type, brand, model")
+ devices = cursor.fetchall()
+
+ return render_template('non_loanable_devices.html', devices=devices)
+
+
+@app.route('/add_non_loanable_device', methods=['GET', 'POST'])
+def add_non_loanable_device():
+ """Add a new non-loanable device to inventory"""
+ if request.method == 'POST':
+ brand = request.form['brand']
+ model = request.form['model']
+ serial_number = request.form['serial_number']
+ device_type = request.form['device_type']
+ location = request.form['location']
+ notes = request.form['notes']
+ purchase_date = request.form.get('purchase_date', '')
+ purchase_cost = request.form.get('purchase_cost', '')
+
+ try:
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute('''
+ INSERT INTO non_loanable_devices
+ (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
+ VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
+ ''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
+ conn.commit()
+ flash('Non-loanable device added successfully!', 'success')
+ except sqlite3.IntegrityError:
+ flash('Error: Serial number already exists!', 'error')
+
+ return redirect(url_for('non_loanable_devices'))
+
+ return render_template('add_non_loanable_device.html')
+
+
+@app.route('/edit_non_loanable_device/', methods=['GET', 'POST'])
+def edit_non_loanable_device(device_id):
+ """Edit a non-loanable device"""
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM non_loanable_devices WHERE id = ?", (device_id,))
+ device = cursor.fetchone()
+
+ if not device:
+ flash('Error: Device not found!', 'error')
+ return redirect(url_for('non_loanable_devices'))
+
+ if request.method == 'POST':
+ brand = request.form['brand']
+ model = request.form['model']
+ serial_number = request.form['serial_number']
+ device_type = request.form['device_type']
+ location = request.form['location']
+ status = request.form['status']
+ notes = request.form['notes']
+ purchase_date = request.form.get('purchase_date', '')
+ purchase_cost = request.form.get('purchase_cost', '')
+
+ try:
+ cursor.execute('''
+ UPDATE non_loanable_devices SET
+ brand = ?, model = ?, serial_number = ?, device_type = ?,
+ location = ?, status = ?, notes = ?, purchase_date = ?, purchase_cost = ?
+ WHERE id = ?
+ ''', (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost, device_id))
+ conn.commit()
+ flash('Device updated successfully!', 'success')
+ return redirect(url_for('non_loanable_devices'))
+ except sqlite3.IntegrityError:
+ flash('Error: Serial number already exists!', 'error')
+
+ return render_template('edit_non_loanable_device.html', device=device)
+
+
+@app.route('/delete_non_loanable_device/')
+def delete_non_loanable_device(device_id):
+ """Delete a non-loanable device"""
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,))
+ conn.commit()
+ flash('Device deleted successfully!', 'success')
+
+ return redirect(url_for('non_loanable_devices'))
+
+
if __name__ == '__main__':
# Initialize database
init_db()
diff --git a/docs/FRONTEND_OPTIONS.md b/docs/FRONTEND_OPTIONS.md
new file mode 100644
index 0000000..d8a141e
--- /dev/null
+++ b/docs/FRONTEND_OPTIONS.md
@@ -0,0 +1,1191 @@
+# Separated Frontend Architecture Options
+
+## Overview
+
+The current Tablet Management System uses **server-side rendering** with Flask templates. While this works well for tablets, the interface may be too wide for mobile devices. This document explores **separated frontend architectures** that would provide better mobile responsiveness while keeping the existing backend intact.
+
+---
+
+## Current Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Flask Backend │
+│ ┌─────────┐ ┌─────────┐ ┌─────┐ │
+│ │ Routes │ │ Models │ │ DB │ │
+│ └─────────┘ └─────────┘ └─────┘ │
+└──────────────┬──────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────┐
+│ Flask Templates (Jinja2) │
+│ ┌─────────┐ ┌─────────┐ ┌─────┐ │
+│ │ HTML │ │ CSS │ │ JS │ │
+│ └─────────┘ └─────────┘ └─────┘ │
+└─────────────────────────────────────┘
+ │
+ ▼
+ Browser (Tablet/Desktop)
+```
+
+**Limitations:**
+- Server-rendered HTML (not ideal for dynamic mobile UIs)
+- Limited interactivity without page reloads
+- CSS is basic and not responsive for mobile
+- Tight coupling between backend and frontend
+
+---
+
+## Option 1: Single Page Application (SPA) with REST API
+
+### Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Flask Backend │
+│ ┌─────────────────────────────────┐ │
+│ │ REST API │ │
+│ │ /api/tablets │ │
+│ │ /api/users │ │
+│ │ /api/loans │ │
+│ │ /api/non-loanable-devices │ │
+│ └─────────────────────────────────┘ │
+└──────────────┬──────────────────────┘
+ │ HTTP/JSON
+ ▼
+┌─────────────────────────────────────┐
+│ Frontend (React/Vue/Svelte) │
+│ ┌─────────┐ ┌─────────┐ ┌─────┐ │
+│ │ Components │ │ State │ │ Router│ │
+│ └─────────┘ └─────────┘ └─────┘ │
+└─────────────────────────────────────┘
+ │
+ ▼
+ Browser (Mobile/Tablet/Desktop)
+```
+
+### Implementation Steps
+
+#### 1. Create REST API Layer
+
+Add new routes to `app.py` (without removing existing ones):
+
+```python
+# API Routes (add to app.py)
+@app.route('/api/tablets', methods=['GET'])
+def api_get_tablets():
+ """GET /api/tablets - List all tablets"""
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM tablets")
+ tablets = [dict(row) for row in cursor.fetchall()]
+ return jsonify(tablets)
+
+@app.route('/api/tablets/', methods=['GET'])
+def api_get_tablet(tablet_id):
+ """GET /api/tablets/ - Get single tablet"""
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
+ tablet = cursor.fetchone()
+ if tablet:
+ return jsonify(dict(tablet))
+ return jsonify({'error': 'Tablet not found'}), 404
+
+@app.route('/api/tablets', methods=['POST'])
+def api_create_tablet():
+ """POST /api/tablets - Create new tablet"""
+ data = request.get_json()
+ # Validate and create
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute('''
+ INSERT INTO tablets (brand, model, serial_number, status, notes)
+ VALUES (?, ?, ?, 'available', ?)
+ ''', (data['brand'], data['model'], data['serial_number'], data.get('notes')))
+ conn.commit()
+ tablet_id = cursor.lastrowid
+ cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
+ return jsonify(dict(cursor.fetchone())), 201
+
+# Similar endpoints for users, loans, non_loanable_devices
+```
+
+#### 2. Frontend Structure (React Example)
+
+```
+frontend/
+├── public/
+│ └── index.html
+├── src/
+│ ├── components/
+│ │ ├── TabletList.jsx
+│ │ ├── TabletForm.jsx
+│ │ ├── UserList.jsx
+│ │ ├── LoanForm.jsx
+│ │ ├── LoanHistory.jsx
+│ │ ├── UserLoans.jsx
+│ │ └── NonLoanableDevices.jsx
+│ ├── hooks/
+│ │ └── useApi.js
+│ ├── services/
+│ │ └── api.js
+│ ├── App.jsx
+│ ├── index.js
+│ └── styles/
+│ ├── main.css
+│ └── responsive.css
+├── package.json
+└── README.md
+```
+
+#### 3. API Service (frontend/src/services/api.js)
+
+```javascript
+const API_BASE = '/api';
+
+export const api = {
+ // Tablets
+ getTablets: async (status = null) => {
+ const url = status ? `${API_BASE}/tablets?status=${status}` : `${API_BASE}/tablets`;
+ const response = await fetch(url);
+ return response.json();
+ },
+
+ getTablet: async (id) => {
+ const response = await fetch(`${API_BASE}/tablets/${id}`);
+ return response.json();
+ },
+
+ createTablet: async (tablet) => {
+ const response = await fetch(`${API_BASE}/tablets`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(tablet)
+ });
+ return response.json();
+ },
+
+ updateTablet: async (id, tablet) => {
+ const response = await fetch(`${API_BASE}/tablets/${id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(tablet)
+ });
+ return response.json();
+ },
+
+ // Similar methods for users, loans, non_loanable_devices
+};
+```
+
+#### 4. React Components Example
+
+```jsx
+// frontend/src/components/TabletList.jsx
+import React, { useState, useEffect } from 'react';
+import { api } from '../services/api';
+
+export function TabletList() {
+ const [tablets, setTablets] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ api.getTablets('available').then(data => {
+ setTablets(data);
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) return
Loading...
;
+
+ return (
+
+
Available Tablets
+
+ {tablets.map(tablet => (
+
+
{tablet.brand} {tablet.model}
+
Serial: {tablet.serial_number}
+
Status: {tablet.status}
+
+ ))}
+
+
+ );
+}
+```
+
+#### 5. Responsive CSS
+
+```css
+/* frontend/src/styles/responsive.css */
+
+/* Mobile-first approach */
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: #f5f5f5;
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 100%;
+ padding: 1rem;
+}
+
+/* Cards for mobile */
+.tablet-card, .user-card, .loan-card {
+ background: white;
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ border: 1px solid #e0e0e0;
+}
+
+/* Navigation */
+.nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.nav a {
+ padding: 0.75rem 1rem;
+ background-color: #4CAF50;
+ color: white;
+ text-decoration: none;
+ border-radius: 4px;
+ text-align: center;
+}
+
+/* Tables - responsive */
+.responsive-table {
+ overflow-x: auto;
+}
+
+table {
+ width: 100%;
+ min-width: 600px; /* Allows horizontal scrolling on mobile */
+}
+
+th, td {
+ padding: 0.75rem;
+ white-space: nowrap;
+}
+
+/* Forms */
+form {
+ max-width: 100%;
+}
+
+input, select, textarea {
+ width: 100%;
+ padding: 0.75rem;
+ margin-bottom: 1rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+
+/* Buttons */
+.btn {
+ padding: 0.75rem 1.5rem;
+ width: 100%;
+ margin-bottom: 0.5rem;
+}
+
+/* Breakpoints */
+@media (min-width: 600px) {
+ .nav {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .nav a {
+ flex: 1 1 auto;
+ min-width: 120px;
+ }
+
+ .tablet-card, .user-card, .loan-card {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ margin: 0 auto;
+ }
+
+ table {
+ min-width: auto;
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 960px;
+ }
+
+ .nav {
+ flex-wrap: nowrap;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+```
+
+### Pros and Cons
+
+| Aspect | Pros | Cons |
+|--------|------|------|
+| **User Experience** | Rich, dynamic UI; no page reloads | More complex to develop |
+| **Performance** | Fast after initial load; client-side rendering | Larger initial bundle |
+| **Mobile Support** | Excellent with responsive design | Needs careful CSS work |
+| **Development** | Modern tooling (React/Vue); component-based | Separate codebase to maintain |
+| **SEO** | Poor (SPA) | Needs SSR for better SEO |
+| **Backend Impact** | Minimal (just add API routes) | Need to maintain both templates and API |
+| **Deployment** | Can be hosted separately | More complex deployment |
+
+### Recommended Tech Stack
+
+- **Framework:** React (most popular) or Vue (simpler) or Svelte (smaller bundle)
+- **State Management:** React Query or SWR for data fetching
+- **Styling:** Tailwind CSS or CSS Modules
+- **Routing:** React Router
+- **Build Tool:** Vite (fast) or Create React App
+- **TypeScript:** Optional but recommended for large projects
+
+---
+
+## Option 2: Hybrid Approach (Progressive Enhancement)
+
+### Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Flask Backend │
+│ ┌─────────────────────────────────┐ │
+│ │ Dual Mode: │ │
+│ │ - Server templates (existing) │ │
+│ │ - REST API (new) │ │
+│ └─────────────────────────────────┘ │
+└──────────────┬──────────────────────┘
+ │
+ ┌─────┴─────┐
+ ▼ ▼
+┌─────────────┐ ┌─────────────┐
+│ Desktop │ │ Mobile │
+│ (Existing) │ │ (New SPA) │
+└─────────────┘ └─────────────┘
+```
+
+### How It Works
+
+1. **Desktop/Tablet:** Uses existing server-rendered templates
+2. **Mobile:** Detects mobile device and serves a minimal HTML page that loads the SPA
+3. **Shared Backend:** Both use the same Flask backend
+
+### Implementation
+
+#### 1. Device Detection Middleware
+
+```python
+# app.py
+from flask import request, redirect, url_for
+import re
+
+MOBILE_USER_AGENTS = re.compile(
+ r'android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile',
+ re.IGNORECASE
+)
+
+@app.before_request
+def detect_mobile():
+ user_agent = request.headers.get('User-Agent', '')
+ if MOBILE_USER_AGENTS.search(user_agent):
+ request.is_mobile = True
+ else:
+ request.is_mobile = False
+```
+
+#### 2. Mobile-Specific Route
+
+```python
+@app.route('/mobile')
+def mobile_app():
+ """Serve mobile SPA entry point"""
+ return render_template('mobile.html')
+
+@app.before_request
+def redirect_mobile():
+ """Redirect mobile users to SPA"""
+ if hasattr(request, 'is_mobile') and request.is_mobile:
+ if not request.path.startswith('/api') and not request.path.startswith('/mobile'):
+ return redirect(url_for('mobile_app'))
+```
+
+#### 3. Mobile Entry Point Template
+
+```html
+
+
+
+
+
+
+ Tablet Management - Mobile
+
+
+
+
+
+
+
+```
+
+### Pros and Cons
+
+| Aspect | Pros | Cons |
+|--------|------|------|
+| **User Experience** | Best of both worlds | Two UIs to maintain |
+| **Mobile Support** | Excellent | Desktop UI unchanged (may still be wide) |
+| **Development** | Gradual migration possible | More complex logic |
+| **Backend Impact** | Minimal | Device detection logic |
+| **SEO** | Good (server-rendered desktop) | Mobile SPA has poor SEO |
+| **Deployment** | Single deployment | Larger asset bundle |
+
+---
+
+## Option 3: Flask + HTMX (Lightweight Dynamic UI)
+
+### Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Flask Backend │
+│ ┌─────────────────────────────────┐ │
+│ │ Enhanced Templates │ │
+│ │ - HTML + HTMX attributes │ │
+│ │ - Partial updates via AJAX │ │
+│ └─────────────────────────────────┘ │
+└──────────────┬──────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────┐
+│ Browser (Any Device) │
+│ - HTMX handles dynamic updates │
+│ - CSS handles responsiveness │
+└─────────────────────────────────────┘
+```
+
+### What is HTMX?
+
+HTMX allows you to add interactivity to HTML without writing JavaScript. It uses attributes to:
+- Make AJAX requests
+- Update DOM elements
+- Handle form submissions
+- Show loading indicators
+
+### Implementation Example
+
+#### 1. Add HTMX to Base Template
+
+```html
+
+
+
+
+
+
+```
+
+#### 2. Enhance Templates with HTMX
+
+```html
+
+
+
Available Tablets
+
+
+
+
+
+
+ {% for tablet in available_tablets %}
+
+
{{ tablet.brand }} {{ tablet.model }}
+
Serial: {{ tablet.serial_number }}
+
+
+ {% endfor %}
+
+
+
+
+
+```
+
+#### 3. Add HTMX Endpoints
+
+```python
+@app.route('/api/tablets/search')
+def search_tablets():
+ query = request.args.get('search', '')
+ with get_db() as conn:
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT * FROM tablets
+ WHERE brand LIKE ? OR model LIKE ? OR serial_number LIKE ?
+ """, (f'%{query}%', f'%{query}%', f'%{query}%'))
+ tablets = cursor.fetchall()
+ return render_template('partials/tablet_list.html', tablets=tablets)
+
+@app.route('/api/tablets//loan', methods=['POST'])
+def loan_tablet_htmx(tablet_id):
+ # Get user from form
+ user_id = request.form.get('user_id')
+ # Loan logic...
+ return render_template('partials/loan_form.html', tablet_id=tablet_id)
+```
+
+#### 4. Responsive CSS
+
+```css
+/* Add to base.html or separate CSS file */
+
+/* Mobile-first responsive design */
+.tablet-card, .user-card, .loan-card {
+ background: white;
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.nav {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.nav a {
+ padding: 0.75rem;
+ text-align: center;
+}
+
+@media (min-width: 600px) {
+ .nav {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .tablet-card {
+ display: flex;
+ justify-content: space-between;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ margin: 0 auto;
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 960px;
+ }
+
+ .nav {
+ flex-wrap: nowrap;
+ }
+}
+```
+
+### Pros and Cons
+
+| Aspect | Pros | Cons |
+|--------|------|------|
+| **User Experience** | Dynamic updates without full page reloads | Less powerful than full SPA |
+| **Mobile Support** | Good with responsive CSS | Still limited by server rendering |
+| **Development** | Minimal changes to existing code | Need to learn HTMX |
+| **Backend Impact** | Very minimal (just add endpoints) | More routes to maintain |
+| **SEO** | Excellent (server-rendered) | Best of all options |
+| **Deployment** | No changes needed | Simple |
+| **Bundle Size** | Tiny (~14KB for HTMX) | No build step |
+
+---
+
+## Option 4: Mobile App (Native or Cross-Platform)
+
+### Architecture
+
+```
+┌─────────────────────────────────────┐
+│ Flask Backend │
+│ ┌─────────────────────────────────┐ │
+│ │ REST API │ │
+│ │ (Same as Option 1) │ │
+│ └─────────────────────────────────┘ │
+└──────────────┬──────────────────────┘
+ │ HTTP/JSON
+ ▼
+┌─────────────────────────────────────┐
+│ Mobile App │
+│ (React Native / Flutter / Capacitor)│
+└─────────────────────────────────────┘
+ │
+ ▼
+ Mobile Device
+```
+
+### Implementation Options
+
+#### A. React Native (JavaScript)
+
+```javascript
+// App.js
+import React from 'react';
+import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
+
+const API_BASE = 'http://your-server:5000/api';
+
+export default function App() {
+ const [tablets, setTablets] = React.useState([]);
+
+ React.useEffect(() => {
+ fetch(`${API_BASE}/tablets`)
+ .then(res => res.json())
+ .then(data => setTablets(data));
+ }, []);
+
+ return (
+
+ Tablet Management
+ item.id.toString()}
+ renderItem={({item}) => (
+
+ {item.brand} {item.model}
+ SN: {item.serial_number}
+ Status: {item.status}
+
+ )}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 20,
+ backgroundColor: '#f5f5f5',
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ textAlign: 'center',
+ },
+ card: {
+ backgroundColor: 'white',
+ padding: 15,
+ borderRadius: 8,
+ marginBottom: 10,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ brand: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ serial: {
+ fontSize: 14,
+ color: '#666',
+ },
+ status: {
+ fontSize: 14,
+ color: '#4CAF50',
+ },
+});
+```
+
+#### B. Flutter (Dart)
+
+```dart
+// main.dart
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+
+void main() => runApp(MyApp());
+
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Tablet Management',
+ home: TabletListScreen(),
+ );
+ }
+}
+
+class TabletListScreen extends StatefulWidget {
+ @override
+ _TabletListScreenState createState() => _TabletListScreenState();
+}
+
+class _TabletListScreenState extends State {
+ List tablets = [];
+
+ @override
+ void initState() {
+ super.initState();
+ fetchTablets();
+ }
+
+ Future fetchTablets() async {
+ final response = await http.get(Uri.parse('http://your-server:5000/api/tablets'));
+ if (response.statusCode == 200) {
+ setState(() {
+ tablets = json.decode(response.body);
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text('Tablet Management')),
+ body: ListView.builder(
+ itemCount: tablets.length,
+ itemBuilder: (context, index) {
+ final tablet = tablets[index];
+ return Card(
+ child: ListTile(
+ title: Text('${tablet['brand']} ${tablet['model']}'),
+ subtitle: Text('SN: ${tablet['serial_number']}'),
+ trailing: Text(tablet['status']),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+```
+
+#### C. Capacitor (Web App as Mobile App)
+
+Use your existing web app (Option 1 SPA) and wrap it with Capacitor:
+
+```bash
+# Install Capacitor
+npm install @capacitor/core @capacitor/cli
+npx cap init
+
+# Add platforms
+npm install @capacitor/android @capacitor/ios
+npx cap add android
+npx cap add ios
+
+# Build and sync
+npm run build
+npx cap sync
+npx cap open android # or ios
+```
+
+### Pros and Cons
+
+| Aspect | React Native | Flutter | Capacitor |
+|--------|--------------|---------|-----------|
+| **Language** | JavaScript | Dart | JavaScript |
+| **Performance** | Native | Native | WebView |
+| **Code Reuse** | ~80% with web | ~50% with web | ~100% with web |
+| **Learning Curve** | Medium (if know React) | High (new language) | Low (web devs) |
+| **Access to Native** | Good | Excellent | Limited |
+| **Bundle Size** | Medium | Large | Small |
+| **Offline Support** | Yes | Yes | Yes |
+
+---
+
+## Comparison Matrix
+
+| Feature | Current | SPA (Option 1) | Hybrid (Option 2) | HTMX (Option 3) | Mobile App (Option 4) |
+|---------|---------|---------------|------------------|----------------|----------------------|
+| **Mobile Friendly** | ❌ No | ✅ Yes | ✅ Yes | ⚠️ Partial | ✅ Yes |
+| **Desktop Friendly** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
+| **Tablet Friendly** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
+| **Development Effort** | N/A | High | Medium | Low | High |
+| **Backend Changes** | N/A | Low | Low | Very Low | Low (API only) |
+| **Learning Curve** | N/A | Medium | Medium | Low | High |
+| **SEO** | ✅ Good | ❌ Poor | ✅ Good | ✅ Good | ❌ Poor |
+| **Offline Support** | ❌ No | ✅ Yes | ❌ No | ❌ No | ✅ Yes |
+| **Performance** | ⚠️ OK | ✅ Good | ⚠️ OK | ✅ Good | ✅ Excellent |
+| **Deployment** | Simple | Complex | Medium | Simple | Complex |
+| **Maintenance** | Simple | Medium | Complex | Simple | Medium |
+
+---
+
+## Recommendations
+
+### For Immediate Improvement (Low Effort)
+
+**Choose: Option 3 (HTMX)**
+
+- Minimal code changes
+- No new build process
+- Progressive enhancement
+- Good mobile support with responsive CSS
+- Keeps existing server rendering
+
+### For Best User Experience (Medium Effort)
+
+**Choose: Option 1 (SPA with REST API)**
+
+- Modern, dynamic UI
+- Excellent mobile support
+- Can be deployed separately
+- Backend changes are minimal (just add API routes)
+
+### For Native Mobile Experience (High Effort)
+
+**Choose: Option 4 (Mobile App)**
+
+- Best mobile UX
+- Offline capabilities
+- Native device features (camera, etc.)
+- Requires separate mobile development
+
+### For Gradual Migration
+
+**Choose: Option 2 (Hybrid)**
+
+- Start with mobile SPA
+- Keep desktop as-is
+- Migrate desktop later if needed
+- Minimal risk
+
+---
+
+## Implementation Roadmap
+
+### Phase 1: Quick Win (1-2 days)
+
+1. Add responsive CSS to existing templates
+2. Add viewport meta tag
+3. Test on mobile devices
+
+**Result:** Better mobile experience with minimal changes
+
+### Phase 2: Enhanced Interactivity (3-5 days)
+
+1. Add HTMX to templates
+2. Create partial templates for updates
+3. Add new API endpoints for HTMX
+4. Test all interactions
+
+**Result:** Dynamic UI without full SPA complexity
+
+### Phase 3: Full SPA (1-2 weeks)
+
+1. Set up React/Vue project
+2. Create API layer in Flask
+3. Build frontend components
+4. Add responsive design
+5. Test on all devices
+6. Deploy frontend separately
+
+**Result:** Modern, mobile-first web application
+
+### Phase 4: Mobile App (2-4 weeks)
+
+1. Choose framework (React Native/Flutter)
+2. Set up mobile project
+3. Connect to existing API
+4. Build mobile-specific UI
+5. Add offline support
+6. Test on devices
+7. Publish to app stores
+
+**Result:** Native mobile application
+
+---
+
+## File Structure for Separated Frontend
+
+If you choose Option 1 (SPA), here's the recommended structure:
+
+```
+GestionTablets/
+├── backend/ # Existing Flask backend
+│ ├── app.py # Flask app + API routes
+│ ├── templates/ # Existing templates (keep for now)
+│ ├── static/ # Static files
+│ └── ...
+│
+├── frontend/ # NEW: Separated frontend
+│ ├── public/
+│ │ └── index.html
+│ ├── src/
+│ │ ├── components/
+│ │ │ ├── common/
+│ │ │ │ ├── Button.jsx
+│ │ │ │ ├── Card.jsx
+│ │ │ │ ├── Modal.jsx
+│ │ │ │ └── Table.jsx
+│ │ │ ├── TabletList.jsx
+│ │ │ ├── TabletForm.jsx
+│ │ │ ├── UserList.jsx
+│ │ │ ├── UserForm.jsx
+│ │ │ ├── LoanList.jsx
+│ │ │ ├── LoanForm.jsx
+│ │ │ ├── LoanHistory.jsx
+│ │ │ ├── UserLoans.jsx
+│ │ │ └── NonLoanableDevices.jsx
+│ │ ├── hooks/
+│ │ │ ├── useTablets.js
+│ │ │ ├── useUsers.js
+│ │ │ ├── useLoans.js
+│ │ │ └── useApi.js
+│ │ ├── services/
+│ │ │ └── api.js
+│ │ ├── utils/
+│ │ │ ├── formatters.js
+│ │ │ └── validators.js
+│ │ ├── App.jsx
+│ │ ├── App.css
+│ │ ├── index.js
+│ │ └── index.css
+│ ├── package.json
+│ ├── vite.config.js
+│ └── README.md
+│
+├── docs/ # Documentation
+│ ├── MIGRATION_TO_POSTGRES.md
+│ └── FRONTEND_OPTIONS.md # This document
+│
+├── scripts/ # Utility scripts
+│ └── migrate_to_postgres.py
+│
+├── .gitignore
+├── README.md
+├── pyproject.toml
+└── docker-compose.yml
+```
+
+---
+
+## API Endpoints Needed
+
+For any separated frontend, you'll need these API endpoints:
+
+### Tablets
+- `GET /api/tablets` - List all tablets
+- `GET /api/tablets?status=available` - Filter by status
+- `GET /api/tablets/` - Get single tablet
+- `POST /api/tablets` - Create tablet
+- `PUT /api/tablets/` - Update tablet
+- `DELETE /api/tablets/` - Delete tablet
+- `GET /api/tablets/search?q=query` - Search tablets
+
+### Users
+- `GET /api/users` - List all users
+- `GET /api/users/` - Get single user
+- `POST /api/users` - Create user
+- `PUT /api/users/` - Update user
+- `DELETE /api/users/` - Delete user
+- `GET /api/users/search?q=query` - Search users
+
+### Loans
+- `GET /api/loans` - List all loans
+- `GET /api/loans?status=active` - Filter by status
+- `GET /api/loans/` - Get single loan
+- `POST /api/loans` - Create loan
+- `PUT /api/loans//return` - Return tablet
+- `GET /api/loans/user/` - Get loans by user
+- `GET /api/loans/tablet/` - Get loans by tablet
+
+### Non-Loanable Devices
+- `GET /api/non-loanable-devices` - List all
+- `GET /api/non-loanable-devices/` - Get single device
+- `POST /api/non-loanable-devices` - Create device
+- `PUT /api/non-loanable-devices/` - Update device
+- `DELETE /api/non-loanable-devices/` - Delete device
+
+### Statistics
+- `GET /api/stats` - Get dashboard statistics
+
+---
+
+## Responsive Design Guidelines
+
+### Breakpoints
+
+```css
+/* Mobile-first approach */
+:root {
+ --breakpoint-xs: 0px;
+ --breakpoint-sm: 576px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 992px;
+ --breakpoint-xl: 1200px;
+}
+
+/* Usage */
+@media (min-width: 576px) { /* Small devices (landscape phones) */ }
+@media (min-width: 768px) { /* Medium devices (tablets) */ }
+@media (min-width: 992px) { /* Large devices (desktops) */ }
+@media (min-width: 1200px) { /* Extra large devices */ }
+```
+
+### Mobile-First Principles
+
+1. **Start with mobile** - Design for smallest screen first
+2. **Progressive enhancement** - Add features for larger screens
+3. **Touch targets** - Minimum 48x48px for touch elements
+4. **Font sizes** - Minimum 16px for readability
+5. **Spacing** - Adequate padding for touch
+6. **Navigation** - Bottom navigation for mobile, top for desktop
+7. **Forms** - Large, easy-to-use inputs
+8. **Tables** - Consider cards instead of tables on mobile
+
+### Touch Target Sizes
+
+| Element | Minimum Size | Recommended Size |
+|---------|--------------|------------------|
+| Buttons | 48x48px | 56x56px |
+| Form inputs | 48px height | 56px height |
+| List items | 48px height | 64px height |
+| Checkboxes/Radios | 24x24px | 32x32px |
+
+---
+
+## Deployment Options
+
+### Option A: Separate Servers
+
+```
+┌─────────────────┐ ┌─────────────────┐
+│ Backend Server │────▶│ Frontend Server │
+│ (Flask) │ │ (Nginx/Apache) │
+│ :5000 │ │ :80/:443 │
+└─────────────────┘ └─────────────────┘
+ │ │
+ ▼ ▼
+ API Requests Static Files
+```
+
+**Pros:** Separate scaling, independent deployment
+**Cons:** More complex setup, CORS configuration
+
+### Option B: Same Server, Different Routes
+
+```
+┌─────────────────────────────────────┐
+│ Flask Server │
+│ ┌─────────────────────────────────┐ │
+│ │ /api/* → Backend routes │ │
+│ │ /* → Frontend (SPA) │ │
+│ └─────────────────────────────────┘ │
+└─────────────────────────────────────┘
+ │
+ ▼
+ Nginx (reverse proxy)
+ │
+ ▼
+ Client
+```
+
+**Pros:** Simpler deployment, no CORS issues
+**Cons:** Backend serves static files
+
+### Option C: Docker Compose
+
+```yaml
+# docker-compose.yml
+version: '3.8'
+
+services:
+ backend:
+ build: ./backend
+ ports:
+ - "5000:5000"
+ environment:
+ - FLASK_ENV=production
+ restart: unless-stopped
+
+ frontend:
+ build: ./frontend
+ ports:
+ - "80:80"
+ - "443:443"
+ depends_on:
+ - backend
+ restart: unless-stopped
+
+ nginx:
+ image: nginx:alpine
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf
+ depends_on:
+ - backend
+ - frontend
+ restart: unless-stopped
+```
+
+---
+
+## Conclusion
+
+For the Tablet Management System, I recommend the following approach:
+
+### Short Term (1-2 days)
+Start with **Option 3 (HTMX)** to add dynamic updates and responsive CSS to the existing templates. This provides:
+- Immediate mobile improvements
+- Minimal code changes
+- No new dependencies (just HTMX)
+- Progressive enhancement
+
+### Medium Term (1-2 weeks)
+Migrate to **Option 1 (SPA with REST API)** for:
+- Better mobile experience
+- Modern development workflow
+- Separate frontend deployment
+- Easier to maintain long-term
+
+### Long Term (Optional)
+Consider **Option 4 (Mobile App)** if:
+- Users need offline access
+- Need native device features
+- Want app store presence
+
+The current backend (Flask + SQLite) can remain **completely unchanged** for all these options. You only need to add API endpoints, which don't affect the existing template-based functionality.
diff --git a/docs/MIGRATION_TO_POSTGRES.md b/docs/MIGRATION_TO_POSTGRES.md
new file mode 100644
index 0000000..740f8b1
--- /dev/null
+++ b/docs/MIGRATION_TO_POSTGRES.md
@@ -0,0 +1,987 @@
+# PostgreSQL Migration Guide
+
+This document describes how to migrate the Tablet Management System from SQLite to PostgreSQL when the database grows beyond SQLite's capabilities.
+
+## When to Migrate
+
+Consider migrating to PostgreSQL when you encounter any of these scenarios:
+
+| Metric | SQLite Limit | PostgreSQL | Migration Trigger |
+|--------|--------------|------------|-------------------|
+| Database Size | ~10GB max | Unlimited | >1GB |
+| Concurrent Writers | 1 | Thousands | >50 simultaneous |
+| Active Users | <100 | Millions | >200 |
+| Transactions/min | <100 | 100K+ | >500 |
+| Servers | Single machine | Cluster | Multiple servers |
+| High Availability | No | Yes | Required |
+| Backup Strategy | Manual | Automated | Automated needed |
+
+## Architecture Overview
+
+The migration uses a **Repository Pattern** to abstract the database layer, allowing both SQLite and PostgreSQL to work seamlessly.
+
+```
+project/
+├── backend/
+│ ├── config/
+│ │ ├── __init__.py
+│ │ ├── settings.py # Database configuration
+│ │ └── database.py # Repository factory
+│ ├── repositories/
+│ │ ├── __init__.py
+│ │ ├── base_repository.py # Abstract base classes
+│ │ ├── sqlite_repo.py # SQLite implementation
+│ │ └── postgres_repo.py # PostgreSQL implementation
+│ └── app.py # Main application (unchanged)
+├── migrations/ # Alembic migrations
+│ └── versions/
+│ └── initial_schema.py
+├── scripts/
+│ └── migrate_to_postgres.py # Migration script
+└── docker-compose.yml # Optional Docker setup
+```
+
+## Step 1: Install Dependencies
+
+```bash
+# For development
+pip install psycopg2-binary sqlalchemy alembic
+
+# For production (more efficient)
+pip install psycopg2 sqlalchemy alembic
+```
+
+## Step 2: Set Up PostgreSQL
+
+### Option A: Local Installation
+
+```bash
+# Ubuntu/Debian
+sudo apt update
+sudo apt install postgresql postgresql-contrib
+
+# Create database and user
+sudo -u postgres psql
+```
+
+In PostgreSQL shell:
+```sql
+CREATE DATABASE tablet_management;
+CREATE USER tablet_user WITH PASSWORD 'your_secure_password';
+GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user;
+ALTER USER tablet_user CREATEDB;
+\q
+```
+
+### Option B: Docker (Recommended for Development)
+
+```bash
+# Start PostgreSQL container
+docker run --name tablet-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_USER=tablet_user -e POSTGRES_DB=tablet_management -p 5432:5432 -d postgres:16-alpine
+
+# Or use docker-compose (see docker-compose.yml)
+docker-compose up -d postgres
+```
+
+## Step 3: Configure Environment
+
+Create a `.env` file:
+
+```bash
+# Database configuration
+DB_TYPE=postgres # or 'sqlite'
+DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management
+
+# For SQLite (fallback)
+DB_PATH=tablets.db
+```
+
+Or set environment variables:
+
+```bash
+export DB_TYPE=postgres
+export DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management
+```
+
+## Step 4: Create Repository Abstraction
+
+### Base Repository (Abstract Interface)
+
+```python
+# backend/repositories/base_repository.py
+from abc import ABC, abstractmethod
+from typing import Optional, List
+from datetime import datetime
+
+
+class BaseTabletRepository(ABC):
+ @abstractmethod
+ def get_by_id(self, tablet_id: int) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_by_serial(self, serial: str) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_all(self, status: Optional[str] = None) -> List[dict]:
+ pass
+
+ @abstractmethod
+ def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
+ pass
+
+ @abstractmethod
+ def update_status(self, tablet_id: int, status: str) -> bool:
+ pass
+
+ @abstractmethod
+ def delete(self, tablet_id: int) -> bool:
+ pass
+
+
+class BaseUserRepository(ABC):
+ @abstractmethod
+ def get_by_id(self, user_id: int) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_by_identification(self, identification: str) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_all(self) -> List[dict]:
+ pass
+
+ @abstractmethod
+ def add(self, name: str, email: Optional[str], phone: Optional[str], identification: str) -> dict:
+ pass
+
+
+class BaseLoanRepository(ABC):
+ @abstractmethod
+ def get_by_id(self, loan_id: int) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_active_by_tablet(self, tablet_id: int) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_by_user(self, user_id: int) -> List[dict]:
+ pass
+
+ @abstractmethod
+ def get_all(self, status: Optional[str] = None) -> List[dict]:
+ pass
+
+ @abstractmethod
+ def create(self, tablet_id: int, user_id: int) -> dict:
+ pass
+
+ @abstractmethod
+ def return_loan(self, loan_id: int) -> bool:
+ pass
+
+
+class BaseNonLoanableDeviceRepository(ABC):
+ @abstractmethod
+ def get_by_id(self, device_id: int) -> Optional[dict]:
+ pass
+
+ @abstractmethod
+ def get_all(self) -> List[dict]:
+ pass
+
+ @abstractmethod
+ def add(self, brand: str, model: str, serial_number: str, device_type: str,
+ location: Optional[str] = None, notes: Optional[str] = None,
+ purchase_date: Optional[str] = None, purchase_cost: Optional[float] = None) -> dict:
+ pass
+
+ @abstractmethod
+ def update(self, device_id: int, **kwargs) -> bool:
+ pass
+
+ @abstractmethod
+ def delete(self, device_id: int) -> bool:
+ pass
+```
+
+### SQLite Implementation
+
+```python
+# backend/repositories/sqlite_repo.py
+import sqlite3
+from typing import Optional, List
+from .base_repository import (
+ BaseTabletRepository, BaseUserRepository,
+ BaseLoanRepository, BaseNonLoanableDeviceRepository
+)
+
+
+class SQLiteTabletRepository(BaseTabletRepository):
+ def __init__(self, db_path: str = 'tablets.db'):
+ self.db_path = db_path
+ self._init_db()
+
+ def _init_db(self):
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS tablets (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ brand TEXT NOT NULL,
+ model TEXT NOT NULL,
+ serial_number TEXT UNIQUE NOT NULL,
+ status TEXT DEFAULT 'available',
+ notes TEXT
+ )
+ ''')
+ conn.commit()
+ conn.close()
+
+ def get_by_id(self, tablet_id: int) -> Optional[dict]:
+ conn = sqlite3.connect(self.db_path)
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
+ row = cursor.fetchone()
+ conn.close()
+ return dict(row) if row else None
+
+ def get_by_serial(self, serial: str) -> Optional[dict]:
+ conn = sqlite3.connect(self.db_path)
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM tablets WHERE serial_number = ?", (serial,))
+ row = cursor.fetchone()
+ conn.close()
+ return dict(row) if row else None
+
+ def get_all(self, status: Optional[str] = None) -> List[dict]:
+ conn = sqlite3.connect(self.db_path)
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ query = "SELECT * FROM tablets"
+ params = ()
+ if status:
+ query += " WHERE status = ?"
+ params = (status,)
+ cursor.execute(query, params)
+ results = [dict(row) for row in cursor.fetchall()]
+ conn.close()
+ return results
+
+ def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
+ conn = sqlite3.connect(self.db_path)
+ conn.row_factory = sqlite3.Row
+ cursor = conn.cursor()
+ cursor.execute('''
+ INSERT INTO tablets (brand, model, serial_number, status, notes)
+ VALUES (?, ?, ?, 'available', ?)
+ ''', (brand, model, serial_number, notes))
+ conn.commit()
+ tablet_id = cursor.lastrowid
+ cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
+ row = cursor.fetchone()
+ conn.close()
+ return dict(row)
+
+ def update_status(self, tablet_id: int, status: str) -> bool:
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute("UPDATE tablets SET status = ? WHERE id = ?", (status, tablet_id))
+ conn.commit()
+ changed = cursor.rowcount > 0
+ conn.close()
+ return changed
+
+ def delete(self, tablet_id: int) -> bool:
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM tablets WHERE id = ?", (tablet_id,))
+ conn.commit()
+ deleted = cursor.rowcount > 0
+ conn.close()
+ return deleted
+
+
+# Similar implementations for SQLiteUserRepository, SQLiteLoanRepository, etc.
+```
+
+### PostgreSQL Implementation
+
+```python
+# backend/repositories/postgres_repo.py
+import psycopg2
+from psycopg2 import sql
+from psycopg2.extras import DictCursor
+from typing import Optional, List
+from .base_repository import (
+ BaseTabletRepository, BaseUserRepository,
+ BaseLoanRepository, BaseNonLoanableDeviceRepository
+)
+
+
+class PostgreSQLTabletRepository(BaseTabletRepository):
+ def __init__(self, connection_string: str):
+ self.connection_string = connection_string
+ self._init_db()
+
+ def _get_connection(self):
+ return psycopg2.connect(self.connection_string)
+
+ def _init_db(self):
+ conn = self._get_connection()
+ cursor = conn.cursor()
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS tablets (
+ id SERIAL PRIMARY KEY,
+ brand VARCHAR(100) NOT NULL,
+ model VARCHAR(100) NOT NULL,
+ serial_number VARCHAR(50) UNIQUE NOT NULL,
+ status VARCHAR(20) DEFAULT 'available',
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+ )
+ ''')
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_tablets_serial
+ ON tablets(serial_number)
+ ''')
+ cursor.execute('''
+ CREATE INDEX IF NOT EXISTS idx_tablets_status
+ ON tablets(status)
+ ''')
+ conn.commit()
+ cursor.close()
+ conn.close()
+
+ def get_by_id(self, tablet_id: int) -> Optional[dict]:
+ conn = self._get_connection()
+ cursor = conn.cursor(cursor_factory=DictCursor)
+ cursor.execute("SELECT * FROM tablets WHERE id = %s", (tablet_id,))
+ row = cursor.fetchone()
+ conn.close()
+ return dict(row) if row else None
+
+ def get_by_serial(self, serial: str) -> Optional[dict]:
+ conn = self._get_connection()
+ cursor = conn.cursor(cursor_factory=DictCursor)
+ cursor.execute("SELECT * FROM tablets WHERE serial_number = %s", (serial,))
+ row = cursor.fetchone()
+ conn.close()
+ return dict(row) if row else None
+
+ def get_all(self, status: Optional[str] = None) -> List[dict]:
+ conn = self._get_connection()
+ cursor = conn.cursor(cursor_factory=DictCursor)
+ query = "SELECT * FROM tablets"
+ params = ()
+ if status:
+ query += " WHERE status = %s"
+ params = (status,)
+ cursor.execute(query, params)
+ results = [dict(row) for row in cursor.fetchall()]
+ conn.close()
+ return results
+
+ def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
+ conn = self._get_connection()
+ cursor = conn.cursor(cursor_factory=DictCursor)
+ cursor.execute('''
+ INSERT INTO tablets (brand, model, serial_number, status, notes)
+ VALUES (%s, %s, %s, 'available', %s)
+ RETURNING *
+ ''', (brand, model, serial_number, notes))
+ row = cursor.fetchone()
+ conn.commit()
+ conn.close()
+ return dict(row)
+
+ def update_status(self, tablet_id: int, status: str) -> bool:
+ conn = self._get_connection()
+ cursor = conn.cursor()
+ cursor.execute(
+ "UPDATE tablets SET status = %s, updated_at = NOW() WHERE id = %s",
+ (status, tablet_id)
+ )
+ conn.commit()
+ changed = cursor.rowcount > 0
+ conn.close()
+ return changed
+
+ def delete(self, tablet_id: int) -> bool:
+ conn = self._get_connection()
+ cursor = conn.cursor()
+ cursor.execute("DELETE FROM tablets WHERE id = %s", (tablet_id,))
+ conn.commit()
+ deleted = cursor.rowcount > 0
+ conn.close()
+ return deleted
+
+
+# Similar implementations for PostgreSQLUserRepository, PostgreSQLLoanRepository, etc.
+```
+
+## Step 5: Create Repository Factory
+
+```python
+# backend/config/database.py
+import os
+from backend.repositories.sqlite_repo import (
+ SQLiteTabletRepository, SQLiteUserRepository,
+ SQLiteLoanRepository, SQLiteNonLoanableDeviceRepository
+)
+from backend.repositories.postgres_repo import (
+ PostgreSQLTabletRepository, PostgreSQLUserRepository,
+ PostgreSQLLoanRepository, PostgreSQLNonLoanableDeviceRepository
+)
+from backend.repositories.base_repository import (
+ BaseTabletRepository, BaseUserRepository,
+ BaseLoanRepository, BaseNonLoanableDeviceRepository
+)
+
+
+class DatabaseConfig:
+ def __init__(self):
+ self.db_type = os.getenv('DB_TYPE', 'sqlite')
+ self.db_url = os.getenv('DB_URL', '')
+ self.db_path = os.getenv('DB_PATH', 'tablets.db')
+
+ @property
+ def is_postgres(self) -> bool:
+ return self.db_type == 'postgres'
+
+
+def get_tablet_repository() -> BaseTabletRepository:
+ config = DatabaseConfig()
+ if config.is_postgres:
+ return PostgreSQLTabletRepository(config.db_url)
+ else:
+ return SQLiteTabletRepository(config.db_path)
+
+
+def get_user_repository() -> BaseUserRepository:
+ config = DatabaseConfig()
+ if config.is_postgres:
+ return PostgreSQLUserRepository(config.db_url)
+ else:
+ return SQLiteUserRepository(config.db_path)
+
+
+def get_loan_repository() -> BaseLoanRepository:
+ config = DatabaseConfig()
+ if config.is_postgres:
+ return PostgreSQLLoanRepository(config.db_url)
+ else:
+ return SQLiteLoanRepository(config.db_path)
+
+
+def get_non_loanable_device_repository() -> BaseNonLoanableDeviceRepository:
+ config = DatabaseConfig()
+ if config.is_postgres:
+ return PostgreSQLNonLoanableDeviceRepository(config.db_url)
+ else:
+ return SQLiteNonLoanableDeviceRepository(config.db_path)
+```
+
+## Step 6: Update Application to Use Repositories
+
+Modify your application to use the repository pattern:
+
+```python
+# In your app.py or service layer
+from backend.config.database import (
+ get_tablet_repository, get_user_repository,
+ get_loan_repository, get_non_loanable_device_repository
+)
+
+# Instead of direct SQLite calls:
+tablet_repo = get_tablet_repository()
+user_repo = get_user_repository()
+loan_repo = get_loan_repository()
+
+# Example: Loan a tablet
+def loan_tablet(tablet_id: int, user_id: int):
+ # Get repositories
+ tablet_repo = get_tablet_repository()
+ user_repo = get_user_repository()
+ loan_repo = get_loan_repository()
+
+ # Validate
+ tablet = tablet_repo.get_by_id(tablet_id)
+ if not tablet:
+ raise ValueError("Tablet not found")
+
+ if tablet['status'] != 'available':
+ raise ValueError("Tablet not available")
+
+ user = user_repo.get_by_id(user_id)
+ if not user:
+ raise ValueError("User not found")
+
+ # Check for active loan
+ active_loan = loan_repo.get_active_by_tablet(tablet_id)
+ if active_loan:
+ raise ValueError("Tablet already loaned")
+
+ # Create loan
+ loan = loan_repo.create(tablet_id, user_id)
+
+ # Update tablet status
+ tablet_repo.update_status(tablet_id, 'loaned')
+
+ return loan
+```
+
+## Step 7: Create Migration Script
+
+```python
+# scripts/migrate_to_postgres.py
+#!/usr/bin/env python3
+"""
+Migration script from SQLite to PostgreSQL
+"""
+import sqlite3
+import psycopg2
+from psycopg2.extras import DictCursor
+import argparse
+from tqdm import tqdm
+import os
+
+
+def create_postgres_tables(conn):
+ """Create all tables in PostgreSQL"""
+ cursor = conn.cursor()
+
+ # Tablets
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS tablets (
+ id SERIAL PRIMARY KEY,
+ brand VARCHAR(100) NOT NULL,
+ model VARCHAR(100) NOT NULL,
+ serial_number VARCHAR(50) UNIQUE NOT NULL,
+ status VARCHAR(20) DEFAULT 'available',
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+ )
+ ''')
+
+ # Users
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS users (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ email VARCHAR(255),
+ phone VARCHAR(20),
+ identification VARCHAR(50) UNIQUE NOT NULL,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+ )
+ ''')
+
+ # Loans
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS loans (
+ id SERIAL PRIMARY KEY,
+ tablet_id INTEGER NOT NULL REFERENCES tablets(id) ON DELETE RESTRICT,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
+ loan_date TIMESTAMP NOT NULL DEFAULT NOW(),
+ return_date TIMESTAMP,
+ status VARCHAR(20) DEFAULT 'active',
+ created_at TIMESTAMP DEFAULT NOW()
+ )
+ ''')
+
+ # Non-loanable devices
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS non_loanable_devices (
+ id SERIAL PRIMARY KEY,
+ brand VARCHAR(100) NOT NULL,
+ model VARCHAR(100) NOT NULL,
+ serial_number VARCHAR(50) UNIQUE NOT NULL,
+ device_type VARCHAR(50) NOT NULL,
+ location VARCHAR(100),
+ status VARCHAR(20) DEFAULT 'available',
+ notes TEXT,
+ purchase_date DATE,
+ purchase_cost DECIMAL(10,2),
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+ )
+ ''')
+
+ # Indexes for performance
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_serial ON tablets(serial_number)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_status ON tablets(status)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_brand ON tablets(brand)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_identification ON users(identification)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_tablet ON loans(tablet_id)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_user ON loans(user_id)')
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_status ON loans(status)')
+
+ conn.commit()
+
+
+def migrate_table(conn_sqlite, conn_pg, table_name: str, pg_create_table: str):
+ """Generic migration for a table"""
+ cursor_sqlite = conn_sqlite.cursor()
+ cursor_pg = conn_pg.cursor()
+
+ # Get all data from SQLite
+ cursor_sqlite.execute(f"SELECT * FROM {table_name}")
+ rows = cursor_sqlite.fetchall()
+
+ if not rows:
+ print(f"No data to migrate for {table_name}")
+ return
+
+ # Get column names
+ column_names = [desc[0] for desc in cursor_sqlite.description]
+
+ # Prepare INSERT statement
+ columns = ', '.join(column_names)
+ placeholders = ', '.join(['%s'] * len(column_names))
+ insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders}) ON CONFLICT DO NOTHING"
+
+ # Migrate data
+ for row in tqdm(rows, desc=f"Migrating {table_name}"):
+ cursor_pg.execute(insert_sql, row)
+
+ conn_pg.commit()
+ print(f"✓ Migrated {len(rows)} rows from {table_name}")
+
+
+def migrate_all(sqlite_path: str, pg_url: str):
+ """Migrate all data from SQLite to PostgreSQL"""
+ print("Starting migration from SQLite to PostgreSQL...")
+
+ # Connect to SQLite
+ conn_sqlite = sqlite3.connect(sqlite_path)
+
+ # Connect to PostgreSQL
+ conn_pg = psycopg2.connect(pg_url)
+
+ try:
+ # Create tables
+ print("Creating PostgreSQL tables...")
+ create_postgres_tables(conn_pg)
+
+ # Migrate each table
+ migrate_table(conn_sqlite, conn_pg, 'tablets', '')
+ migrate_table(conn_sqlite, conn_pg, 'users', '')
+ migrate_table(conn_sqlite, conn_pg, 'loans', '')
+ migrate_table(conn_sqlite, conn_pg, 'non_loanable_devices', '')
+
+ print("\n✓ Migration completed successfully!")
+ print(f" SQLite: {sqlite_path}")
+ print(f" PostgreSQL: {pg_url}")
+
+ except Exception as e:
+ conn_pg.rollback()
+ print(f"\n✗ Migration failed: {e}")
+ raise
+ finally:
+ conn_sqlite.close()
+ conn_pg.close()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Migrate from SQLite to PostgreSQL')
+ parser.add_argument('--sqlite', default='tablets.db', help='SQLite database path')
+ parser.add_argument('--postgres', required=True, help='PostgreSQL connection URL')
+ args = parser.parse_args()
+
+ migrate_all(args.sqlite, args.postgres)
+```
+
+## Step 8: Run Migration
+
+```bash
+# Test the migration first (dry run)
+python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management_test
+
+# Verify data in test database
+psql -U tablet_user -d tablet_management_test -c "SELECT COUNT(*) FROM tablets;"
+
+# When ready, migrate to production
+python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management
+```
+
+## Step 9: Switch to PostgreSQL
+
+```bash
+# Update environment variables
+export DB_TYPE=postgres
+export DB_URL=postgresql://tablet_user:password@localhost:5432/tablet_management
+
+# Restart application
+python app.py
+```
+
+## Step 10: Verify and Monitor
+
+```bash
+# Check application logs for errors
+# Monitor database connections
+psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;"
+
+# Check active connections
+psql -U postgres -c "SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management';"
+```
+
+## Rollback Plan
+
+If something goes wrong:
+
+1. **Immediate rollback:**
+ ```bash
+ # Switch back to SQLite
+export DB_TYPE=sqlite
+export DB_PATH=tablets.db
+ python app.py
+ ```
+
+2. **Data verification:**
+ ```bash
+ # Compare counts
+ sqlite3 tablets.db "SELECT COUNT(*) FROM tablets;"
+ psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;"
+ ```
+
+3. **Backup PostgreSQL data:**
+ ```bash
+ pg_dump -U tablet_user -d tablet_management > postgres_backup_$(date +%Y%m%d).sql
+ ```
+
+## Docker Compose (Optional)
+
+For easy deployment with Docker:
+
+```yaml
+# docker-compose.yml
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: tablet_db
+ environment:
+ POSTGRES_DB: tablet_management
+ POSTGRES_USER: tablet_user
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U tablet_user -d tablet_management"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+
+ app:
+ build: .
+ container_name: tablet_app
+ environment:
+ DB_TYPE: postgres
+ DB_URL: postgresql://tablet_user:${POSTGRES_PASSWORD:-changeme}@postgres:5432/tablet_management
+ ports:
+ - "5000:5000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ restart: unless-stopped
+
+volumes:
+ postgres_data:
+```
+
+Start with Docker:
+```bash
+docker-compose up -d
+```
+
+## Benefits of PostgreSQL
+
+### Performance
+- **Concurrency:** Multiple writers simultaneously (no lock contention)
+- **Indexing:** Advanced index types (B-tree, Hash, GiST, GIN, BRIN)
+- **Query Optimization:** Advanced query planner
+- **Connection Pooling:** Built-in support
+
+### Scalability
+- **Vertical:** Handles large datasets efficiently
+- **Horizontal:** Read replicas, partitioning, sharding
+- **Connections:** Supports thousands of concurrent connections
+
+### Reliability
+- **ACID Compliance:** Full transaction support
+- **Point-in-Time Recovery:** Restore to any moment
+- **Replication:** Master-slave, synchronous, asynchronous
+- **Backups:** `pg_dump`, `pg_basebackup`, continuous archiving
+
+### Security
+- **Authentication:** Multiple methods (password, MD5, SCRAM, LDAP, Kerberos)
+- **Authorization:** Role-based access control (RBAC)
+- **Row-Level Security:** Policies for fine-grained access
+- **Encryption:** SSL, at-rest encryption
+
+### Features
+- **JSON Support:** Native JSON/JSONB data type
+- **Full-Text Search:** Advanced text search capabilities
+- **Arrays:** Store arrays of values
+- **Custom Types:** Create your own data types
+- **Triggers:** Automatic actions on events
+- **Stored Procedures:** Server-side functions
+
+## Monitoring PostgreSQL
+
+### Basic Queries
+
+```sql
+-- Active connections
+SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management';
+
+-- Table sizes
+SELECT table_name, pg_size_pretty(pg_total_relation_size(table_name))
+FROM information_schema.tables WHERE table_schema = 'public';
+
+-- Index usage
+SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
+FROM pg_stat_user_indexes;
+
+-- Slow queries (requires pg_stat_statements extension)
+SELECT query, total_time, calls, mean_time
+FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10;
+```
+
+### Enable pg_stat_statements
+
+```sql
+-- In PostgreSQL
+CREATE EXTENSION pg_stat_statements;
+
+-- Then in postgresql.conf
+shared_preload_libraries = 'pg_stat_statements'
+pg_stat_statements.track = all
+```
+
+## Maintenance Tasks
+
+### Regular Maintenance
+
+```bash
+# Vacuum (reclaim space, update statistics)
+vacuumdb -U tablet_user -d tablet_management --analyze
+
+# Reindex (rebuild indexes)
+reindexdb -U tablet_user -d tablet_management
+```
+
+### Backup Strategy
+
+```bash
+# Daily backup
+pg_dump -U tablet_user -d tablet_management > /backups/tablet_management_$(date +%Y%m%d).sql
+
+# Compressed backup
+pg_dump -U tablet_user -d tablet_management | gzip > /backups/tablet_management_$(date +%Y%m%d).sql.gz
+
+# Continuous archiving (WAL)
+# In postgresql.conf:
+wal_level = replica
+archive_mode = on
+archive_command = 'test ! -f /backups/wal/%f && cp %p /backups/wal/%f'
+```
+
+## Performance Optimization
+
+### Configuration Tuning
+
+```conf
+# postgresql.conf recommendations
+shared_buffers = 4GB # 25% of total RAM
+work_mem = 16MB # For complex sorts
+maintenance_work_mem = 512MB # For VACUUM, index creation
+effective_cache_size = 12GB # 75% of total RAM
+random_page_cost = 1.1 # SSD: 1.1, HDD: 4.0
+max_worker_processes = 8 # Number of CPU cores
+max_parallel_workers_per_gather = 4 # Parallel query workers
+max_connections = 200 # Expected max connections
+```
+
+### Index Optimization
+
+```sql
+-- Add indexes for common queries
+CREATE INDEX idx_loans_user_status ON loans(user_id, status);
+CREATE INDEX idx_loans_date_range ON loans(loan_date, return_date);
+
+-- Partial index for active loans
+CREATE INDEX idx_loans_active ON loans(tablet_id) WHERE status = 'active';
+
+-- Composite index for user loans
+CREATE INDEX idx_loans_user_tablet ON loans(user_id, tablet_id);
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Connection refused:**
+```bash
+# Check if PostgreSQL is running
+sudo systemctl status postgresql
+
+# Check port
+netstat -tuln | grep 5432
+```
+
+**Authentication failed:**
+```bash
+# Verify user and password
+psql -U tablet_user -d tablet_management -h localhost
+
+# Check pg_hba.conf
+sudo nano /etc/postgresql/16/main/pg_hba.conf
+```
+
+**Database does not exist:**
+```bash
+# Create database
+createdb -U postgres tablet_management
+```
+
+**Permission denied:**
+```sql
+-- Grant permissions
+GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user;
+GRANT ALL ON SCHEMA public TO tablet_user;
+```
+
+### Logs
+
+```bash
+# PostgreSQL logs
+sudo tail -f /var/log/postgresql/postgresql-16-main.log
+
+# Application logs
+journalctl -u tablet_management -f
+```
+
+## Conclusion
+
+Migrating from SQLite to PostgreSQL provides:
+- Better performance at scale
+- True concurrency
+- Enhanced reliability
+- Advanced features
+- Production-ready infrastructure
+
+The repository pattern ensures a smooth transition with minimal code changes, and the migration script automates the data transfer process.
diff --git a/gestiontablets.sh b/gestiontablets.sh
new file mode 100644
index 0000000..1d4b29d
--- /dev/null
+++ b/gestiontablets.sh
@@ -0,0 +1 @@
+vibe --resume fa1bb230
diff --git a/minimal_app.py b/minimal_app.py
index ff0295a..381a26d 100644
--- a/minimal_app.py
+++ b/minimal_app.py
@@ -44,6 +44,22 @@ def init_db():
)
''')
+ # Create non_loanable_devices table
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS non_loanable_devices (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ brand TEXT NOT NULL,
+ model TEXT NOT NULL,
+ serial_number TEXT UNIQUE NOT NULL,
+ device_type TEXT NOT NULL,
+ location TEXT,
+ status TEXT DEFAULT 'available',
+ notes TEXT,
+ purchase_date TEXT,
+ purchase_cost REAL
+ )
+ ''')
+
conn.commit()
conn.close()
@@ -202,6 +218,59 @@ def show_loan_history():
conn.close()
+
+def add_non_loanable_device(brand, model, serial_number, device_type, location='', notes='', purchase_date='', purchase_cost=None):
+ """Add a new non-loanable device to inventory"""
+ conn = sqlite3.connect('tablets.db')
+ cursor = conn.cursor()
+
+ try:
+ cursor.execute('''
+ INSERT INTO non_loanable_devices
+ (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
+ VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
+ ''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
+ conn.commit()
+ print(f"✓ Added non-loanable device: {brand} {model} ({serial_number}) - Type: {device_type}")
+ except sqlite3.IntegrityError:
+ print(f"✗ Error: Serial number {serial_number} already exists")
+ finally:
+ conn.close()
+
+
+def show_non_loanable_devices():
+ """Show all non-loanable devices"""
+ conn = sqlite3.connect('tablets.db')
+ cursor = conn.cursor()
+
+ cursor.execute("SELECT id, brand, model, serial_number, device_type, location, status FROM non_loanable_devices")
+ devices = cursor.fetchall()
+
+ print("\n=== Non-Loanable Devices ===")
+ if devices:
+ for device in devices:
+ print(f"ID: {device[0]}, Type: {device[4]}, {device[1]} {device[2]} ({device[3]}), Location: {device[5] or 'N/A'}, Status: {device[6]}")
+ else:
+ print("No non-loanable devices")
+
+ conn.close()
+
+
+def delete_non_loanable_device(device_id):
+ """Delete a non-loanable device"""
+ conn = sqlite3.connect('tablets.db')
+ cursor = conn.cursor()
+
+ cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,))
+ conn.commit()
+
+ if cursor.rowcount > 0:
+ print(f"✓ Non-loanable device {device_id} deleted")
+ else:
+ print(f"✗ Error: Device {device_id} not found")
+
+ conn.close()
+
def main():
"""Main menu"""
init_db()
@@ -218,9 +287,12 @@ def main():
print("5. Show Available Tablets")
print("6. Show Active Loans")
print("7. Show Loan History")
- print("8. Exit")
+ print("8. Add Non-Loanable Device")
+ print("9. Show Non-Loanable Devices")
+ print("10. Delete Non-Loanable Device")
+ print("11. Exit")
- choice = input("Enter your choice (1-8): ")
+ choice = input("Enter your choice (1-11): ")
if choice == '1':
print("\n=== Add Tablet ===")
@@ -269,6 +341,28 @@ def main():
show_loan_history()
elif choice == '8':
+ print("\n=== Add Non-Loanable Device ===")
+ brand = input("Brand: ")
+ model = input("Model: ")
+ serial_number = input("Serial Number: ")
+ device_type = input("Device Type (e.g., projector, monitor): ")
+ location = input("Location (optional): ")
+ notes = input("Notes (optional): ")
+ add_non_loanable_device(brand, model, serial_number, device_type, location, notes)
+
+ elif choice == '9':
+ show_non_loanable_devices()
+
+ elif choice == '10':
+ print("\n=== Delete Non-Loanable Device ===")
+ show_non_loanable_devices()
+ device_id = input("Enter Device ID to delete: ")
+ try:
+ delete_non_loanable_device(int(device_id))
+ except ValueError:
+ print("✗ Error: Invalid ID format")
+
+ elif choice == '11':
print("Goodbye!")
break
diff --git a/pyproject.toml b/pyproject.toml
index e409926..08df5da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,3 +5,33 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = "test_*.py"
+python_classes = "Test*"
+python_functions = "test_*"
+verbose = 1
+addopts = "-v"
+
+[tool.coverage.run]
+source = ["."]
+omit = [
+ "*/tests/*",
+ "*/.venv/*",
+ "*/__pycache__/*",
+ "*/.git/*",
+ "*/templates/*",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+]
+
+[tool.coverage.html]
+directory = "htmlcov"
diff --git a/simple_app.py b/simple_app.py
index f50478d..0674035 100644
--- a/simple_app.py
+++ b/simple_app.py
@@ -62,6 +62,22 @@ def init_db():
)
''')
+ # Create non_loanable_devices table
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS non_loanable_devices (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ brand TEXT NOT NULL,
+ model TEXT NOT NULL,
+ serial_number TEXT UNIQUE NOT NULL,
+ device_type TEXT NOT NULL,
+ location TEXT,
+ status TEXT DEFAULT 'available',
+ notes TEXT,
+ purchase_date TEXT,
+ purchase_cost REAL
+ )
+ ''')
+
conn.commit()
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
diff --git a/templates/add_non_loanable_device.html b/templates/add_non_loanable_device.html
new file mode 100644
index 0000000..ae0489e
--- /dev/null
+++ b/templates/add_non_loanable_device.html
@@ -0,0 +1,63 @@
+{% extends "base.html" %}
+
+{% block content %}
+
Add Non-Loanable Device
+
Add a device that will be tracked in inventory but cannot be loaned to users (e.g., projectors, monitors, etc.)
+{% endblock %}
diff --git a/templates/user_loans.html b/templates/user_loans.html
index 8c10045..07fac86 100644
--- a/templates/user_loans.html
+++ b/templates/user_loans.html
@@ -7,45 +7,98 @@
This page demonstrates the many-to-many relationship between users and tablets via the loans table.
-
+
+
+
+
+
+
+
{% for user_data in users_with_loans %}
{% set user = user_data.user %}
{% set loans = user_data.loans %}
+ {% set active_loans = loans|selectattr('status', 'equalto', 'active')|list %}
+ {% set returned_loans = loans|selectattr('status', 'equalto', 'returned')|list %}
-