Compare commits
No commits in common. "3039363b6b47a0c279b7d112055ad5170455e635" and "8a8d2f02fa0164c06879111db19e6d815c588507" have entirely different histories.
3039363b6b
...
8a8d2f02fa
12 changed files with 38 additions and 937 deletions
21
README.md
21
README.md
|
|
@ -10,16 +10,15 @@ 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 (includes non-loanable device management)
|
- `minimal_app.py` - Interactive command-line application
|
||||||
- `test_app.py` - Test script that demonstrates functionality
|
- `test_app.py` - Test script that demonstrates functionality
|
||||||
- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table)
|
- `tablets.db` - SQLite database (created automatically)
|
||||||
- `simple_app.py` - Web-based version (requires Flask, includes non-loanable device management)
|
- `simple_app.py` - Web-based version (requires Flask)
|
||||||
- `app.py` - Alternative web version (requires Flask, includes non-loanable device management)
|
- `app.py` - Alternative web version (requires Flask)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -145,7 +144,6 @@ 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
|
||||||
|
|
@ -155,17 +153,6 @@ 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
221
REQUIREMENTS.md
|
|
@ -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í.
|
|
||||||
|
|
@ -1,149 +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)
|
|
||||||
109
app.py
109
app.py
|
|
@ -63,22 +63,6 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Create non_loanable_devices table for devices that cannot be loaned
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
brand TEXT NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
serial_number TEXT UNIQUE NOT NULL,
|
|
||||||
device_type TEXT NOT NULL,
|
|
||||||
location TEXT,
|
|
||||||
status TEXT DEFAULT 'available',
|
|
||||||
notes TEXT,
|
|
||||||
purchase_date TEXT,
|
|
||||||
purchase_cost REAL
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
|
@ -335,99 +319,6 @@ 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 +0,0 @@
|
||||||
vibe --resume fa1bb230
|
|
||||||
|
|
@ -44,22 +44,6 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Create non_loanable_devices table
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
brand TEXT NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
serial_number TEXT UNIQUE NOT NULL,
|
|
||||||
device_type TEXT NOT NULL,
|
|
||||||
location TEXT,
|
|
||||||
status TEXT DEFAULT 'available',
|
|
||||||
notes TEXT,
|
|
||||||
purchase_date TEXT,
|
|
||||||
purchase_cost REAL
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
@ -218,59 +202,6 @@ 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()
|
||||||
|
|
@ -287,12 +218,9 @@ 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. Add Non-Loanable Device")
|
print("8. Exit")
|
||||||
print("9. Show Non-Loanable Devices")
|
|
||||||
print("10. Delete Non-Loanable Device")
|
|
||||||
print("11. Exit")
|
|
||||||
|
|
||||||
choice = input("Enter your choice (1-11): ")
|
choice = input("Enter your choice (1-8): ")
|
||||||
|
|
||||||
if choice == '1':
|
if choice == '1':
|
||||||
print("\n=== Add Tablet ===")
|
print("\n=== Add Tablet ===")
|
||||||
|
|
@ -341,28 +269,6 @@ 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,22 +62,6 @@ def init_db():
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Create non_loanable_devices table
|
|
||||||
cursor.execute('''
|
|
||||||
CREATE TABLE IF NOT EXISTS non_loanable_devices (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
brand TEXT NOT NULL,
|
|
||||||
model TEXT NOT NULL,
|
|
||||||
serial_number TEXT UNIQUE NOT NULL,
|
|
||||||
device_type TEXT NOT NULL,
|
|
||||||
location TEXT,
|
|
||||||
status TEXT DEFAULT 'available',
|
|
||||||
notes TEXT,
|
|
||||||
purchase_date TEXT,
|
|
||||||
purchase_cost REAL
|
|
||||||
)
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
{% 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,7 +144,6 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
{% 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 %}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
{% 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,98 +7,45 @@
|
||||||
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>
|
||||||
|
|
||||||
<!-- Search Box -->
|
<div class="user-loans-container">
|
||||||
<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">
|
<span class="loan-count">{{ loans|length }} tablet{{ 's' if loans|length != 1 else '' }} loaned</span>
|
||||||
{{ active_loans|length }} active, {{ returned_loans|length }} past
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if active_loans %}
|
{% if loans %}
|
||||||
<div class="loans-section current-loans">
|
<table class="loans-table">
|
||||||
<h4>Current Loans</h4>
|
<thead>
|
||||||
<table class="loans-table">
|
<tr>
|
||||||
<thead>
|
<th>Tablet</th>
|
||||||
<tr>
|
<th>Serial Number</th>
|
||||||
<th>Tablet</th>
|
<th>Loan Date</th>
|
||||||
<th>Serial Number</th>
|
<th>Return Date</th>
|
||||||
<th>Loan Date</th>
|
<th>Status</th>
|
||||||
<th>Status</th>
|
</tr>
|
||||||
|
</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>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for loan in active_loans %}
|
</table>
|
||||||
<tr class="loan-row status-{{ loan.status }}">
|
{% else %}
|
||||||
<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>
|
||||||
|
|
@ -109,32 +56,6 @@
|
||||||
{% 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;
|
||||||
|
|
@ -144,23 +65,6 @@
|
||||||
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;
|
||||||
|
|
@ -194,28 +98,6 @@
|
||||||
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;
|
||||||
|
|
@ -238,7 +120,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.loan-row.status-returned {
|
.loan-row.status-returned {
|
||||||
background-color: #f5f5f5;
|
background-color: #e8f5e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
|
|
@ -255,7 +137,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-returned {
|
.status-badge.status-returned {
|
||||||
background-color: #9E9E9E;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue