diff --git a/.coverage b/.coverage deleted file mode 100644 index f539a48..0000000 Binary files a/.coverage and /dev/null differ diff --git a/.env.example b/.env.example deleted file mode 100644 index 6540c16..0000000 --- a/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Configuración de ejemplo para GestionTablets -# COPIA este archivo a .env y ajusta los valores según tu entorno - -# Configuración de Flask -FLASK_APP=app.py -FLASK_ENV=development - -# Clave secreta para sesiones y CSRF -# GENERA UNA NUEVA CON: openssl rand -hex 32 -# O en Python: python3 -c "import secrets; print(secrets.token_hex(32))" -SECRET_KEY=cambiar_esta_clave_por_una_segura_en_produccion - -# Configuración de la base de datos -DATABASE=tablets.db - -# Puerto del servidor (solo para desarrollo) -PORT=5000 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3beb5d8..0000000 --- a/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info/ - -# Virtual environments -.venv/ - -# Database files -*.db -*.db-journal - -# Environment files -.env -.env.local -.env.*.local - -# IDE/Editor -.idea/ -.vscode/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Python -.python-version - -# Project-specific -notes_development/ -.vibe/ - -# Logs and temporary files -*.log -*.tmp - -# Test coverage -.htmlcov/ -.coverage - -# Local configuration diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ea43bfa..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -# Changelog - -## [2025-12-01] - Student Data Import Implementation Initial Release - -- **Added** new `students` table schema with fields: `id`, `order_number`, `cial_code`, `nif_nie_passport`, `registration_number`, `file_number`, `first_name`, `last_name`, `full_name`, `birth_date`, `gender`, `study_group`, `created_at`, `updated_at`. -- **Added** indexes on `cial_code`, `nif_nie_passport`, `(last_name, first_name)`, `birth_date`, `gender`, `study_group`. -- **Added** `assigned_to_student` foreign key column to `tablets` and `non_loanable_devices` tables. -- **Added** `student_id` foreign key to `loans` table. -- **Implemented** CSV import script (`scripts/import_students.py`) for ISO-8859-1 (Latin-1) encoded CSV files with automatic UTF-8 handling, name field splitting, date conversion (DD/MM/YYYY → YYYY-MM-DD), discard "Estudio" field, duplicate checking, progress reporting, and dry-run support. -- **Created** database migration script (`scripts/migrate_database.py`) with backup, schema creation, index creation, foreign key addition, and consistency verification. -- **Updated** Flask application (`app.py`): - - Added new routes: `/students`, `/add_student`, `/student/`, `/edit_student/`, `/delete_student/`, `/import_students`. - - Modified loan routes to optionally associate tablet loans with students. - - Added student model definitions and relationships. - - Added indexes and validation for new student fields. -- **Added** Jinja2 front‑end templates: - - `templates/students.html` – list/search/student details with filters and pagination. - - `templates/add_student.html` – form for adding new students. - - `templates/edit_student.html` – form for editing existing students. - - `templates/student_detail.html` – view student details including loan and device history. - - `templates/import_students.html` – CSV import interface with instructions. - - Updated `base.html` to include Students navigation link. - - Updated `loan_tablet.html` with student selection dropdown. -- **Updated** `requirements.txt` to include Flask, Flask-Babel, python-dotenv, pytest, pytest-cov, waitress. -- **Created** documentation file `docs/DATABASE_SCHEMA_SPECIFICATION.md` with detailed schema definition, import rules, and migration path. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c37e12e..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,82 +0,0 @@ -# Contributing Guide - -## Git Workflow - -### Branch Strategy - -| Branch Type | Naming | Purpose | -|-------------|--------|---------| -| `main` | `main` | Production-ready code | -| `develop` | `develop` | Integration branch for features | -| Feature | `feature/` | New functionality | -| Hotfix | `hotfix/` | Urgent bug fixes | - -### Workflow - -```bash -# Clone repository -git clone -cd GestionTablets - -# Setup develop branch -git checkout -b develop - -# Create feature branch -git checkout -b feature/my-feature -git push origin feature/my-feature - -# Make changes, commit -git add . -git commit -m "feat: add my feature" -git push origin feature/my-feature - -# Create Pull Request to develop -``` - -### Commit Message Format - -``` -type(scope): description - -[optional body] - -[optional footer] -``` - -**Types**: -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation changes -- `refactor`: Code refactoring -- `chore`: Maintenance tasks -- `test`: Test-related changes - -**Examples**: -- `feat(loans): add user loans view page` -- `fix(tablet): validate serial number uniqueness` -- `docs: update README with database schema` -- `refactor(app): extract loan logic to service` - -### Pull Request Process - -1. Target `develop` branch (or `main` for hotfixes) -2. Include clear description of changes -3. Reference related issues -4. Wait for code review -5. Squash merge preferred - -### Reverting Changes - -```bash -# Discard unstaged changes -git checkout -- file.txt - -# Unstage file -git reset HEAD file.txt - -# Revert to previous commit (safe) -git revert HEAD - -# Hard reset to commit (destructive) -git reset --hard -``` diff --git a/Datos_programa.csv b/Datos_programa.csv deleted file mode 100644 index e8d7df8..0000000 --- a/Datos_programa.csv +++ /dev/null @@ -1,2 +0,0 @@ -N Or.,"Apellidos, Nombre",Fecha Nac.,C.I.A.L.,NIF/NIE/Pas.,Registro,Expediente,Sexo,Grupo,Estudio -1,"PERIQUITA DE LOS PALOTES, JUANITA",24/08/2009,B19L64119K,24587243H,4847,,M,1 FPB BSICA,1 CFGB Servicios Socioculturales y a la Comunidad - Actividades Domsticas y Limpieza de edificios (LOMLOE-LOOIFP) diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index d6ab4d5..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,442 +0,0 @@ -# Student Data Import Implementation Summary - -## Overview - -This document summarizes the implementation of the student data import functionality for the GestionTablets project. The implementation includes a new database schema for students, CSV import scripts, updated application code, and new frontend templates. - -## Files Created - -### 1. Database Schema Specification -**File:** `docs/DATABASE_SCHEMA_SPECIFICATION.md` - -- Complete SQLite database schema specification -- Detailed field descriptions and data types -- Index definitions for optimal query performance -- Data import specifications and validation rules -- API endpoint definitions -- Migration path from existing database - -### 2. CSV Import Script -**File:** `scripts/import_students.py` - -Features: -- Reads CSV files encoded in ISO-8859-1 (Latin-1) -- Converts encoding to UTF-8 automatically -- Splits "Apellidos, Nombre" field into separate "last_name" and "first_name" fields -- Converts date format from DD/MM/YYYY to YYYY-MM-DD -- Discards the "Estudio" field as per requirements -- Validates all required fields (CIAL code, first name, last name) -- Handles duplicates gracefully -- Provides detailed statistics and error reporting -- Supports dry-run mode for testing - -Usage: -```bash -# Dry run (validate without importing) -python3 scripts/import_students.py Datos_programa.csv --dry-run --verbose - -# Actual import -python3 scripts/import_students.py Datos_programa.csv --verbose -``` - -### 3. Database Migration Script -**File:** `scripts/migrate_database.py` - -Features: -- Creates backup of existing database before migration -- Creates new `students` table with all required fields and indexes -- Adds `assigned_to_student` column to `tablets` and `non_loanable_devices` tables -- Adds `student_id` column to `loans` table -- Adds timestamp columns (`created_at`, `updated_at`) to existing tables -- Verifies migration success -- Supports dry-run mode - -Usage: -```bash -# Dry run -python3 scripts/migrate_database.py --dry-run - -# Actual migration -python3 scripts/migrate_database.py -``` - -## Files Modified - -### 1. Application Code -**File:** `app.py` - -Changes: -- Added `students` table creation in `init_db()` function -- Added indexes for students table -- Updated `non_loanable_devices` table to include `assigned_to_student` field -- Added new routes: - - `/students` - List all students with search, filter, and pagination - - `/add_student` - Add new student (GET/POST) - - `/student/` - View student details - - `/edit_student/` - Edit student (GET/POST) - - `/delete_student/` - Delete student - - `/import_students` - CSV import interface (GET/POST) -- Updated `loan_tablet` route to support optional student association -- Added student selection to loan form - -### 2. Base Template -**File:** `templates/base.html` - -Changes: -- Added "Students" link to navigation menu - -### 3. Loan Tablet Template -**File:** `templates/loan_tablet.html` - -Changes: -- Updated to use Bootstrap 5 classes (matching other templates) -- Added student selection dropdown with search functionality -- Improved form layout and styling - -### 4. Requirements -**File:** `requirements.txt` - -Updated with all necessary dependencies: -- Flask==2.3.3 -- Flask-Babel==2.0.0 -- python-dotenv==1.0.0 -- pytest==7.4.0 -- pytest-cov==4.1.0 -- waitress==2.1.2 - -## Files Created (Templates) - -### 1. Students List Template -**File:** `templates/students.html` - -Features: -- Displays all students in a paginated table -- Search functionality (by name, CIAL, NIF, etc.) -- Filter by gender and study group -- Responsive design -- Action buttons for view, edit, delete - -### 2. Add Student Template -**File:** `templates/add_student.html` - -Features: -- Form for adding new students -- All required fields with validation -- Organized in logical sections (Identification, Personal Info, Study Info) -- Responsive design - -### 3. Student Detail Template -**File:** `templates/student_detail.html` - -Features: -- Displays complete student information -- Shows assigned tablets and other devices -- Shows loan history -- Breadcrumbs for navigation -- Edit and delete buttons - -### 4. Edit Student Template -**File:** `templates/edit_student.html` - -Features: -- Form for editing existing students -- Pre-populated with current student data -- Same validation as add form -- Breadcrumbs for navigation - -### 5. Import Students Template -**File:** `templates/import_students.html` - -Features: -- File upload form for CSV import -- Instructions and requirements -- Sample data format preview -- Clear visual design - -## Database Schema - -### New Table: students - -```sql -CREATE TABLE IF NOT EXISTS students ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - order_number INTEGER, - cial_code TEXT UNIQUE NOT NULL, - nif_nie_passport TEXT UNIQUE, - registration_number INTEGER, - file_number TEXT, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - full_name TEXT NOT NULL, - birth_date TEXT, - gender TEXT CHECK(gender IN ('M', 'F', 'O')), - study_group TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT unique_identification UNIQUE (cial_code, nif_nie_passport) -); -``` - -### Indexes Created - -- `idx_students_cial` - On cial_code -- `idx_students_nif` - On nif_nie_passport -- `idx_students_name` - On (last_name, first_name) -- `idx_students_birth_date` - On birth_date -- `idx_students_gender` - On gender -- `idx_students_group` - On study_group -- `idx_tablets_student` - On tablets.assigned_to_student -- `idx_non_loanable_student` - On non_loanable_devices.assigned_to_student -- `idx_loans_student` - On loans.student_id - -### Modified Tables - -1. **tablets**: Added `assigned_to_student` column (foreign key to students) -2. **non_loanable_devices**: Added `assigned_to_student` column (foreign key to students) -3. **loans**: Added `student_id` column (foreign key to students) -4. All tables: Added `created_at` and `updated_at` timestamp columns - -## CSV Import Process - -### Field Mapping - -| CSV Field | Database Field | Transformation | -|-----------|----------------|----------------| -| Nº Or. | order_number | Integer conversion | -| Apellidos, Nombre | last_name, first_name | Split on ", " | -| Apellidos, Nombre | full_name | Keep as-is | -| Fecha Nac. | birth_date | DD/MM/YYYY → YYYY-MM-DD | -| C.I.A.L. | cial_code | Direct mapping | -| NIF/NIE/Pas. | nif_nie_passport | Direct mapping | -| Registro | registration_number | Integer conversion | -| Expediente | file_number | Direct mapping (nullable) | -| Sexo | gender | Direct mapping (M/F/O) | -| Grupo | study_group | Direct mapping | -| Estudio | - | **DISCARDED** | - -### Validation Rules - -1. **Required Fields**: cial_code, first_name, last_name -2. **Unique Fields**: cial_code, nif_nie_passport (if provided) -3. **Date Format**: birth_date must be valid YYYY-MM-DD -4. **Gender**: Must be 'M', 'F', or 'O' - -## Testing - -### Import Script Test Results - -``` -$ python3 scripts/import_students.py Datos_programa.csv --dry-run --verbose - -Processing row 1/1: - Raw: {'nº or.': '1', 'apellidos, nombre': 'PERIQUITA DE LOS PALOTES, JUANITA', ...} - Processed: {'order_number': 1, 'cial_code': 'B19L64119K', ...} - [DRY RUN] Would import: PERIQUITA DE LOS PALOTES, JUANITA (B19L64119K) - -IMPORT STATISTICS -Total rows in CSV: 1 -Rows processed: 1 -Valid rows: 1 -Invalid rows: 0 -``` - -### Actual Import Test - -``` -$ python3 scripts/import_students.py Datos_programa.csv --verbose - -Processing row 1/1: - Inserted student ID: 1 - -IMPORT STATISTICS -Total rows in CSV: 1 -Rows processed: 1 -Valid rows: 1 -Invalid rows: 0 -Rows imported: 1 -Duplicates skipped: 0 -Errors: 0 -``` - -### Database Verification - -```sql -SELECT * FROM students; --- Returns: 1 row with all fields correctly populated -``` - -## New Features - -### 1. Student Management -- List all students with pagination -- Search and filter students -- Add new students -- View student details -- Edit student information -- Delete students (with safety checks) -- Import students from CSV - -### 2. Enhanced Loan Management -- Loan tablets to students directly -- Associate loans with both staff users and students -- View student loan history - -### 3. Device Assignment -- Assign tablets to students -- Assign non-loanable devices to students -- View all devices assigned to a student - -## Usage Examples - -### Import Students from CSV - -```bash -# Import from file -python3 scripts/import_students.py path/to/data.csv - -# Import with custom database -python3 scripts/import_students.py data.csv --database mydb.db - -# Dry run to validate -python3 scripts/import_students.py data.csv --dry-run - -# Skip errors and continue -python3 scripts/import_students.py data.csv --skip-errors -``` - -### Web Interface - -1. Navigate to `/students` to view all students -2. Click "Add Student" to add a new student manually -3. Click "Import CSV" to upload a CSV file -4. Click on a student to view their details, assigned devices, and loan history -5. When loaning a tablet, optionally select a student to associate with the loan - -## Migration Instructions - -### For Existing Installations - -1. **Backup your database**: - ```bash - cp tablets.db tablets.db.backup - ``` - -2. **Run the migration script**: - ```bash - python3 scripts/migrate_database.py - ``` - -3. **Import your CSV data**: - ```bash - python3 scripts/import_students.py Datos_programa.csv - ``` - -4. **Start the application**: - ```bash - python3 app.py - ``` - -### For New Installations - -1. **Initialize the database**: - ```bash - python3 app.py # This will create the database with all tables - ``` - -2. **Import your CSV data**: - ```bash - python3 scripts/import_students.py Datos_programa.csv - ``` - -3. **Start the application**: - ```bash - python3 app.py - ``` - -## File Structure - -``` -GestionTablets/ -├── app.py # Updated with student routes -├── scripts/ -│ ├── import_students.py # CSV import script -│ └── migrate_database.py # Database migration script -├── templates/ -│ ├── base.html # Updated with Students link -│ ├── loan_tablet.html # Updated with student selection -│ ├── students.html # New: Student list -│ ├── add_student.html # New: Add student form -│ ├── edit_student.html # New: Edit student form -│ ├── student_detail.html # New: Student details -│ └── import_students.html # New: CSV import interface -├── docs/ -│ └── DATABASE_SCHEMA_SPECIFICATION.md # New: Schema documentation -├── requirements.txt # Updated dependencies -└── Datos_programa.csv # Sample CSV file -``` - -## Next Steps - -1. **Test the application**: Run the app and verify all student functionality works -2. **Add translations**: Update translation files for new student-related strings -3. **Add more CSV files**: Test with additional CSV files to ensure robustness -4. **Performance testing**: Test with large CSV files (1000+ rows) -5. **Error handling**: Test edge cases (empty files, malformed data, etc.) - -## Notes - -- The implementation preserves all existing functionality -- The new student features are fully integrated with the existing tablet and loan management -- All templates use consistent styling (Bootstrap 5 classes) -- The import script handles encoding conversion automatically -- The database migration is safe and creates backups automatically - -## Roadmap - -- **Español (Spanish)** — Default language for this internal tool -- **English** — Deprecated; available for legacy users but not actively maintained - -### Frontend Revamp (High Priority) - -**Goal**: Improve mobile readability and standardize UI across all templates - -#### 1. Database Results Display (Main Content Area) -- **Current issue**: Tables overflow on mobile, inconsistent styling across templates -- **Solution**: - - Responsive tables with horizontal scroll on mobile - - Card-based layouts for mobile (show data as stacked cards instead of tables) - - Standardize column widths and spacing - - Improve readability of data cells (better contrast, padding) - -#### 2. CSS Framework Standardization -- **Current issue**: Mixed Bootstrap 5, Tailwind, and plain CSS across templates -- **Solution**: - - Standardize all templates on Tailwind CSS (already in use for `index.html`) - - Remove Bootstrap and Font Awesome dependencies - - Create reusable component classes for buttons, forms, cards - -#### 3. Mobile Navigation -- **Current issue**: Horizontal navigation bar with 8+ links doesn't collapse on mobile -- **Solution**: - - Implement hamburger menu for mobile devices - - Responsive breakpoints for different screen sizes - -#### 4. Form Responsiveness -- **Current issue**: Forms use Bootstrap grid classes, inconsistent spacing -- **Solution**: - - Tailwind responsive grid for form layouts - - Consistent input styling and validation feedback - - Mobile-friendly touch targets - -#### 5. Dark Mode Enhancement -- **Current issue**: Incomplete dark mode coverage, some elements still have light backgrounds -- **Solution**: - - Comprehensive dark mode CSS variables - - Test all pages in dark mode - - Ensure proper contrast ratios - -### Implementation Approach -- **Phase 1**: Fix database results tables (mobile scroll, card alternative) -- **Phase 2**: Standardize CSS framework (remove Bootstrap, use Tailwind only) -- **Phase 3**: Responsive navigation and forms -- **Phase 4**: Dark mode polish and accessibility improvements diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index 2de19d4..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,354 +0,0 @@ -# Instalación y Configuración - GestionTablets - -**Versión:** 1.0 -**Fecha:** 24 de junio de 2026 -**Idioma:** Español - ---- - -## 📋 Requisitos Previos - -Antes de instalar GestionTablets, asegúrate de que tu sistema cumple con los siguientes requisitos: - -### Sistema Operativo -- **Linux** (recomendado: Debian 12+, Ubuntu 22.04+) -- **macOS** (11+) -- **Windows** (10+ con WSL2 recomendado) - -### Dependencias del Sistema - -| Requisito | Versión Mínima | Cómo instalar (Debian/Ubuntu) | Notas | -|-----------|----------------|--------------------------------|-------| -| Python | 3.13.0 | `sudo apt update && sudo apt install python3.13` | **Obligatorio** | -| Git | 2.30+ | `sudo apt install git` | Para clonar el repositorio | -| curl | - | `sudo apt install curl` | Para instalar `uv` | -| SQLite3 | 3.35+ | `sudo apt install sqlite3` | Incluido en Python | - -> ⚠️ **NOTA IMPORTANTE:** Este proyecto **requiere Python 3.13+** debido al uso de `uv` como gestor de dependencias. - ---- - -## 🚀 Instalación Rápida - -Ejecuta estos comandos en tu terminal: - -```bash -# 1. Clonar el repositorio -git clone http://hq.ijuanes.ovh:3333/ijuanes/GestionTablets.git -cd GestionTablets - -# 2. Ejecutar el script de configuración (requiere permisos de ejecución) -chmod +x setup.sh -./setup.sh -``` - -El script `setup.sh` se encargará de: -- Instalar `uv` si no está presente -- Crear un entorno virtual aislado -- Instalar todas las dependencias Python -- Inicializar la base de datos -- Configurar las variables de entorno básicas - ---- - -## 🔧 Instalación Manual (sin setup.sh) - -Si prefieres instalar manualmente, sigue estos pasos: - -### 1. Instalar uv (gestor de paquetes de Python) - -```bash -# Instalar uv (Astral) -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Añadir uv al PATH (para la sesión actual) -export PATH="$HOME/.local/bin:$PATH" - -# Verificar instalación -uv --version -``` - -> **Nota:** Si usas `zsh`, añade `export PATH="$HOME/.local/bin:$PATH"` a tu `~/.zshrc`. -> Si usas `bash`, añádelo a tu `~/.bashrc` o `~/.bash_profile`. - -### 2. Crear entorno virtual - -```bash -# Crear entorno virtual con uv -uv venv .venv - -# Activar el entorno virtual -source .venv/bin/activate -``` - -### 3. Instalar dependencias - -```bash -# Instalar dependencias desde requirements.txt -uv pip install -r requirements.txt -``` - -### 4. Inicializar la base de datos - -```bash -# Inicializar la base de datos SQLite -python3 -c "from app import init_db; init_db(); print('✅ Base de datos inicializada')" -``` - -### 5. Configurar variables de entorno - -Crea un archivo `.env` en el directorio raíz del proyecto: - -```bash -# Copiar el archivo de ejemplo (si existe) -cp .env.example .env - -# O crear manualmente -cat > .env << EOF -# Configuración de Flask -FLASK_APP=app.py -FLASK_ENV=production - -# Clave secreta (generar una nueva con: openssl rand -hex 32) -SECRET_KEY=tu_clave_secreta_aqui_cambiarla - -# Base de datos (opcional, por defecto: tablets.db) -DATABASE=tablets.db -EOF -``` - -> ⚠️ **IMPORTANTE:** Genera una clave secreta segura: -> ```bash -> openssl rand -hex 32 -> ``` - ---- - -## 🏃‍♂️ Ejecución - -### Desarrollo - -```bash -# Activar el entorno virtual (si no lo está) -source .venv/bin/activate - -# Ejecutar la aplicación en modo desarrollo -python3 app.py -``` - -La aplicación estará disponible en: `http://localhost:5000` - -### Producción - -Para producción, se recomienda usar **Waitress** (ya incluido en `requirements.txt`): - -```bash -# Activar el entorno virtual -source .venv/bin/activate - -# Ejecutar con Waitress (más robusto que el servidor de desarrollo) -waitress-serve --port=8080 app:app -``` - -La aplicación estará disponible en: `http://localhost:8080` - -> **Nota:** Para producción real, considera: -> - Usar un proxy inverso (Nginx, Apache) -> - Configurar HTTPS -> - Establecer un dominio/IP estática - ---- - -## 📦 Actualización - -Para actualizar a la última versión: - -```bash -# Desde el directorio del proyecto -git pull origin develop - -# Actualizar dependencias (opcional, si requirements.txt ha cambiado) -source .venv/bin/activate -uv pip install -r requirements.txt --upgrade - -# Reiniciar la aplicación -# (Ctrl+C para detener, luego vuelve a ejecutar) -``` - ---- - -## 🔄 Migración de Base de Datos - -Si ya tienes una base de datos existente y necesitas migrar a la nueva estructura (con soporte para alumnos): - -```bash -# Hacer backup de la base de datos actual -cp tablets.db tablets.db.backup - -# Ejecutar el script de migración -python3 scripts/migrate_database.py - -# Importar datos de alumnos (si tienes un CSV) -python3 scripts/import_students.py Datos_programa.csv --verbose -``` - -> **Nota:** El script de migración crea automáticamente un backup antes de realizar cambios. - ---- - -## 🧪 Ejecución de Pruebas - -Para verificar que todo funciona correctamente: - -```bash -# Activar el entorno virtual -source .venv/bin/activate - -# Ejecutar todas las pruebas -python3 -m pytest - -# Ejecutar con cobertura de código -python3 -m pytest --cov=app --cov-report=term - -# Ejecutar pruebas específicas -python3 -m pytest tests/test_import.py -v -``` - -Deberías ver: `46 passed, 1 warning in X.XXs` - ---- - -## 📂 Estructura del Proyecto - -``` -GestionTablets/ -├── app.py # Aplicación Flask principal -├── requirements.txt # Dependencias Python -├── setup.sh # Script de configuración automática -├── INSTALL.md # Este documento -├── .env.example # Plantilla de variables de entorno -├── .gitignore # Archivos ignorados por Git -├── tablets.db # Base de datos SQLite (generada) -├── docs/ -│ └── DATABASE_SCHEMA_SPECIFICATION.md # Esquema de la base de datos -├── scripts/ -│ ├── import_students.py # Importación de alumnos desde CSV -│ └── migrate_database.py # Migración de base de datos -├── templates/ # Plantillas HTML (Jinja2) -│ ├── base.html # Plantilla base -│ ├── index.html # Página principal -│ ├── students.html # Lista de alumnos -│ └── ... -└── translations/ # Archivos de traducción (i18n) -``` - ---- - -## 🛠️ Solución de Problemas - -### Problema: "Command 'uv' not found" - -**Solución:** -```bash -# Instalar uv manualmente -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Añadir al PATH -export PATH="$HOME/.local/bin:$PATH" - -# Verificar -uv --version -``` - -### Problema: "Python 3.13 not found" - -**Solución:** -```bash -# Instalar Python 3.13 en Debian/Ubuntu -sudo apt update -sudo apt install python3.13 python3.13-venv python3.13-dev - -# Verificar -python3.13 --version -``` - -### Problema: "ModuleNotFoundError: No module named 'flask'" - -**Solución:** -```bash -# Asegúrate de que el entorno virtual está activado -source .venv/bin/activate - -# Instalar dependencias -uv pip install -r requirements.txt -``` - -### Problema: "Permission denied" al ejecutar setup.sh - -**Solución:** -```bash -# Dar permisos de ejecución -chmod +x setup.sh - -# Ejecutar -./setup.sh -``` - -### Problema: La base de datos no se inicializa - -**Solución:** -```bash -# Forzar inicialización manual -python3 -c "from app import init_db; init_db()" -``` - ---- - -## 📝 Variables de Entorno - -| Variable | Valor por Defecto | Descripción | Requerida | -|----------|-------------------|-------------|-----------| -| `FLASK_APP` | `app.py` | Archivo principal de Flask | No | -| `FLASK_ENV` | `production` | Entorno de Flask (`development` o `production`) | No | -| `SECRET_KEY` | - | Clave secreta para sesiones (¡genera una nueva!) | **Sí** | -| `DATABASE` | `tablets.db` | Ruta a la base de datos SQLite | No | -| `PORT` | `5000` | Puerto del servidor (solo para desarrollo) | No | - ---- - -## 🔒 Seguridad - -### Clave Secreta -- **Nunca** uses la clave secreta por defecto en producción. -- Genera una nueva con: `openssl rand -hex 32` -- Guárdala en `.env` (este archivo **no** debe comitearse a Git) - -### Base de Datos -- El archivo `tablets.db` contiene todos los datos del sistema. -- **Haz backups regulares:** `cp tablets.db tablets.db.backup` -- No expongas el archivo públicamente. - -### Acceso a la Aplicación -- Actualmente, la aplicación no tiene autenticación. -- **Recomendación para producción:** - - Usa un proxy inverso con autenticación (Nginx + Basic Auth) - - O implementa autenticación en Flask (ver propuestas de mejora) - ---- - -## 📞 Soporte - -Si encuentras problemas: - -1. **Revisa esta documentación** (INSTALL.md) -2. **Consulta los logs** de la aplicación -3. **Verifica los requisitos** (Python 3.13+, uv, etc.) -4. **Contacta al administrador** del sistema (ijuanes) - ---- - -## 📚 Documentación Adicional - -- [DOCUMENTACION_RECAPITULACION.md](DOCUMENTACION_RECAPITULACION.md) - Recapitulación técnica completa -- [IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md) - Resumen de implementación -- [docs/DATABASE_SCHEMA_SPECIFICATION.md](docs/DATABASE_SCHEMA_SPECIFICATION.md) - Esquema detallado de la base de datos diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2eaafb4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 ijuanes + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 0ded615..d75d17d 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,3 @@ -# Tablet Lending and Return Management System +# GestionTablets -A simple SQLite-based system for managing tablet lending and returns. - -## Features - -- **Tablet Management**: Add, track, and manage tablets in inventory -- **User Management**: Register users who can borrow tablets -- **Loan Tracking**: Track which tablets are loaned to which users -- **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` - **DEPRECATED** - Interactive command-line application (limited functionality, use `app.py` instead) -- `test_app.py` - Test script that demonstrates functionality -- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table) -- `app.py` - **Recommended** - Full-featured web version (requires Flask, includes all features) - -## Quick Start - -### 1. Run the test to see it in action - -```bash -python3 test_app.py -``` - -This will: -- Create the database -- Add sample tablets and users -- Demonstrate loan and return operations -- Show the complete workflow - -### 2. Run the web application (Recommended) - -```bash -python3 app.py -``` - -This provides a full-featured web interface for: -- Adding tablets (with notes field) -- Adding users (with email and phone) -- Loaning tablets -- Returning tablets -- Viewing inventory and loan status -- User loans view with search -- Non-loanable devices management -- Project management with markdown notes - -### 3. Database Structure - -The system uses three main tables with a **many-to-many relationship** between users and tablets: - -**tablets** -- `id`: Primary key -- `brand`: Tablet brand -- `model`: Tablet model -- `serial_number`: Unique serial number -- `status`: 'available' or 'loaned' - -**users** -- `id`: Primary key -- `name`: User's name -- `identification`: Unique identification - -**loans** (junction table) -- `id`: Primary key -- `tablet_id`: Foreign key to tablets -- `user_id`: Foreign key to users -- `loan_date`: When the tablet was loaned -- `return_date`: When the tablet was returned (NULL if still active) -- `status`: 'active' or 'returned' - -> **Note**: The loans table enables a many-to-many relationship. One user can loan **multiple tablets** (one-to-many from user to loans), and each tablet can be loaned to different users over time. The `status` field on tablets ensures a device can only be loaned to one user at a time. - -## Requirements - -- Python 3.x (built-in sqlite3 module) -- No additional dependencies needed for the command-line version -- Flask required for web versions (optional) - -## Usage Examples - -### Adding a tablet -``` -python3 -c " -import sqlite3 -conn = sqlite3.connect('tablets.db') -cursor = conn.cursor() -cursor.execute('INSERT INTO tablets (brand, model, serial_number, status) VALUES (?, ?, ?, ?)', - ('Microsoft', 'Surface Pro', 'SN004', 'available')) -conn.commit() -conn.close() -print('Tablet added!') -" -``` - -### Adding a user -``` -python3 -c " -import sqlite3 -conn = sqlite3.connect('tablets.db') -cursor = conn.cursor() -cursor.execute('INSERT INTO users (name, identification) VALUES (?, ?)', - ('Alice Brown', 'ID004')) -conn.commit() -conn.close() -print('User added!') -" -``` - -### Querying available tablets -``` -python3 -c " -import sqlite3 -conn = sqlite3.connect('tablets.db') -cursor = conn.cursor() -cursor.execute('SELECT brand, model, serial_number FROM tablets WHERE status = ?', ('available',)) -for row in cursor.fetchall(): - print(f'{row[0]} {row[1]} ({row[2]})') -conn.close() -" -``` - -## Web Version (Optional) - -If you want to use the web interface: - -1. Install Flask: -```bash -pip install flask -``` - -2. Run the web application: -```bash -python3 app.py -``` - -3. Open your browser to: http://localhost:5000 - -### Web Features - -- **Home**: View available tablets and active loans -- **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 - -To backup your data: -```bash -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 +Gestión de las Tablets del IES Schamann (préstamo y devolución). \ 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 deleted file mode 100644 index 02c80c4..0000000 --- a/RUNNING.md +++ /dev/null @@ -1,69 +0,0 @@ -# Tablet Management System - Running - -✅ **Status**: Application is running successfully! - -## Access Information - -- **Web Interface**: [http://localhost:5000](http://localhost:5000) -- **Port**: 5000 -- **Framework**: Flask 3.1.3 -- **Database**: SQLite (`tablets.db`) - -## How to Use - -### Web Interface -1. Open your browser to: [http://localhost:5000](http://localhost:5000) -2. Use the navigation menu to: - - **Home**: View available tablets and active loans - - **Add Tablet**: Add new tablets to inventory - - **Add User**: Register new users - - **Loan Tablet**: Loan a tablet to a user - - **Loan History**: View complete loan history - -### Command Line (Alternative) -If you prefer command line: -```bash -# Run the interactive application -python3 minimal_app.py - -# Run the test/demo -python3 test_app.py -``` - -## Current Data -The system already contains test data: -- **3 Tablets**: Samsung Galaxy Tab S7, Apple iPad Pro, Lenovo Tab P11 -- **3 Users**: John Doe, Jane Smith, Bob Johnson -- **2 Loans**: One active, one returned - -## Stopping the Server -To stop the Flask application: -```bash -pkill -f "python app.py" -``` - -## Technical Details -- **Backend**: SQLite 3 -- **Frontend**: Flask with HTML/CSS -- **Port**: 5000 (configurable in app.py) -- **Virtual Environment**: `.venv/` (created with uv) - -## Running Tests - -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 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 deleted file mode 100644 index a1a03e1..0000000 --- a/app.py +++ /dev/null @@ -1,1016 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple Tablet Lending and Return Management Web Application -with SQLite backend -""" - -from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session -import sqlite3 -import os -from datetime import datetime -from flask_babel import Babel, gettext, lazy_gettext - -app = Flask(__name__) -app.secret_key = 'your_secret_key_here' - -# Configure supported languages -app.config['BABEL_DEFAULT_LOCALE'] = 'es' -app.config['LANGUAGES'] = { - 'en': 'English', - 'es': 'Español' -} - -# Configure translation directory -app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations' - -# Locale selector function -def get_locale(): - # Try to get language from cookie - lang = request.cookies.get('language') - if lang and lang in app.config['LANGUAGES']: - return lang - # Try to get from session - if hasattr(request, 'session') and 'language' in request.session: - return request.session['language'] - # Default to Spanish (es) for internal app - return 'es' - - -# Before request handler to set language from URL param and persist to cookie/session -@app.before_request -def handle_language(): - lang = request.args.get('lang') - if lang and lang in app.config['LANGUAGES']: - # Store in session - session['language'] = lang - # Set cookie for longer persistence (1 year) - # Note: We need to use response.set_cookie, but before_request can't modify response - # So we'll handle this in a separate route - return None - - -# Language switcher endpoint -@app.route('/set_language') -def set_language(): - lang = request.args.get('lang') - if lang and lang in app.config['LANGUAGES']: - session['language'] = lang - # Set cookie - resp = make_response(redirect(request.referrer or '/')) - resp.set_cookie('language', lang, max_age=31536000) # 1 year - return resp - return redirect(request.referrer or '/') - -# Initialize Babel with locale selector -babel = Babel(app, locale_selector=get_locale) - -# Context processor to make language and config available in templates -@app.context_processor -def inject_global_variables(): - lang = get_locale() - return { - 'language': lang, - 'config': app.config - } - -# Database setup -DATABASE = 'tablets.db' - -def get_db(): - """Get database connection""" - conn = sqlite3.connect(DATABASE) - conn.row_factory = sqlite3.Row - return conn - -def init_db(): - """Initialize database with required tables""" - with get_db() as conn: - cursor = conn.cursor() - - # Create tablets table - 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 - ) - ''') - - # Create users table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - email TEXT, - phone TEXT, - identification TEXT UNIQUE - ) - ''') - - # Create loans table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS loans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tablet_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - loan_date TEXT NOT NULL, - return_date TEXT, - status TEXT DEFAULT 'active', - FOREIGN KEY (tablet_id) REFERENCES tablets (id), - FOREIGN KEY (user_id) REFERENCES users (id) - ) - ''') - - # Create students table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS students ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - order_number INTEGER, - cial_code TEXT UNIQUE NOT NULL, - nif_nie_passport TEXT UNIQUE, - registration_number INTEGER, - file_number TEXT, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - full_name TEXT NOT NULL, - birth_date TEXT, - gender TEXT CHECK(gender IN ('M', 'F', 'O')), - study_group TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT unique_identification UNIQUE (cial_code, nif_nie_passport) - ) - ''') - - # Create indexes for students - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_cial ON students(cial_code) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_nif ON students(nif_nie_passport) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_name ON students(last_name, first_name) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_birth_date ON students(birth_date) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_gender ON students(gender) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_students_group ON students(study_group) - ''') - - # 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, - assigned_to_student INTEGER, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (assigned_to_student) REFERENCES students(id) - ) - ''') - - # Create index for non_loanable_devices student relationship - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_non_loanable_student ON non_loanable_devices(assigned_to_student) - ''') - - conn.commit() - -@app.route('/') -def index(): - """Main page showing available tablets and active loans""" - with get_db() as conn: - cursor = conn.cursor() - - # Get available tablets - cursor.execute("SELECT * FROM tablets WHERE status = 'available'") - available_tablets = cursor.fetchall() - - # Get active loans - cursor.execute(''' - SELECT l.id, t.brand, t.model, t.serial_number, u.name, l.loan_date - FROM loans l - JOIN tablets t ON l.tablet_id = t.id - JOIN users u ON l.user_id = u.id - WHERE l.status = 'active' - ''') - active_loans = cursor.fetchall() - - return render_template('index.html', - available_tablets=available_tablets, - active_loans=active_loans) - -@app.route('/add_tablet', methods=['GET', 'POST']) -def add_tablet(): - """Add a new tablet to inventory""" - if request.method == 'POST': - brand = request.form['brand'] - model = request.form['model'] - serial_number = request.form['serial_number'] - notes = request.form['notes'] - - try: - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO tablets (brand, model, serial_number, status, notes) - VALUES (?, ?, ?, 'available', ?) - ''', (brand, model, serial_number, notes)) - conn.commit() - flash('Tablet added successfully!', 'success') - except sqlite3.IntegrityError: - flash('Error: Serial number already exists!', 'error') - - return redirect(url_for('index')) - - return render_template('add_tablet.html') - -@app.route('/add_user', methods=['GET', 'POST']) -def add_user(): - """Add a new user""" - if request.method == 'POST': - name = request.form['name'] - email = request.form['email'] - phone = request.form['phone'] - identification = request.form['identification'] - - try: - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO users (name, email, phone, identification) - VALUES (?, ?, ?, ?) - ''', (name, email, phone, identification)) - conn.commit() - flash('User added successfully!', 'success') - except sqlite3.IntegrityError: - flash('Error: Identification already exists!', 'error') - - return redirect(url_for('index')) - - return render_template('add_user.html') - -@app.route('/loan_tablet', methods=['GET', 'POST']) -def loan_tablet(): - """Loan a tablet to a user""" - if request.method == 'POST': - tablet_id = request.form['tablet_id'] - user_id = request.form['user_id'] - student_id = request.form.get('student_id') # Optional - can be loaned to student directly - - with get_db() as conn: - cursor = conn.cursor() - - # Update tablet status - cursor.execute("UPDATE tablets SET status = 'loaned' WHERE id = ?", (tablet_id,)) - - # Create loan record - loan_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - cursor.execute(''' - INSERT INTO loans (tablet_id, user_id, student_id, loan_date, status) - VALUES (?, ?, ?, ?, 'active') - ''', (tablet_id, user_id, student_id, loan_date)) - - conn.commit() - flash('Tablet loaned successfully!', 'success') - - return redirect(url_for('index')) - - # Get data for form - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM tablets WHERE status = 'available'") - available_tablets = cursor.fetchall() - cursor.execute("SELECT * FROM users") - users = cursor.fetchall() - cursor.execute("SELECT * FROM students ORDER BY last_name, first_name") - students = cursor.fetchall() - - return render_template('loan_tablet.html', - available_tablets=available_tablets, - users=users, students=students) - -@app.route('/students') -def list_students(): - """List all students with pagination and search""" - from flask import request - - # Get query parameters - page = request.args.get('page', 1, type=int) - search = request.args.get('search', '').strip() - gender_filter = request.args.get('gender', '') - group_filter = request.args.get('group', '') - - # Pagination settings - per_page = 50 - offset = (page - 1) * per_page - - with get_db() as conn: - cursor = conn.cursor() - - # Build query - query = "SELECT * FROM students" - conditions = [] - params = [] - - # Add search filter - if search: - conditions.append("(last_name LIKE ? OR first_name LIKE ? OR full_name LIKE ? OR cial_code LIKE ? OR nif_nie_passport LIKE ?)") - search_param = f"%{search}%" - params.extend([search_param] * 5) - - # Add gender filter - if gender_filter and gender_filter != 'all': - conditions.append("gender = ?") - params.append(gender_filter) - - # Add group filter - if group_filter and group_filter != 'all': - conditions.append("study_group LIKE ?") - params.append(f"%{group_filter}%") - - # Combine conditions - if conditions: - query += " WHERE " + " AND ".join(conditions) - - query += " ORDER BY last_name, first_name COLLATE NOCASE" - - # Get total count for pagination - count_query = f"SELECT COUNT(*) FROM students" - if conditions: - count_query += " WHERE " + " AND ".join(conditions) - cursor.execute(count_query, params) - total_students = cursor.fetchone()[0] - total_pages = (total_students + per_page - 1) // per_page - - # Add pagination to query - query += f" LIMIT {per_page} OFFSET {offset}" - - # Execute main query - cursor.execute(query, params) - students = cursor.fetchall() - - # Get distinct values for filters - cursor.execute("SELECT DISTINCT gender FROM students WHERE gender IS NOT NULL ORDER BY gender") - genders = [row[0] for row in cursor.fetchall()] - - cursor.execute("SELECT DISTINCT study_group FROM students WHERE study_group IS NOT NULL ORDER BY study_group") - groups = [row[0] for row in cursor.fetchall()] - - return render_template('students.html', - students=students, - page=page, - total_pages=total_pages, - total_students=total_students, - search=search, - gender=gender_filter, - group=group_filter, - genders=genders, - groups=groups, - per_page=per_page) - - -@app.route('/add_student', methods=['GET', 'POST']) -def add_student(): - """Add a new student""" - if request.method == 'POST': - order_number = request.form.get('order_number') - cial_code = request.form.get('cial_code', '').strip() - nif_nie_passport = request.form.get('nif_nie_passport', '').strip() - registration_number = request.form.get('registration_number') - file_number = request.form.get('file_number', '').strip() - first_name = request.form.get('first_name', '').strip() - last_name = request.form.get('last_name', '').strip() - full_name = f"{last_name}, {first_name}" if last_name and first_name else request.form.get('full_name', '').strip() - birth_date = request.form.get('birth_date', '').strip() - gender = request.form.get('gender', '').strip() - study_group = request.form.get('study_group', '').strip() - - # Validate required fields - if not cial_code: - flash('Error: CIAL code is required!', 'error') - return render_template('add_student.html') - - if not first_name: - flash('Error: First name is required!', 'error') - return render_template('add_student.html') - - if not last_name: - flash('Error: Last name is required!', 'error') - return render_template('add_student.html') - - # Validate gender - if gender and gender.upper() not in ['M', 'F', 'O']: - flash('Error: Invalid gender! Must be M, F, or O.', 'error') - return render_template('add_student.html') - - # Validate date format - if birth_date: - try: - datetime.strptime(birth_date, '%Y-%m-%d') - except ValueError: - flash('Error: Invalid date format! Use YYYY-MM-DD.', 'error') - return render_template('add_student.html') - - try: - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO students - (order_number, cial_code, nif_nie_passport, registration_number, file_number, - first_name, last_name, full_name, birth_date, gender, study_group, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ''', (order_number, cial_code, nif_nie_passport or None, registration_number, - file_number or None, first_name, last_name, full_name, - birth_date or None, gender or None, study_group or None)) - conn.commit() - flash('Student added successfully!', 'success') - return redirect(url_for('list_students')) - except sqlite3.IntegrityError as e: - if 'UNIQUE constraint failed: students.cial_code' in str(e): - flash('Error: CIAL code already exists!', 'error') - elif 'UNIQUE constraint failed: students.nif_nie_passport' in str(e): - flash('Error: NIF/NIE/Passport already exists!', 'error') - else: - flash(f'Error: {str(e)}', 'error') - - return render_template('add_student.html') - - -@app.route('/student/') -def student_detail(student_id): - """Show details for a specific student""" - with get_db() as conn: - cursor = conn.cursor() - - # Get student info - cursor.execute("SELECT * FROM students WHERE id = ?", (student_id,)) - student = cursor.fetchone() - - if not student: - flash('Error: Student not found!', 'error') - return redirect(url_for('list_students')) - - # Get tablets assigned to this student - cursor.execute(''' - SELECT t.* FROM tablets t - WHERE t.assigned_to_student = ? - ''', (student_id,)) - assigned_tablets = cursor.fetchall() - - # Get non-loanable devices assigned to this student - cursor.execute(''' - SELECT * FROM non_loanable_devices - WHERE assigned_to_student = ? - ''', (student_id,)) - assigned_devices = cursor.fetchall() - - # Get loan history for this student - cursor.execute(''' - SELECT l.*, t.brand, t.model, t.serial_number - FROM loans l - JOIN tablets t ON l.tablet_id = t.id - WHERE l.student_id = ? - ORDER BY l.loan_date DESC - ''', (student_id,)) - loans = cursor.fetchall() - - return render_template('student_detail.html', - student=student, - assigned_tablets=assigned_tablets, - assigned_devices=assigned_devices, - loans=loans) - - -@app.route('/edit_student/', methods=['GET', 'POST']) -def edit_student(student_id): - """Edit a student""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM students WHERE id = ?", (student_id,)) - student = cursor.fetchone() - - if not student: - flash('Error: Student not found!', 'error') - return redirect(url_for('list_students')) - - if request.method == 'POST': - order_number = request.form.get('order_number') - cial_code = request.form.get('cial_code', '').strip() - nif_nie_passport = request.form.get('nif_nie_passport', '').strip() - registration_number = request.form.get('registration_number') - file_number = request.form.get('file_number', '').strip() - first_name = request.form.get('first_name', '').strip() - last_name = request.form.get('last_name', '').strip() - full_name = f"{last_name}, {first_name}" if last_name and first_name else request.form.get('full_name', '').strip() - birth_date = request.form.get('birth_date', '').strip() - gender = request.form.get('gender', '').strip() - study_group = request.form.get('study_group', '').strip() - - # Validate required fields - if not cial_code: - flash('Error: CIAL code is required!', 'error') - return render_template('edit_student.html', student=student) - - if not first_name: - flash('Error: First name is required!', 'error') - return render_template('edit_student.html', student=student) - - if not last_name: - flash('Error: Last name is required!', 'error') - return render_template('edit_student.html', student=student) - - # Validate gender - if gender and gender.upper() not in ['M', 'F', 'O']: - flash('Error: Invalid gender! Must be M, F, or O.', 'error') - return render_template('edit_student.html', student=student) - - # Validate date format - if birth_date: - try: - datetime.strptime(birth_date, '%Y-%m-%d') - except ValueError: - flash('Error: Invalid date format! Use YYYY-MM-DD.', 'error') - return render_template('edit_student.html', student=student) - - try: - cursor.execute(''' - UPDATE students SET - order_number = ?, - cial_code = ?, - nif_nie_passport = ?, - registration_number = ?, - file_number = ?, - first_name = ?, - last_name = ?, - full_name = ?, - birth_date = ?, - gender = ?, - study_group = ?, - updated_at = CURRENT_TIMESTAMP - WHERE id = ? - ''', (order_number, cial_code, nif_nie_passport or None, registration_number, - file_number or None, first_name, last_name, full_name, - birth_date or None, gender or None, study_group or None, student_id)) - conn.commit() - flash('Student updated successfully!', 'success') - return redirect(url_for('student_detail', student_id=student_id)) - except sqlite3.IntegrityError as e: - if 'UNIQUE constraint failed: students.cial_code' in str(e): - flash('Error: CIAL code already exists!', 'error') - elif 'UNIQUE constraint failed: students.nif_nie_passport' in str(e): - flash('Error: NIF/NIE/Passport already exists!', 'error') - else: - flash(f'Error: {str(e)}', 'error') - - return render_template('edit_student.html', student=student) - - -@app.route('/delete_student/') -def delete_student(student_id): - """Delete a student""" - with get_db() as conn: - cursor = conn.cursor() - - # Check if student has active loans or assigned devices - cursor.execute("SELECT COUNT(*) FROM loans WHERE student_id = ? AND status = 'active'", (student_id,)) - active_loans = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*) FROM tablets WHERE assigned_to_student = ?", (student_id,)) - assigned_tablets = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*) FROM non_loanable_devices WHERE assigned_to_student = ?", (student_id,)) - assigned_devices = cursor.fetchone()[0] - - if active_loans > 0 or assigned_tablets > 0 or assigned_devices > 0: - flash('Error: Cannot delete student with active loans or assigned devices!', 'error') - return redirect(url_for('student_detail', student_id=student_id)) - - cursor.execute("DELETE FROM students WHERE id = ?", (student_id,)) - conn.commit() - flash('Student deleted successfully!', 'success') - - return redirect(url_for('list_students')) - - -@app.route('/import_students', methods=['GET', 'POST']) -def import_students(): - """Import students from CSV file""" - if request.method == 'POST': - # Check if file was uploaded - if 'csv_file' not in request.files: - flash('Error: No file uploaded!', 'error') - return redirect(url_for('import_students')) - - file = request.files['csv_file'] - - if file.filename == '': - flash('Error: No file selected!', 'error') - return redirect(url_for('import_students')) - - if not file.filename.lower().endswith('.csv'): - flash('Error: Please upload a CSV file!', 'error') - return redirect(url_for('import_students')) - - # Save uploaded file temporarily - temp_filename = file.filename if file.filename else 'import.csv' - temp_path = os.path.join('/tmp', temp_filename) - file.save(temp_path) - - # Run import script - import subprocess - result = subprocess.run( - ['python3', 'scripts/import_students.py', temp_path, '--database', DATABASE], - capture_output=True, - text=True, - cwd=os.path.dirname(os.path.abspath(__file__)) - ) - - # Clean up temp file - if os.path.exists(temp_path): - os.remove(temp_path) - - if result.returncode == 0: - flash('Students imported successfully!', 'success') - else: - flash(f'Error importing students: {result.stderr}', 'error') - - return redirect(url_for('list_students')) - - return render_template('import_students.html') - - -@app.route('/return_tablet/') -def return_tablet(loan_id): - """Return a loaned tablet""" - with get_db() as conn: - cursor = conn.cursor() - - # Get loan information - cursor.execute("SELECT tablet_id FROM loans WHERE id = ?", (loan_id,)) - loan = cursor.fetchone() - - if loan: - tablet_id = loan['tablet_id'] - - # Update loan status and return date - return_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - cursor.execute(''' - UPDATE loans SET status = 'returned', return_date = ? WHERE id = ? - ''', (return_date, loan_id)) - - # Update tablet status - cursor.execute("UPDATE tablets SET status = 'available' WHERE id = ?", (tablet_id,)) - - conn.commit() - flash('Tablet returned successfully!', 'success') - else: - flash('Error: Loan not found!', 'error') - - return redirect(url_for('index')) - -@app.route('/history') -def history(): - """Show loan history""" - with get_db() as conn: - cursor = conn.cursor() - - cursor.execute(''' - SELECT l.id, t.brand, t.model, t.serial_number, u.name, - l.loan_date, l.return_date, l.status - FROM loans l - JOIN tablets t ON l.tablet_id = t.id - JOIN users u ON l.user_id = u.id - ORDER BY l.loan_date DESC - ''') - loans = cursor.fetchall() - - return render_template('history.html', loans=loans) - - -@app.route('/project_management') -def project_management(): - """Project management page with markdown editor for development notes""" - return render_template('project_management.html') - - -@app.route('/get_notes/') -def get_notes(filename): - """Serve notes file content""" - try: - with open(filename, 'r', encoding='utf-8') as f: - content = f.read() - return content, 200, { - 'Content-Type': 'text/markdown; charset=utf-8', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0' - } - except FileNotFoundError: - return '', 404 - except Exception as e: - return str(e), 500 - - -@app.route('/save_notes', methods=['POST']) -def save_notes(): - """Save markdown notes to file""" - data = request.get_json() - content = data.get('content', '') - filename = data.get('filename', 'notes_development/project_notes.md') - - try: - # Ensure directory exists - os.makedirs(os.path.dirname(filename), exist_ok=True) - - # Write content to file - with open(filename, 'w', encoding='utf-8') as f: - f.write(content) - - return {'status': 'success', 'message': 'Notes saved successfully'} - except Exception as e: - return {'status': 'error', 'message': str(e)}, 500 - - -@app.route('/user_loans') -def user_loans(): - """ - Show all users with their loan information. - Supports search, filtering, and pagination via HTMX. - """ - from flask import request - - # Get query parameters - page = request.args.get('page', 1, type=int) - search = request.args.get('search', '').strip() - status_filter = request.args.get('status', '') - - # Pagination settings - per_page = 20 - offset = (page - 1) * per_page - - with get_db() as conn: - cursor = conn.cursor() - - # Build base query for users with loan counts - query = """ - SELECT - u.id, u.name, u.identification, u.email, u.phone, - COUNT(l.id) as loan_count, - MAX(l.loan_date) as last_loan_date - FROM users u - LEFT JOIN loans l ON u.id = l.user_id AND l.status = 'active' - """ - - conditions = [] - params = [] - having_conditions = [] - - # Add search filter - if search: - conditions.append("(u.name LIKE ? OR u.identification LIKE ? OR u.email LIKE ?)") - search_param = f"%{search}%" - params.extend([search_param, search_param, search_param]) - - # Add status filter (use HAVING for aggregate functions) - if status_filter == 'with_loans': - having_conditions.append("COUNT(l.id) > 0") - elif status_filter == 'no_loans': - having_conditions.append("COUNT(l.id) = 0") - - # Combine conditions for WHERE clause - where_clause = "" - if conditions: - where_clause = " WHERE " + " AND ".join(conditions) - - # Combine HAVING conditions - having_clause = "" - if having_conditions: - having_clause = " HAVING " + " AND ".join(having_conditions) - - # Group by and order - group_by = " GROUP BY u.id, u.name, u.identification, u.email, u.phone" - order_by = " ORDER BY u.name COLLATE NOCASE" - - # Get total count for pagination - # We need to count distinct users matching the criteria - count_query = f"SELECT COUNT(DISTINCT u.id) FROM users u LEFT JOIN loans l ON u.id = l.user_id AND l.status = 'active'{where_clause}{having_clause}" - cursor.execute(count_query, params) - result = cursor.fetchone() - total_users = result[0] if result else 0 - total_pages = (total_users + per_page - 1) // per_page - - # Build final query with pagination - final_query = query + where_clause + group_by + having_clause + order_by + f" LIMIT {per_page} OFFSET {offset}" - - # Execute main query - cursor.execute(final_query, params) - users = cursor.fetchall() - - # Convert to list of dicts for template - users_list = [] - for user in users: - users_list.append({ - 'id': user[0], - 'name': user[1], - 'identification': user[2], - 'email': user[3], - 'phone': user[4], - 'loan_count': user[5], - 'last_loan_date': user[6] - }) - - # Check if this is an HTMX request - is_htmx = request.headers.get('HX-Request') == 'true' - - if is_htmx: - # Return just the results partial for HTMX swap - return render_template('components/user_loans_results.html', - users=users_list, - page=page, - total_pages=total_pages, - total_users=total_users, - search=search, - status=status_filter, - per_page=per_page) - else: - # Full page render - return render_template('user_loans.html', - users=users_list, - page=page, - total_pages=total_pages, - total_users=total_users, - search=search, - status=status_filter, - per_page=per_page) - - - - -@app.route('/user_loans/') -def user_loans_detail(user_id): - """ - Show detailed loan history for a specific user. - """ - with get_db() as conn: - cursor = conn.cursor() - - # Get user info - cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) - user = cursor.fetchone() - - if not user: - flash('User not found', 'error') - return redirect(url_for('user_loans')) - - # Get all loans for this user - cursor.execute(''' - SELECT l.id, l.tablet_id, l.loan_date, l.return_date, l.status, - t.brand, t.model, t.serial_number, t.status as tablet_status - FROM loans l - JOIN tablets t ON l.tablet_id = t.id - WHERE l.user_id = ? - ORDER BY l.loan_date DESC - ''', (user_id,)) - loans = cursor.fetchall() - - # Get active loans count - cursor.execute(''' - SELECT COUNT(*) FROM loans - WHERE user_id = ? AND status = 'active' - ''', (user_id,)) - active_count = cursor.fetchone()[0] - - # Get returned loans count - cursor.execute(''' - SELECT COUNT(*) FROM loans - WHERE user_id = ? AND status = 'returned' - ''', (user_id,)) - returned_count = cursor.fetchone()[0] - - return render_template('user_loans_detail.html', - user=user, - loans=loans, - active_count=active_count, - returned_count=returned_count) - - - -@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() - - # Create templates directory if it doesn't exist - if not os.path.exists('templates'): - os.makedirs('templates') - - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/assets/Schamann.png b/assets/Schamann.png deleted file mode 100644 index 8292d12..0000000 Binary files a/assets/Schamann.png and /dev/null differ diff --git a/basic_server.py b/basic_server.py deleted file mode 100644 index b2731e4..0000000 --- a/basic_server.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -""" -Basic HTTP server to serve the tablet management system -""" - -from http.server import SimpleHTTPRequestHandler, HTTPServer -import os - -class MyHandler(SimpleHTTPRequestHandler): - def do_GET(self): - if self.path == '/': - self.path = '/index.html' - return super().do_GET() - -def run_server(): - port = 8080 - server_address = ('', port) - httpd = HTTPServer(server_address, MyHandler) - - # Create a simple index file if it doesn't exist - if not os.path.exists('index.html'): - with open('index.html', 'w') as f: - f.write(''' - - - Tablet Management System - - - -

Tablet Management System

- -
-

Welcome to the Tablet Management System!

-

This system is running with a SQLite backend.

- -

Available Commands:

-
    -
  • Test the system: python3 test_app.py
  • -
  • Run interactive mode: python3 minimal_app.py
  • -
  • Check database: sqlite3 tablets.db
  • -
- -

Current Status:

-

✓ Database: tablets.db

-

✓ Web server: Running on port 8080

-

✓ System: Ready for use

-
- -

Quick Test Results:

-
Running tests...
- - - -''') - - print(f"Server running on http://localhost:{port}") - print("Serving current directory") - print("Press Ctrl+C to stop the server") - - try: - httpd.serve_forever() - except KeyboardInterrupt: - print("\nServer stopped") - -if __name__ == '__main__': - run_server() \ No newline at end of file diff --git a/check_flask.py b/check_flask.py deleted file mode 100644 index 6ea9046..0000000 --- a/check_flask.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -""" -Check if Flask is available and provide installation instructions -""" - -try: - import flask - print("✓ Flask is available!") - print("You can run the web version:") - print(" python3 simple_app.py") - print("Then open: http://localhost:8000") -except ImportError: - print("✗ Flask is not installed") - print("\nTo install Flask:") - print(" pip install flask") - print("\nOr use the system package manager:") - print(" sudo apt install python3-flask") - print("\nYou can still use the command-line version:") - print(" python3 minimal_app.py") - print(" python3 test_app.py") \ No newline at end of file diff --git a/compile_translations.py b/compile_translations.py deleted file mode 100644 index 4f165ba..0000000 --- a/compile_translations.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -""" -Compile .po files to .mo files for Flask-Babel -""" -import os -from pathlib import Path - -def compile_translations(): - translations_dir = Path('translations') - - for lang_dir in translations_dir.iterdir(): - if lang_dir.is_dir(): - lc_messages_dir = lang_dir / 'LC_MESSAGES' - if lc_messages_dir.exists(): - for po_file in lc_messages_dir.glob('*.po'): - mo_file = lc_messages_dir / f"{po_file.stem}.mo" - print(f"Compiling {po_file} -> {mo_file}") - # Use msgfmt to compile - os.system(f"msgfmt -o {mo_file} {po_file}") - - print("✅ All translations compiled!") - -if __name__ == '__main__': - compile_translations() diff --git a/debug_server.py b/debug_server.py deleted file mode 100644 index 7645070..0000000 --- a/debug_server.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -from http.server import HTTPServer, BaseHTTPRequestHandler - -class DebugHandler(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - self.wfile.write(b'Hello from Tablet Management Server!') - -def run(): - try: - server = HTTPServer(('', 8000), DebugHandler) - print("Debug server running on port 8000") - server.serve_forever() - except Exception as e: - print(f"Error: {e}") - -if __name__ == '__main__': - run() \ No newline at end of file diff --git a/docs/CLI_VS_WEB.md b/docs/CLI_VS_WEB.md deleted file mode 100644 index f9eb00c..0000000 --- a/docs/CLI_VS_WEB.md +++ /dev/null @@ -1,522 +0,0 @@ -# CLI vs Web Functionality Comparison - -## ⚠️ DEPRECATION NOTICE - -**The CLI version (`minimal_app.py`) is now DEPRECATED.** - -Please use the **web interface (`app.py`)** for all operations. The web interface provides: -- Complete feature set -- Full database schema support -- Responsive mobile interface -- Better user experience - -The CLI will be removed in a future version. This document is kept for historical reference. - ---- - -## Overview - -This document compares the functionality between the **DEPRECATED CLI version** (`minimal_app.py`) and the **Web version** (`app.py`) of the Tablet Management System. - -## Current State - -### Web Version (app.py) - ✅ RECOMMENDED - -The web version is the **primary and recommended** interface for all users. - -| Feature | Status | Route | Template | -|---------|--------|-------|---------| -| Add Tablet | ✅ | `/add_tablet` | `add_tablet.html` | -| Add User | ✅ | `/add_user` | `add_user.html` | -| Loan Tablet | ✅ | `/loan_tablet` | `loan_tablet.html` | -| Return Tablet | ✅ | `/return_tablet/` | N/A (redirects) | -| Show Available Tablets | ✅ | `/` (index) | `index.html` | -| Show Active Loans | ✅ | `/` (index) | `index.html` | -| Loan History | ✅ | `/history` | `history.html` | -| User Loans | ✅ | `/user_loans` | `user_loans.html` | -| Add Non-Loanable Device | ✅ | `/add_non_loanable_device` | `add_non_loanable_device.html` | -| Show Non-Loanable Devices | ✅ | `/non_loanable_devices` | `non_loanable_devices.html` | -| Edit Non-Loanable Device | ✅ | `/edit_non_loanable_device/` | `edit_non_loanable_device.html` | -| Delete Non-Loanable Device | ✅ | `/delete_non_loanable_device/` | N/A (redirects) | -| Project Management | ✅ | `/project_management` | `project_management.html` | - -### CLI Version (minimal_app.py) - ❌ DEPRECATED - -**This CLI is deprecated and should not be used for new development.** - -The CLI has limited functionality and does not support all database columns used by the web version. - -| Feature | Status | Function | -|---------|--------|----------| -| Add Tablet | ⚠️ Partial | `add_tablet()` - Missing notes field | -| Add User | ⚠️ Partial | `add_user()` - Missing email/phone | -| Loan Tablet | ✅ | `loan_tablet()` | -| Return Tablet | ✅ | `return_tablet()` | -| Show Available Tablets | ✅ | `show_available_tablets()` | -| Show Active Loans | ✅ | `show_active_loans()` | -| Loan History | ✅ | `show_loan_history()` | -| Add Non-Loanable Device | ✅ | `add_non_loanable_device()` | -| Show Non-Loanable Devices | ✅ | `show_non_loanable_devices()` | -| Delete Non-Loanable Device | ✅ | `delete_non_loanable_device()` | -| Show Users | ✅ | `show_users()` | -| **User Loans** | ❌ Missing | N/A | -| **Edit Non-Loanable Device** | ❌ Missing | N/A | -| **Project Management** | ❌ Missing | N/A | - -## Migration Path - -### For Existing CLI Users - -**Stop using the CLI and switch to the web interface:** - -1. Run the web interface: - ```bash - python3 app.py - ``` - -2. Open your browser to: http://localhost:5000 - -3. The web interface uses the **same database** (`tablets.db`), so all your data is preserved. - -### Database Compatibility - -The CLI's database schema is **missing some columns** that the web version uses: -- `tablets.notes` - Added by web version -- `users.email` - Added by web version -- `users.phone` - Added by web version - -**If you've only used the CLI:** Your database is missing these columns. The web interface will still work, but won't be able to store notes, email, or phone until the columns are added. - -**To fix the database schema:** -```bash -# Run the web interface - it will add missing columns automatically -python3 app.py -``` - -The web version's `init_db()` will add any missing columns when it runs. - -## Recommendation - -**Use `app.py` (web interface) for all operations.** - -The CLI (`minimal_app.py`) is deprecated and will be removed in a future version. All development and maintenance efforts should focus on the web interface. - - -## Missing Features in CLI - -### 1. User Management -**Web:** Supports name, email, phone, identification -**CLI:** Only supports name, identification - -**Missing:** -- Email field -- Phone field - -### 2. Tablet Management -**Web:** Supports brand, model, serial_number, notes -**CLI:** Supports brand, model, serial_number - -**Missing:** -- Notes field - -### 3. Non-Loanable Devices -**Web:** Full CRUD (Create, Read, Update, Delete) -**CLI:** Only Create, Read, Delete - -**Missing:** -- Edit functionality - -### 4. Additional Features -**Web:** Has these features -**CLI:** Missing - -- User Loans page (shows loans by user with search) -- Project Management (markdown notes editor) -- Flash messages (CLI uses print, which is fine) - -## Database Schema Comparison - -### Web Version Schema (app.py) - -```sql --- tablets -CREATE TABLE 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 -) - --- users -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - email TEXT, - phone TEXT, - identification TEXT UNIQUE -) - --- loans -CREATE TABLE loans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tablet_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - loan_date TEXT NOT NULL, - return_date TEXT, - status TEXT DEFAULT 'active', - FOREIGN KEY (tablet_id) REFERENCES tablets (id), - FOREIGN KEY (user_id) REFERENCES users (id) -) - --- non_loanable_devices -CREATE TABLE 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 -) -``` - -### CLI Version Schema (minimal_app.py) - -```sql --- tablets -CREATE TABLE tablets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - brand TEXT NOT NULL, - model TEXT NOT NULL, - serial_number TEXT UNIQUE NOT NULL, - status TEXT DEFAULT 'available' - -- MISSING: notes TEXT -) - --- users -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - identification TEXT UNIQUE - -- MISSING: email TEXT, phone TEXT -) - --- loans -CREATE TABLE loans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tablet_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - loan_date TEXT NOT NULL, - return_date TEXT, - status TEXT DEFAULT 'active', - FOREIGN KEY (tablet_id) REFERENCES tablets (id), - FOREIGN KEY (user_id) REFERENCES users (id) -) - --- non_loanable_devices -CREATE TABLE 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 -) -``` - -**Schema Differences:** -- `tablets` table: CLI missing `notes` column -- `users` table: CLI missing `email` and `phone` columns - -## Recommendations - -### Option 1: Update CLI to Match Web (Recommended) - -Update `minimal_app.py` to: -1. Add `notes` field to tablets -2. Add `email` and `phone` fields to users -3. Add edit functionality for non-loanable devices -4. Add user loans view -5. Add project management (optional) - -**Pros:** -- CLI has full feature parity with web -- Same database schema -- Users can use either interface - -**Cons:** -- More complex CLI -- May not be needed if web is primary interface - -### Option 2: Keep CLI Minimal (Current State) - -Leave CLI as-is for basic operations only. - -**Pros:** -- Simple, focused CLI -- Less code to maintain - -**Cons:** -- Database schema mismatch -- Users can't access all features via CLI -- Confusing for users who expect same functionality - -### Option 3: Create Separate Database (Not Recommended) - -Use different databases for CLI and web. - -**Pros:** -- Each can have optimized schema - -**Cons:** -- Data duplication -- Sync issues -- Confusing for users - -## Suggested Action Plan - -### Priority 1: Fix Database Schema Mismatch - -The CLI's `init_db()` creates tables without `notes`, `email`, and `phone` columns, but the web version expects them. This can cause issues. - -**Solution:** Update `minimal_app.py` `init_db()` to match `app.py` schema. - -### Priority 2: Add Missing Fields to CLI Functions - -1. Update `add_tablet()` to accept and store `notes` -2. Update `add_user()` to accept and store `email` and `phone` -3. Update `show_available_tablets()` and `show_non_loanable_devices()` to display all fields - -### Priority 3: Add Missing Features (Optional) - -1. Add `edit_non_loanable_device()` function -2. Add `show_user_loans()` function -3. Consider adding project management (lower priority) - -## Code Changes Required - -### 1. Update init_db() in minimal_app.py - -```python -def init_db(): - """Initialize database with required tables - MATCH WEB VERSION""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - # Create tablets table - ADD NOTES COLUMN - 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 - ) - ''') - - # Create users table - ADD EMAIL AND PHONE COLUMNS - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - email TEXT, - phone TEXT, - identification TEXT UNIQUE - ) - ''') - - # loans and non_loanable_devices are already correct - # ... rest of init_db -``` - -### 2. Update add_tablet() Function - -```python -def add_tablet(brand, model, serial_number, notes=''): - """Add a new tablet to inventory""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - try: - cursor.execute(''' - INSERT INTO tablets (brand, model, serial_number, status, notes) - VALUES (?, ?, ?, 'available', ?) - ''', (brand, model, serial_number, notes)) - conn.commit() - print(f"✓ Added tablet: {brand} {model} ({serial_number})") - if notes: - print(f" Notes: {notes}") - except sqlite3.IntegrityError: - print(f"✗ Error: Serial number {serial_number} already exists") - finally: - conn.close() -``` - -### 3. Update add_user() Function - -```python -def add_user(name, identification, email='', phone=''): - """Add a new user""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - try: - cursor.execute(''' - INSERT INTO users (name, email, phone, identification) - VALUES (?, ?, ?, ?) - ''', (name, email, phone, identification)) - conn.commit() - print(f"✓ Added user: {name} ({identification})") - if email: - print(f" Email: {email}") - if phone: - print(f" Phone: {phone}") - except sqlite3.IntegrityError: - print(f"✗ Error: Identification {identification} already exists") - finally: - conn.close() -``` - -### 4. Update show_available_tablets() to Display Notes - -```python -def show_available_tablets(): - """Show available tablets""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - cursor.execute("SELECT id, brand, model, serial_number, notes FROM tablets WHERE status = 'available'") - tablets = cursor.fetchall() - - print("\n=== Available Tablets ===") - if tablets: - for tablet in tablets: - notes = f" | Notes: {tablet[4]}" if tablet[4] else "" - print(f"ID: {tablet[0]}, {tablet[1]} {tablet[2]} ({tablet[3]}){notes}") - else: - print("No available tablets") - - conn.close() -``` - -### 5. Add Missing Functions - -#### Edit Non-Loanable Device - -```python -def edit_non_loanable_device(device_id, **kwargs): - """Edit a non-loanable device""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - # Build update query dynamically - updates = [] - params = [] - for key, value in kwargs.items(): - if value is not None: - updates.append(f"{key} = ?") - params.append(value) - - if not updates: - print("✗ No fields to update") - conn.close() - return - - params.append(device_id) - query = f"UPDATE non_loanable_devices SET {', '.join(updates)} WHERE id = ?" - - cursor.execute(query, params) - conn.commit() - - if cursor.rowcount > 0: - print(f"✓ Non-loanable device {device_id} updated") - else: - print(f"✗ Error: Device {device_id} not found") - - conn.close() -``` - -#### Show User Loans - -```python -def show_user_loans(): - """Show loans grouped by user""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - # Get all users with their loans - cursor.execute(''' - SELECT u.id, u.name, u.identification, - GROUP_CONCAT(l.id, ",") as loan_ids, - COUNT(l.id) as loan_count - FROM users u - LEFT JOIN loans l ON u.id = l.user_id - GROUP BY u.id, u.name, u.identification - ''') - users = cursor.fetchall() - - print("\n=== User Loans ===") - if users: - for user in users: - print(f"\nUser: {user[1]} ({user[2]}) - {user[3]} loans") - - # Get loans for this user - cursor.execute(''' - SELECT l.id, t.brand, t.model, t.serial_number, l.loan_date, l.return_date, l.status - FROM loans l - JOIN tablets t ON l.tablet_id = t.id - WHERE l.user_id = ? - ORDER BY l.loan_date DESC - ''', (user[0],)) - loans = cursor.fetchall() - - for loan in loans: - return_date = loan[5] or 'Not returned' - print(f" Loan {loan[0]}: {loan[1]} {loan[2]} ({loan[3]})") - print(f" Loan Date: {loan[4]}, Return Date: {return_date}, Status: {loan[6]}") - else: - print("No users found") - - conn.close() -``` - -## Migration Strategy - -If you want to update the CLI to match the web version: - -1. **Backup current database** - ```bash - cp tablets.db tablets.db.backup - ``` - -2. **Update minimal_app.py** with the changes above - -3. **Run updated CLI** - ```bash - python3 minimal_app.py - ``` - -4. **Test all functionality** - -5. **If database schema changed**, you may need to: - - Drop and recreate tables (if starting fresh) - - Or add missing columns with ALTER TABLE (if preserving data) - -## Conclusion - -The CLI version is **lagging behind** the web version in terms of: -- Database schema (missing columns) -- Feature completeness (missing functions) -- Field support (missing email, phone, notes) - -**Recommendation:** Update the CLI to match the web version's database schema and functionality to ensure consistency and avoid confusion for users who may use both interfaces. diff --git a/docs/CORPORATE_DESIGN.md b/docs/CORPORATE_DESIGN.md deleted file mode 100644 index eae6bed..0000000 --- a/docs/CORPORATE_DESIGN.md +++ /dev/null @@ -1,176 +0,0 @@ -# Corporate Design Specifications - -## Overview - -This document defines the corporate design specifications for the Tablet Management System, including color palette, logo usage, and branding guidelines. - ---- - -## Color Palette - -### Primary Corporate Colors - -The following three colors constitute the official corporate color palette: - -| Color Name | Hex Code | RGB | Usage | -|------------|----------|-----|-------| -| **Cobalt Blue** | `#1338BE` | rgb(19, 56, 190) | Primary brand color, headers, navigation, main actions | -| **Tiger Orange** | `#FC6A03` | rgb(252, 106, 3) | Accents, highlights, loading indicators, secondary actions | -| **Emerald Green** | `#028A0F` | rgb(2, 138, 15) | Success states, positive feedback, confirmation | - -### Color Shades - -Each corporate color has been extended with a full shade range (50-900) for use in Tailwind CSS: - -#### Cobalt Blue Shades -- `cobalt-50`: `#f0f5ff` (Lightest) -- `cobalt-100`: `#e0eaff` -- `cobalt-200`: `#b3c6ff` -- `cobalt-300`: `#80a0ff` -- `cobalt-400`: `#4d7aff` -- **`cobalt-500`: `#1338BE`** (Primary) -- `cobalt-600`: `#0f2a9c` -- `cobalt-700`: `#0b1f72` -- `cobalt-800`: `#081552` -- `cobalt-900`: `#060f38` (Darkest) - -#### Tiger Orange Shades -- `tiger-50`: `#fff8f0` (Lightest) -- `tiger-100`: `#ffe8d6` -- `tiger-200`: `#ffccab` -- `tiger-300`: `#ffae80` -- `tiger-400`: `#ff8c55` -- **`tiger-500`: `#FC6A03`** (Primary) -- `tiger-600`: `#e05a00` -- `tiger-700`: `#b84500` -- `tiger-800`: `#8c3300` -- `tiger-900`: `#662500` (Darkest) - -#### Emerald Green Shades -- `emerald-50`: `#f0fdf4` (Lightest) -- `emerald-100`: `#dcfce7` -- `emerald-200`: `#bbf7d0` -- `emerald-300`: `#86efac` -- `emerald-400`: `#4ade80` -- **`emerald-500`: `#028A0F`** (Primary) -- `emerald-600`: `#006b0c` -- `emerald-700`: `#005209` -- `emerald-800`: `#003d07` -- `emerald-900`: `#002904` (Darkest) - ---- - -## Logo Usage - -### Logo File -- **File**: `assets/Schamann.png` -- **Dimensions**: 175 x 161 pixels -- **Format**: PNG with transparency -- **Web Path**: `/static/Schamann.png` - -### Display Guidelines -- The logo should be displayed at a fixed height of 40px in the header -- Use `object-contain` to maintain aspect ratio -- The logo should appear to the left of the application title -- Do not stretch or distort the logo - -### Implementation -```html -Corporate Logo -``` - ---- - -## Usage Examples - -### Headers and Navigation -```html - -
- - -