Merge feature/user-loans-search: Non-loanable device management

This commit is contained in:
ijuanes 2026-06-09 23:54:44 +01:00
commit 3039363b6b
12 changed files with 937 additions and 38 deletions

View file

@ -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
View 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
View 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
View file

@ -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
View file

@ -0,0 +1 @@
vibe --resume fa1bb230

View file

@ -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

View file

@ -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):

View 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 %}

View file

@ -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>

View 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 %}

View 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 %}

View file

@ -7,45 +7,98 @@
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 %}
<table class="loans-table"> <div class="loans-section current-loans">
<thead> <h4>Current Loans</h4>
<tr> <table class="loans-table">
<th>Tablet</th> <thead>
<th>Serial Number</th> <tr>
<th>Loan Date</th> <th>Tablet</th>
<th>Return Date</th> <th>Serial Number</th>
<th>Status</th> <th>Loan Date</th>
</tr> <th>Status</th>
</thead>
<tbody>
{% for loan in 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>{{ loan.return_date or '-' }}</td>
<td>
<span class="status-badge status-{{ loan.status }}">
{{ loan.status }}
</span>
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for loan in active_loans %}
{% else %} <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">
<thead>
<tr>
<th>Tablet</th>
<th>Serial Number</th>
<th>Loan Date</th>
<th>Return Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for loan in returned_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>{{ loan.return_date or '-' }}</td>
<td>
<span class="status-badge status-{{ loan.status }}">
{{ loan.status }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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;
} }