diff --git a/README.md b/README.md
index 83496e1..58fab68 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,15 @@ 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 (includes non-loanable device management)
+- `minimal_app.py` - Interactive command-line application
- `test_app.py` - Test script that demonstrates functionality
-- `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)
+- `tablets.db` - SQLite database (created automatically)
+- `simple_app.py` - Web-based version (requires Flask)
+- `app.py` - Alternative web version (requires Flask)
## Quick Start
@@ -145,7 +144,6 @@ 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
@@ -155,48 +153,6 @@ 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
deleted file mode 100644
index f0c6929..0000000
--- a/REQUIREMENTS.md
+++ /dev/null
@@ -1,221 +0,0 @@
-# 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 02c80c4..9c37f3d 100644
--- a/RUNNING.md
+++ b/RUNNING.md
@@ -48,22 +48,10 @@ pkill -f "python app.py"
- **Port**: 5000 (configurable in app.py)
- **Virtual Environment**: `.venv/` (created with uv)
-## Running Tests
+## 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`
-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.
+Enjoy managing your tablets! 📱💻
\ No newline at end of file
diff --git a/TECHNICAL_SPECIFICATIONS.md b/TECHNICAL_SPECIFICATIONS.md
deleted file mode 100644
index a6995c2..0000000
--- a/TECHNICAL_SPECIFICATIONS.md
+++ /dev/null
@@ -1,200 +0,0 @@
-# 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 7dfba4d..a5332b7 100644
--- a/app.py
+++ b/app.py
@@ -63,22 +63,6 @@ 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('/')
@@ -335,99 +319,6 @@ 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
deleted file mode 100644
index d8a141e..0000000
--- a/docs/FRONTEND_OPTIONS.md
+++ /dev/null
@@ -1,1191 +0,0 @@
-# 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
deleted file mode 100644
index 740f8b1..0000000
--- a/docs/MIGRATION_TO_POSTGRES.md
+++ /dev/null
@@ -1,987 +0,0 @@
-# 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
deleted file mode 100644
index 1d4b29d..0000000
--- a/gestiontablets.sh
+++ /dev/null
@@ -1 +0,0 @@
-vibe --resume fa1bb230
diff --git a/minimal_app.py b/minimal_app.py
index 381a26d..ff0295a 100644
--- a/minimal_app.py
+++ b/minimal_app.py
@@ -44,22 +44,6 @@ 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()
@@ -218,59 +202,6 @@ 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()
@@ -287,12 +218,9 @@ def main():
print("5. Show Available Tablets")
print("6. Show Active Loans")
print("7. Show Loan History")
- print("8. Add Non-Loanable Device")
- print("9. Show Non-Loanable Devices")
- print("10. Delete Non-Loanable Device")
- print("11. Exit")
+ print("8. Exit")
- choice = input("Enter your choice (1-11): ")
+ choice = input("Enter your choice (1-8): ")
if choice == '1':
print("\n=== Add Tablet ===")
@@ -341,28 +269,6 @@ 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 08df5da..e409926 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,33 +5,3 @@ 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 0674035..f50478d 100644
--- a/simple_app.py
+++ b/simple_app.py
@@ -62,22 +62,6 @@ 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
deleted file mode 100644
index ae0489e..0000000
--- a/templates/add_non_loanable_device.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% 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 07fac86..8c10045 100644
--- a/templates/user_loans.html
+++ b/templates/user_loans.html
@@ -7,98 +7,45 @@
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 %}
-