Merge feature/user-loans-search: Non-loanable device management
This commit is contained in:
commit
3039363b6b
12 changed files with 937 additions and 38 deletions
21
README.md
21
README.md
|
|
@ -10,15 +10,16 @@ A simple SQLite-based system for managing tablet lending and returns.
|
||||||
- **Return Management**: Record when tablets are returned
|
- **Return Management**: Record when tablets are returned
|
||||||
- **History Tracking**: Complete history of all loans and returns
|
- **History Tracking**: Complete history of all loans and returns
|
||||||
- **User Loans View**: See all tablets loaned to each user (demonstrates one-to-many relationship)
|
- **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
|
- **Project Notes**: Markdown editor for development documentation
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `minimal_app.py` - Interactive command-line application
|
- `minimal_app.py` - Interactive command-line application (includes non-loanable device management)
|
||||||
- `test_app.py` - Test script that demonstrates functionality
|
- `test_app.py` - Test script that demonstrates functionality
|
||||||
- `tablets.db` - SQLite database (created automatically)
|
- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table)
|
||||||
- `simple_app.py` - Web-based version (requires Flask)
|
- `simple_app.py` - Web-based version (requires Flask, includes non-loanable device management)
|
||||||
- `app.py` - Alternative web version (requires Flask)
|
- `app.py` - Alternative web version (requires Flask, includes non-loanable device management)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -144,6 +145,7 @@ python3 app.py
|
||||||
- **Loan Tablet**: Loan a tablet to a user (with search for large datasets)
|
- **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)
|
- **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
|
- **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`)
|
- **Project Management**: Markdown editor for development notes (saved to `notes_development/project_notes.md`)
|
||||||
|
|
||||||
## Database Backup
|
## Database Backup
|
||||||
|
|
@ -153,6 +155,17 @@ To backup your data:
|
||||||
cp tablets.db tablets_backup_$(date +%Y%m%d).db
|
cp tablets.db tablets_backup_$(date +%Y%m%d).db
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Version Control
|
||||||
|
|
||||||
|
This project uses Git for version control with the following structure:
|
||||||
|
|
||||||
|
- `master` - Production-ready releases
|
||||||
|
- `develop` - Integration branch for features
|
||||||
|
- `feature/*` - Individual feature branches
|
||||||
|
- `hotfix/*` - Urgent bug fixes
|
||||||
|
|
||||||
|
See `CONTRIBUTING.md` for detailed workflow and commit conventions.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This is a simple educational project. Feel free to use and modify it as needed.
|
This is a simple educational project. Feel free to use and modify it as needed.
|
||||||
221
REQUIREMENTS.md
Normal file
221
REQUIREMENTS.md
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
# Requisitos del Sistema de Gestión de Préstamos de Tablets
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
El sistema debe permitir la gestión del préstamo y devolución de dispositivos tablets entre un inventario y un conjunto de usuarios. El enfoque principal es el seguimiento del estado de cada dispositivo y su relación con los usuarios que los han tomado prestados.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Requisitos Faltantes
|
||||||
|
|
||||||
|
### Gestión de dispositivos no prestables
|
||||||
|
|
||||||
|
✅ **IMPLEMENTADO** - El sistema ahora permite:
|
||||||
|
- Registrar nuevos dispositivos no prestables en un inventario separado (tabla `non_loanable_devices`)
|
||||||
|
- Gestionar dispositivos no prestables a través de:
|
||||||
|
- Interfaz web (app.py): /non_loanable_devices, /add_non_loanable_device, /edit_non_loanable_device, /delete_non_loanable_device
|
||||||
|
- Interfaz CLI (minimal_app.py): Opciones 8, 9, 10 del menú
|
||||||
|
- La implementación utiliza una tabla separada en la misma base de datos con los siguientes campos:
|
||||||
|
- brand, model, serial_number (único), device_type, location, status, notes, purchase_date, purchase_cost
|
||||||
|
|
||||||
|
## Requisitos Funcionales
|
||||||
|
|
||||||
|
### Gestión de Dispositivos
|
||||||
|
|
||||||
|
El sistema debe permitir:
|
||||||
|
- Registrar nuevos dispositivos en el inventario
|
||||||
|
- Identificar cada dispositivo de manera única
|
||||||
|
- Almacenar información descriptiva de cada dispositivo (marca, modelo, número de serie)
|
||||||
|
- Consultar el estado actual de cada dispositivo (disponible, prestado)
|
||||||
|
|
||||||
|
### Gestión de Usuarios
|
||||||
|
|
||||||
|
El sistema debe permitir:
|
||||||
|
- Registrar nuevos usuarios
|
||||||
|
- Identificar cada usuario de manera única
|
||||||
|
- Almacenar información de contacto básica (nombre, identificación)
|
||||||
|
- Consultar la información de los usuarios registrados
|
||||||
|
|
||||||
|
### Gestión de Préstamos
|
||||||
|
|
||||||
|
El sistema debe permitir:
|
||||||
|
- Asignar un dispositivo disponible a un usuario
|
||||||
|
- Registrar la fecha y hora del préstamo
|
||||||
|
- Registrar la fecha y hora de la devolución
|
||||||
|
- Consultar el historial de préstamos de un dispositivo
|
||||||
|
- Consultar el historial de préstamos de un usuario
|
||||||
|
- Visualizar todos los dispositivos actualmente prestados
|
||||||
|
|
||||||
|
### Consulta y Reportes
|
||||||
|
|
||||||
|
El sistema debe permitir:
|
||||||
|
- Buscar dispositivos por cualquier atributo (marca, modelo, número de serie)
|
||||||
|
- Buscar usuarios por cualquier atributo (nombre, identificación)
|
||||||
|
- Filtrar préstamos por estado (activos, finalizados)
|
||||||
|
- Filtrar préstamos por dispositivo
|
||||||
|
- Filtrar préstamos por usuario
|
||||||
|
- Ver el historial completo de préstamos y devoluciones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modelo de Datos
|
||||||
|
|
||||||
|
### Entidades Principales
|
||||||
|
|
||||||
|
1. **Dispositivo**
|
||||||
|
- Identificador único
|
||||||
|
- Marca
|
||||||
|
- Modelo
|
||||||
|
- Número de serie (único)
|
||||||
|
- Estado (disponible, prestado)
|
||||||
|
|
||||||
|
2. **Usuario**
|
||||||
|
- Identificador único
|
||||||
|
- Nombre
|
||||||
|
- Identificación (única)
|
||||||
|
|
||||||
|
3. **Préstamo**
|
||||||
|
- Identificador único
|
||||||
|
- Dispositivo asociado
|
||||||
|
- Usuario asociado
|
||||||
|
- Fecha de préstamo
|
||||||
|
- Fecha de devolución (opcional)
|
||||||
|
- Estado (activo, finalizado)
|
||||||
|
|
||||||
|
### Relaciones
|
||||||
|
|
||||||
|
- Un **usuario** puede tener **múltiples préstamos** (relación uno a muchos)
|
||||||
|
- Un **dispositivo** solo puede estar en **un préstamo activo** a la vez
|
||||||
|
- Un **préstamo** relaciona exactamente un dispositivo con un usuario
|
||||||
|
- El historial de préstamos permite rastrear todos los movimientos de cada dispositivo
|
||||||
|
|
||||||
|
### Reglas de Negocio
|
||||||
|
|
||||||
|
1. Un dispositivo en estado "prestado" no puede ser prestado a otro usuario
|
||||||
|
2. Un préstamo finalizado debe registrar fecha de devolución
|
||||||
|
3. La identificación de dispositivos y usuarios debe ser única
|
||||||
|
4. Al devolver un dispositivo, este vuelve al estado "disponible"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flujos de Trabajo
|
||||||
|
|
||||||
|
### Registro de un Nuevo Dispositivo
|
||||||
|
|
||||||
|
1. El sistema solicita la información del dispositivo (marca, modelo, número de serie)
|
||||||
|
2. El sistema valida que el número de serie no esté registrado
|
||||||
|
3. El sistema registra el dispositivo con estado "disponible"
|
||||||
|
4. El sistema confirma el registro
|
||||||
|
|
||||||
|
### Registro de un Nuevo Usuario
|
||||||
|
|
||||||
|
1. El sistema solicita la información del usuario (nombre, identificación)
|
||||||
|
2. El sistema valida que la identificación no esté registrada
|
||||||
|
3. El sistema registra el usuario
|
||||||
|
4. El sistema confirma el registro
|
||||||
|
|
||||||
|
### Préstamo de un Dispositivo
|
||||||
|
|
||||||
|
1. El sistema muestra los dispositivos disponibles
|
||||||
|
2. El usuario selecciona un dispositivo
|
||||||
|
3. El sistema muestra los usuarios registrados
|
||||||
|
4. El usuario selecciona un usuario
|
||||||
|
5. El sistema registra el préstamo con:
|
||||||
|
- Dispositivo seleccionado
|
||||||
|
- Usuario seleccionado
|
||||||
|
- Fecha y hora actual
|
||||||
|
- Estado "activo"
|
||||||
|
6. El sistema actualiza el estado del dispositivo a "prestado"
|
||||||
|
7. El sistema confirma el préstamo
|
||||||
|
|
||||||
|
### Devolución de un Dispositivo
|
||||||
|
|
||||||
|
1. El sistema muestra los préstamos activos
|
||||||
|
2. El usuario selecciona un préstamo
|
||||||
|
3. El sistema registra la fecha y hora de devolución
|
||||||
|
4. El sistema actualiza el estado del préstamo a "finalizado"
|
||||||
|
5. El sistema actualiza el estado del dispositivo a "disponible"
|
||||||
|
6. El sistema confirma la devolución
|
||||||
|
|
||||||
|
### Consulta de Préstamos por Usuario
|
||||||
|
|
||||||
|
1. El sistema muestra la lista de usuarios
|
||||||
|
2. El usuario selecciona un usuario
|
||||||
|
3. El sistema muestra:
|
||||||
|
- Dispositivos actualmente prestados (préstamos activos)
|
||||||
|
- Historial de préstamos pasados (opcional: expandible)
|
||||||
|
4. El sistema permite buscar por cualquier campo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requisitos No Funcionales
|
||||||
|
|
||||||
|
### Usabilidad
|
||||||
|
|
||||||
|
- La interfaz debe permitir búsqueda eficiente en listas grandes (500+ elementos)
|
||||||
|
- La visualización de información debe ser clara y organizada
|
||||||
|
- Los estados deben ser fácilmente identificables
|
||||||
|
- Las acciones principales deben ser accesibles
|
||||||
|
|
||||||
|
### Escalabilidad
|
||||||
|
|
||||||
|
- El diseño debe soportar un crecimiento en el número de dispositivos y usuarios
|
||||||
|
- La búsqueda debe ser eficiente
|
||||||
|
- El historial debe ser consultable sin afectar el rendimiento
|
||||||
|
|
||||||
|
### Mantenibilidad
|
||||||
|
|
||||||
|
- La estructura de datos debe ser clara
|
||||||
|
- Las relaciones entre entidades deben estar bien definidas
|
||||||
|
- Los flujos de trabajo deben estar documentados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diseño de Interfaz (Conceptual)
|
||||||
|
|
||||||
|
### Vistas Principales
|
||||||
|
|
||||||
|
1. **Vista de Inventario**
|
||||||
|
- Lista de dispositivos
|
||||||
|
- Filtros por estado
|
||||||
|
- Búsqueda
|
||||||
|
|
||||||
|
2. **Vista de Usuarios**
|
||||||
|
- Lista de usuarios
|
||||||
|
- Búsqueda
|
||||||
|
|
||||||
|
3. **Vista de Préstamos Activos**
|
||||||
|
- Lista de préstamos en curso
|
||||||
|
- Filtros por dispositivo/usuario
|
||||||
|
- Acciones: devolver dispositivo
|
||||||
|
|
||||||
|
4. **Vista de Historial**
|
||||||
|
- Lista completa de préstamos
|
||||||
|
- Filtros por fecha, usuario, dispositivo, estado
|
||||||
|
- Detalles de cada préstamo
|
||||||
|
|
||||||
|
5. **Vista de Préstamos por Usuario**
|
||||||
|
- Lista de usuarios
|
||||||
|
- Para cada usuario: dispositivos actualmente prestados
|
||||||
|
- Opcional: historial de préstamos pasados (expandible)
|
||||||
|
|
||||||
|
### Elementos de Interacción
|
||||||
|
|
||||||
|
- Campos de búsqueda en todas las listas
|
||||||
|
- Botones de acción claros (Prestar, Devolver, Registrar)
|
||||||
|
- Indicadores visuales de estado (colores, iconos)
|
||||||
|
- Confirmación de acciones críticas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas de Implementación
|
||||||
|
|
||||||
|
Este documento describe los requisitos a nivel de diseño. La implementación específica (lenguaje de programación, base de datos, framework, arquitectura) queda a criterio del equipo de desarrollo y puede variar según las necesidades técnicas.
|
||||||
|
|
||||||
|
Los detalles técnicos como:
|
||||||
|
- Tecnologías específicas
|
||||||
|
- Estructura de base de datos física
|
||||||
|
- APIs
|
||||||
|
- Autenticación
|
||||||
|
- Despliegue
|
||||||
|
|
||||||
|
Deberán ser definidos en la fase de diseño técnico y pueden evolucionar sin afectar los requisitos funcionales descritos aquí.
|
||||||
149
TECHNICAL_SPECIFICATIONS.md
Normal file
149
TECHNICAL_SPECIFICATIONS.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# 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)
|
||||||
109
app.py
109
app.py
|
|
@ -63,6 +63,22 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Create non_loanable_devices table for devices that cannot be loaned
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
brand TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
serial_number TEXT UNIQUE NOT NULL,
|
||||||
|
device_type TEXT NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
status TEXT DEFAULT 'available',
|
||||||
|
notes TEXT,
|
||||||
|
purchase_date TEXT,
|
||||||
|
purchase_cost REAL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|
@ -319,6 +335,99 @@ def user_loans():
|
||||||
return render_template('user_loans.html', users_with_loans=user_loans_list)
|
return render_template('user_loans.html', users_with_loans=user_loans_list)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/non_loanable_devices')
|
||||||
|
def non_loanable_devices():
|
||||||
|
"""Show all non-loanable devices"""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM non_loanable_devices ORDER BY device_type, brand, model")
|
||||||
|
devices = cursor.fetchall()
|
||||||
|
|
||||||
|
return render_template('non_loanable_devices.html', devices=devices)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/add_non_loanable_device', methods=['GET', 'POST'])
|
||||||
|
def add_non_loanable_device():
|
||||||
|
"""Add a new non-loanable device to inventory"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
brand = request.form['brand']
|
||||||
|
model = request.form['model']
|
||||||
|
serial_number = request.form['serial_number']
|
||||||
|
device_type = request.form['device_type']
|
||||||
|
location = request.form['location']
|
||||||
|
notes = request.form['notes']
|
||||||
|
purchase_date = request.form.get('purchase_date', '')
|
||||||
|
purchase_cost = request.form.get('purchase_cost', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO non_loanable_devices
|
||||||
|
(brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
|
||||||
|
''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
|
||||||
|
conn.commit()
|
||||||
|
flash('Non-loanable device added successfully!', 'success')
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash('Error: Serial number already exists!', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('non_loanable_devices'))
|
||||||
|
|
||||||
|
return render_template('add_non_loanable_device.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/edit_non_loanable_device/<int:device_id>', 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/<int:device_id>')
|
||||||
|
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__':
|
if __name__ == '__main__':
|
||||||
# Initialize database
|
# Initialize database
|
||||||
init_db()
|
init_db()
|
||||||
|
|
|
||||||
1
gestiontablets.sh
Normal file
1
gestiontablets.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
vibe --resume fa1bb230
|
||||||
|
|
@ -44,6 +44,22 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Create non_loanable_devices table
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
brand TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
serial_number TEXT UNIQUE NOT NULL,
|
||||||
|
device_type TEXT NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
status TEXT DEFAULT 'available',
|
||||||
|
notes TEXT,
|
||||||
|
purchase_date TEXT,
|
||||||
|
purchase_cost REAL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
@ -202,6 +218,59 @@ def show_loan_history():
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def add_non_loanable_device(brand, model, serial_number, device_type, location='', notes='', purchase_date='', purchase_cost=None):
|
||||||
|
"""Add a new non-loanable device to inventory"""
|
||||||
|
conn = sqlite3.connect('tablets.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO non_loanable_devices
|
||||||
|
(brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?)
|
||||||
|
''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost))
|
||||||
|
conn.commit()
|
||||||
|
print(f"✓ Added non-loanable device: {brand} {model} ({serial_number}) - Type: {device_type}")
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
print(f"✗ Error: Serial number {serial_number} already exists")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def show_non_loanable_devices():
|
||||||
|
"""Show all non-loanable devices"""
|
||||||
|
conn = sqlite3.connect('tablets.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT id, brand, model, serial_number, device_type, location, status FROM non_loanable_devices")
|
||||||
|
devices = cursor.fetchall()
|
||||||
|
|
||||||
|
print("\n=== Non-Loanable Devices ===")
|
||||||
|
if devices:
|
||||||
|
for device in devices:
|
||||||
|
print(f"ID: {device[0]}, Type: {device[4]}, {device[1]} {device[2]} ({device[3]}), Location: {device[5] or 'N/A'}, Status: {device[6]}")
|
||||||
|
else:
|
||||||
|
print("No non-loanable devices")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_non_loanable_device(device_id):
|
||||||
|
"""Delete a non-loanable device"""
|
||||||
|
conn = sqlite3.connect('tablets.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if cursor.rowcount > 0:
|
||||||
|
print(f"✓ Non-loanable device {device_id} deleted")
|
||||||
|
else:
|
||||||
|
print(f"✗ Error: Device {device_id} not found")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main menu"""
|
"""Main menu"""
|
||||||
init_db()
|
init_db()
|
||||||
|
|
@ -218,9 +287,12 @@ def main():
|
||||||
print("5. Show Available Tablets")
|
print("5. Show Available Tablets")
|
||||||
print("6. Show Active Loans")
|
print("6. Show Active Loans")
|
||||||
print("7. Show Loan History")
|
print("7. Show Loan History")
|
||||||
print("8. Exit")
|
print("8. Add Non-Loanable Device")
|
||||||
|
print("9. Show Non-Loanable Devices")
|
||||||
|
print("10. Delete Non-Loanable Device")
|
||||||
|
print("11. Exit")
|
||||||
|
|
||||||
choice = input("Enter your choice (1-8): ")
|
choice = input("Enter your choice (1-11): ")
|
||||||
|
|
||||||
if choice == '1':
|
if choice == '1':
|
||||||
print("\n=== Add Tablet ===")
|
print("\n=== Add Tablet ===")
|
||||||
|
|
@ -269,6 +341,28 @@ def main():
|
||||||
show_loan_history()
|
show_loan_history()
|
||||||
|
|
||||||
elif choice == '8':
|
elif choice == '8':
|
||||||
|
print("\n=== Add Non-Loanable Device ===")
|
||||||
|
brand = input("Brand: ")
|
||||||
|
model = input("Model: ")
|
||||||
|
serial_number = input("Serial Number: ")
|
||||||
|
device_type = input("Device Type (e.g., projector, monitor): ")
|
||||||
|
location = input("Location (optional): ")
|
||||||
|
notes = input("Notes (optional): ")
|
||||||
|
add_non_loanable_device(brand, model, serial_number, device_type, location, notes)
|
||||||
|
|
||||||
|
elif choice == '9':
|
||||||
|
show_non_loanable_devices()
|
||||||
|
|
||||||
|
elif choice == '10':
|
||||||
|
print("\n=== Delete Non-Loanable Device ===")
|
||||||
|
show_non_loanable_devices()
|
||||||
|
device_id = input("Enter Device ID to delete: ")
|
||||||
|
try:
|
||||||
|
delete_non_loanable_device(int(device_id))
|
||||||
|
except ValueError:
|
||||||
|
print("✗ Error: Invalid ID format")
|
||||||
|
|
||||||
|
elif choice == '11':
|
||||||
print("Goodbye!")
|
print("Goodbye!")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,22 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Create non_loanable_devices table
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
brand TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
serial_number TEXT UNIQUE NOT NULL,
|
||||||
|
device_type TEXT NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
status TEXT DEFAULT 'available',
|
||||||
|
notes TEXT,
|
||||||
|
purchase_date TEXT,
|
||||||
|
purchase_cost REAL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
|
|
||||||
63
templates/add_non_loanable_device.html
Normal file
63
templates/add_non_loanable_device.html
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Add Non-Loanable Device</h2>
|
||||||
|
<p>Add a device that will be tracked in inventory but cannot be loaned to users (e.g., projectors, monitors, etc.)</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/add_non_loanable_device">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device_type">Device Type:</label>
|
||||||
|
<select id="device_type" name="device_type" required>
|
||||||
|
<option value="">Select device type...</option>
|
||||||
|
<option value="projector">Projector</option>
|
||||||
|
<option value="monitor">Monitor</option>
|
||||||
|
<option value="laptop">Laptop (non-loanable)</option>
|
||||||
|
<option value="printer">Printer</option>
|
||||||
|
<option value="camera">Camera</option>
|
||||||
|
<option value="audio">Audio Equipment</option>
|
||||||
|
<option value="network">Network Equipment</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="brand">Brand:</label>
|
||||||
|
<input type="text" id="brand" name="brand" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="model">Model:</label>
|
||||||
|
<input type="text" id="model" name="model" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="serial_number">Serial Number:</label>
|
||||||
|
<input type="text" id="serial_number" name="serial_number" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Location:</label>
|
||||||
|
<input type="text" id="location" name="location">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="purchase_date">Purchase Date:</label>
|
||||||
|
<input type="date" id="purchase_date" name="purchase_date">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="purchase_cost">Purchase Cost:</label>
|
||||||
|
<input type="number" id="purchase_cost" name="purchase_cost" step="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notes">Notes:</label>
|
||||||
|
<textarea id="notes" name="notes"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn">Add Device</button>
|
||||||
|
<a href="/non_loanable_devices" class="btn">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -144,6 +144,7 @@
|
||||||
<a href="/loan_tablet">Loan Tablet</a>
|
<a href="/loan_tablet">Loan Tablet</a>
|
||||||
<a href="/history">Loan History</a>
|
<a href="/history">Loan History</a>
|
||||||
<a href="/user_loans">User Loans</a>
|
<a href="/user_loans">User Loans</a>
|
||||||
|
<a href="/non_loanable_devices">Non-Loanable Devices</a>
|
||||||
<a href="/project_management">Project Management</a>
|
<a href="/project_management">Project Management</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
71
templates/edit_non_loanable_device.html
Normal file
71
templates/edit_non_loanable_device.html
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Edit Non-Loanable Device</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="/edit_non_loanable_device/{{ device.id }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device_type">Device Type:</label>
|
||||||
|
<select id="device_type" name="device_type" required>
|
||||||
|
<option value="projector" {% if device.device_type == 'projector' %}selected{% endif %}>Projector</option>
|
||||||
|
<option value="monitor" {% if device.device_type == 'monitor' %}selected{% endif %}>Monitor</option>
|
||||||
|
<option value="laptop" {% if device.device_type == 'laptop' %}selected{% endif %}>Laptop (non-loanable)</option>
|
||||||
|
<option value="printer" {% if device.device_type == 'printer' %}selected{% endif %}>Printer</option>
|
||||||
|
<option value="camera" {% if device.device_type == 'camera' %}selected{% endif %}>Camera</option>
|
||||||
|
<option value="audio" {% if device.device_type == 'audio' %}selected{% endif %}>Audio Equipment</option>
|
||||||
|
<option value="network" {% if device.device_type == 'network' %}selected{% endif %}>Network Equipment</option>
|
||||||
|
<option value="other" {% if device.device_type == 'other' %}selected{% endif %}>Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="brand">Brand:</label>
|
||||||
|
<input type="text" id="brand" name="brand" value="{{ device.brand }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="model">Model:</label>
|
||||||
|
<input type="text" id="model" name="model" value="{{ device.model }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="serial_number">Serial Number:</label>
|
||||||
|
<input type="text" id="serial_number" name="serial_number" value="{{ device.serial_number }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Location:</label>
|
||||||
|
<input type="text" id="location" name="location" value="{{ device.location or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status">Status:</label>
|
||||||
|
<select id="status" name="status" required>
|
||||||
|
<option value="available" {% if device.status == 'available' %}selected{% endif %}>Available</option>
|
||||||
|
<option value="in_use" {% if device.status == 'in_use' %}selected{% endif %}>In Use</option>
|
||||||
|
<option value="maintenance" {% if device.status == 'maintenance' %}selected{% endif %}>Maintenance</option>
|
||||||
|
<option value="retired" {% if device.status == 'retired' %}selected{% endif %}>Retired</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="purchase_date">Purchase Date:</label>
|
||||||
|
<input type="date" id="purchase_date" name="purchase_date" value="{{ device.purchase_date or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="purchase_cost">Purchase Cost:</label>
|
||||||
|
<input type="number" id="purchase_cost" name="purchase_cost" step="0.01" value="{{ device.purchase_cost or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notes">Notes:</label>
|
||||||
|
<textarea id="notes" name="notes">{{ device.notes or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn">Update Device</button>
|
||||||
|
<a href="/non_loanable_devices" class="btn">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
43
templates/non_loanable_devices.html
Normal file
43
templates/non_loanable_devices.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Non-Loanable Devices Inventory</h2>
|
||||||
|
<p>These devices are tracked in inventory but cannot be loaned to users.</p>
|
||||||
|
<a href="/add_non_loanable_device" class="btn">Add Non-Loanable Device</a>
|
||||||
|
|
||||||
|
{% if devices %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Model</th>
|
||||||
|
<th>Serial Number</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for device in devices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ device.device_type }}</td>
|
||||||
|
<td>{{ device.brand }}</td>
|
||||||
|
<td>{{ device.model }}</td>
|
||||||
|
<td>{{ device.serial_number }}</td>
|
||||||
|
<td>{{ device.location or '-' }}</td>
|
||||||
|
<td>{{ device.status }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/edit_non_loanable_device/{{ device.id }}" class="btn">Edit</a>
|
||||||
|
<a href="/delete_non_loanable_device/{{ device.id }}" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this device?')">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>No non-loanable devices registered.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -7,18 +7,67 @@
|
||||||
This page demonstrates the many-to-many relationship between users and tablets via the loans table.
|
This page demonstrates the many-to-many relationship between users and tablets via the loans table.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="user-loans-container">
|
<!-- Search Box -->
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="searchInput" placeholder="Search by user name, identification, tablet brand, model, or serial number..."
|
||||||
|
onkeyup="filterUsers()">
|
||||||
|
<button onclick="clearSearch()" class="btn btn-clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-loans-container" id="userLoansContainer">
|
||||||
{% for user_data in users_with_loans %}
|
{% for user_data in users_with_loans %}
|
||||||
{% set user = user_data.user %}
|
{% set user = user_data.user %}
|
||||||
{% set loans = user_data.loans %}
|
{% set loans = user_data.loans %}
|
||||||
|
{% set active_loans = loans|selectattr('status', 'equalto', 'active')|list %}
|
||||||
|
{% set returned_loans = loans|selectattr('status', 'equalto', 'returned')|list %}
|
||||||
|
|
||||||
<div class="user-card">
|
<div class="user-card"
|
||||||
|
data-search="{{ user.name|lower }} {{ user.identification|lower }} {% for loan in loans %}{{ loan.brand|lower }} {{ loan.model|lower }} {{ loan.serial_number|lower }} {% endfor %}">
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<h3>{{ user.name }} ({{ user.identification }})</h3>
|
<h3>{{ user.name }} ({{ user.identification }})</h3>
|
||||||
<span class="loan-count">{{ loans|length }} tablet{{ 's' if loans|length != 1 else '' }} loaned</span>
|
<span class="loan-count">
|
||||||
|
{{ active_loans|length }} active, {{ returned_loans|length }} past
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if loans %}
|
{% if active_loans %}
|
||||||
|
<div class="loans-section current-loans">
|
||||||
|
<h4>Current Loans</h4>
|
||||||
|
<table class="loans-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tablet</th>
|
||||||
|
<th>Serial Number</th>
|
||||||
|
<th>Loan Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for loan in active_loans %}
|
||||||
|
<tr class="loan-row status-{{ loan.status }}">
|
||||||
|
<td>{{ loan.brand }} {{ loan.model }}</td>
|
||||||
|
<td>{{ loan.serial_number }}</td>
|
||||||
|
<td>{{ loan.loan_date }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ loan.status }}">
|
||||||
|
{{ loan.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if returned_loans %}
|
||||||
|
<div class="loans-section past-loans">
|
||||||
|
<h4>
|
||||||
|
<button class="expand-toggle" onclick="togglePastLoans(this)">
|
||||||
|
▶ Past Loans ({{ returned_loans|length }})
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<div class="past-loans-content" style="display: none;">
|
||||||
<table class="loans-table">
|
<table class="loans-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -30,7 +79,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for loan in loans %}
|
{% for loan in returned_loans %}
|
||||||
<tr class="loan-row status-{{ loan.status }}">
|
<tr class="loan-row status-{{ loan.status }}">
|
||||||
<td>{{ loan.brand }} {{ loan.model }}</td>
|
<td>{{ loan.brand }} {{ loan.model }}</td>
|
||||||
<td>{{ loan.serial_number }}</td>
|
<td>{{ loan.serial_number }}</td>
|
||||||
|
|
@ -45,7 +94,11 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not active_loans and not returned_loans %}
|
||||||
<p class="no-loans">No loans recorded for this user.</p>
|
<p class="no-loans">No loans recorded for this user.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,6 +109,32 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function filterUsers() {
|
||||||
|
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const cards = document.querySelectorAll('.user-card');
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const searchText = card.getAttribute('data-search') || '';
|
||||||
|
card.style.display = searchText.includes(search) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
filterUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePastLoans(button) {
|
||||||
|
const content = button.closest('.loans-section').querySelector('.past-loans-content');
|
||||||
|
const isExpanded = content.style.display !== 'none';
|
||||||
|
|
||||||
|
content.style.display = isExpanded ? 'none' : 'block';
|
||||||
|
const count = button.textContent.match(/\d+/)[0];
|
||||||
|
button.textContent = isExpanded ? '▶ Past Loans (' + count + ')' : '▼ Past Loans (' + count + ') ';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.relationship-note {
|
.relationship-note {
|
||||||
background-color: #e8f5e9;
|
background-color: #e8f5e9;
|
||||||
|
|
@ -65,6 +144,23 @@
|
||||||
border-left: 4px solid #4CAF50;
|
border-left: 4px solid #4CAF50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
background-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.user-loans-container {
|
.user-loans-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -98,6 +194,28 @@
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loans-section {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loans-section h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #4CAF50;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-toggle:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.loans-table {
|
.loans-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
@ -120,7 +238,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.loan-row.status-returned {
|
.loan-row.status-returned {
|
||||||
background-color: #e8f5e9;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
|
|
@ -137,7 +255,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-returned {
|
.status-badge.status-returned {
|
||||||
background-color: #4CAF50;
|
background-color: #9E9E9E;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue