Initial commit: Tablet lending system with user-tablet many-to-many relationship

- Database schema: tablets, users, loans (junction table)
- CLI app: minimal_app.py
- Web apps: app.py (Flask), simple_app.py (http.server)
- Features: loan management, search, user loans view, project notes
- Git structure: CONTRIBUTING.md, .gitignore

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
ijuanes 2026-06-03 10:43:17 +01:00
commit 8a8d2f02fa
25 changed files with 2885 additions and 0 deletions

30
templates/add_tablet.html Normal file
View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block content %}
<h2>Add New Tablet</h2>
<form method="POST" action="/add_tablet">
<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="notes">Notes:</label>
<textarea id="notes" name="notes"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn">Add Tablet</button>
</div>
</form>
{% endblock %}

30
templates/add_user.html Normal file
View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block content %}
<h2>Add New User</h2>
<form method="POST" action="/add_user">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email">
</div>
<div class="form-group">
<label for="phone">Phone:</label>
<input type="text" id="phone" name="phone">
</div>
<div class="form-group">
<label for="identification">Identification:</label>
<input type="text" id="identification" name="identification" required>
</div>
<div class="form-group">
<button type="submit" class="btn">Add User</button>
</div>
</form>
{% endblock %}

153
templates/base.html Normal file
View file

@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tablet Management System</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.nav {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.nav a {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 4px;
}
.nav a:hover {
background-color: #45a049;
}
.section {
margin-bottom: 30px;
}
.section h2 {
color: #4CAF50;
border-bottom: 2px solid #4CAF50;
padding-bottom: 5px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #4CAF50;
color: white;
}
tr:hover {
background-color: #f5f5f5;
}
.btn {
padding: 6px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn:hover {
background-color: #45a049;
}
.btn-danger {
background-color: #f44336;
}
.btn-danger:hover {
background-color: #CC00CC;
}
.flash-message {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.flash-success {
background-color: #dff0d8;
color: #3c763d;
border: 1px solid #d6e9c6;
}
.flash-error {
background-color: #f2dede;
color: #a94442;
border: 1px solid #ebccd1;
}
form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="email"],
textarea,
select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
textarea {
height: 100px;
}
</style>
</head>
<body>
<div class="container">
<h1>Tablet Management System</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash-message flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="nav">
<a href="/">Home</a>
<a href="/add_tablet">Add Tablet</a>
<a href="/add_user">Add User</a>
<a href="/loan_tablet">Loan Tablet</a>
<a href="/history">Loan History</a>
<a href="/user_loans">User Loans</a>
<a href="/project_management">Project Management</a>
</div>
{% block content %}{% endblock %}
</div>
</body>
</html>

35
templates/history.html Normal file
View file

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block content %}
<div class="section">
<h2>Loan History</h2>
{% if loans %}
<table>
<thead>
<tr>
<th>Tablet</th>
<th>Serial Number</th>
<th>Borrower</th>
<th>Loan Date</th>
<th>Return Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for loan in loans %}
<tr>
<td>{{ loan.brand }} {{ loan.model }}</td>
<td>{{ loan.serial_number }}</td>
<td>{{ loan.name }}</td>
<td>{{ loan.loan_date }}</td>
<td>{{ loan.return_date or '-' }}</td>
<td>{{ loan.status }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No loan history available.</p>
{% endif %}
</div>
{% endblock %}

63
templates/index.html Normal file
View file

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block content %}
<div class="section">
<h2>Available Tablets</h2>
{% if available_tablets %}
<table>
<thead>
<tr>
<th>Brand</th>
<th>Model</th>
<th>Serial Number</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for tablet in available_tablets %}
<tr>
<td>{{ tablet.brand }}</td>
<td>{{ tablet.model }}</td>
<td>{{ tablet.serial_number }}</td>
<td>{{ tablet.notes or '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No available tablets.</p>
{% endif %}
</div>
<div class="section">
<h2>Active Loans</h2>
{% if active_loans %}
<table>
<thead>
<tr>
<th>Tablet</th>
<th>Serial Number</th>
<th>Borrower</th>
<th>Loan Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for loan in active_loans %}
<tr>
<td>{{ loan.brand }} {{ loan.model }}</td>
<td>{{ loan.serial_number }}</td>
<td>{{ loan.name }}</td>
<td>{{ loan.loan_date }}</td>
<td>
<a href="/return_tablet/{{ loan.id }}" class="btn btn-danger">Return</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No active loans.</p>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block content %}
<h2>Loan Tablet</h2>
<form method="POST" action="/loan_tablet">
<!-- Tablet Selection with Search -->
<div class="form-group">
<label for="tablet_id">Tablet:</label>
<input type="text" id="tablet_search" class="search-input"
placeholder="Search tablets (brand, model, or serial)..."
onkeyup="filterDropdown('tablet_search', 'tablet_id')">
<select id="tablet_id" name="tablet_id" required size="5">
<option value="">Select a tablet</option>
{% for tablet in available_tablets %}
<option value="{{ tablet.id }}"
data-search="{{ tablet.brand|lower }} {{ tablet.model|lower }} {{ tablet.serial_number|lower }}">
{{ tablet.brand }} {{ tablet.model }} ({{ tablet.serial_number }})
</option>
{% endfor %}
</select>
</div>
<!-- User Selection with Search -->
<div class="form-group">
<label for="user_id">User:</label>
<input type="text" id="user_search" class="search-input"
placeholder="Search users (name or ID)..."
onkeyup="filterDropdown('user_search', 'user_id')">
<select id="user_id" name="user_id" required size="5">
<option value="">Select a user</option>
{% for user in users %}
<option value="{{ user.id }}"
data-search="{{ user.name|lower }} {{ user.identification|lower }}">
{{ user.name }} ({{ user.identification }})
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<button type="submit" class="btn">Loan Tablet</button>
</div>
</form>
<script>
function filterDropdown(searchId, selectId) {
const search = document.getElementById(searchId).value.toLowerCase();
const select = document.getElementById(selectId);
const options = select.options;
for (let i = 1; i < options.length; i++) {
const searchText = options[i].getAttribute('data-search') || '';
options[i].style.display = searchText.includes(search) ? '' : 'none';
}
// Show first visible option if search is empty, otherwise keep selection
if (search === '') {
select.selectedIndex = 0;
}
}
</script>
<style>
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 5px;
}
select[size] {
width: 100%;
max-height: 200px;
overflow-y: auto;
}
</style>
{% endblock %}

View file

@ -0,0 +1,196 @@
{% extends "base.html" %}
{% block content %}
<h2>Project Management - Development Notes</h2>
<div class="project-management-container">
<div class="editor-section">
<div class="editor-controls">
<button onclick="saveNotes()" class="btn">Save Notes</button>
<button onclick="loadNotes()" class="btn">Reload</button>
<span id="status" style="margin-left: 15px; font-style: italic;"></span>
</div>
<div class="editor-row">
<div class="editor-col">
<h3>Editor</h3>
<textarea id="markdown-editor" class="markdown-editor"
placeholder="Write your markdown notes here..."></textarea>
</div>
<div class="editor-col">
<h3>Preview</h3>
<div id="markdown-preview" class="markdown-preview"></div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const NOTES_FILE = 'notes_development/project_notes.md';
// Initialize: load and render notes
document.addEventListener('DOMContentLoaded', function() {
loadNotes();
// Set up live preview
const editor = document.getElementById('markdown-editor');
const preview = document.getElementById('markdown-preview');
editor.addEventListener('input', function() {
preview.innerHTML = marked.parse(editor.value);
});
});
function loadNotes() {
fetch('/get_notes/' + NOTES_FILE)
.then(response => {
if (!response.ok) {
throw new Error('File not found, using default');
}
return response.text();
})
.then(content => {
document.getElementById('markdown-editor').value = content;
document.getElementById('markdown-preview').innerHTML = marked.parse(content);
document.getElementById('status').textContent = 'Loaded successfully';
setTimeout(() => {
document.getElementById('status').textContent = '';
}, 2000);
})
.catch(error => {
document.getElementById('status').textContent = 'Using default content';
setTimeout(() => {
document.getElementById('status').textContent = '';
}, 2000);
});
}
function saveNotes() {
const content = document.getElementById('markdown-editor').value;
const statusEl = document.getElementById('status');
fetch('/save_notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: content, filename: NOTES_FILE })
})
.then(response => {
if (response.ok) {
statusEl.textContent = 'Saved successfully!';
statusEl.style.color = '#4CAF50';
} else {
statusEl.textContent = 'Error saving';
statusEl.style.color = '#f44336';
}
setTimeout(() => {
statusEl.textContent = '';
statusEl.style.color = '';
}, 2000);
})
.catch(error => {
statusEl.textContent = 'Error: ' + error.message;
statusEl.style.color = '#f44336';
setTimeout(() => {
statusEl.textContent = '';
statusEl.style.color = '';
}, 2000);
});
}
// Auto-save every 30 seconds
setInterval(() => {
saveNotes();
}, 30000);
</script>
<style>
.project-management-container {
max-width: 100%;
}
.editor-controls {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
}
.editor-row {
display: flex;
gap: 20px;
}
.editor-col {
flex: 1;
}
.markdown-editor {
width: 100%;
height: 500px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
}
.markdown-preview {
width: 100%;
height: 500px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
overflow-y: auto;
box-sizing: border-box;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin-top: 0;
}
.markdown-preview pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
.markdown-preview code {
font-family: monospace;
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
}
.markdown-preview table {
border-collapse: collapse;
width: 100%;
}
.markdown-preview th,
.markdown-preview td {
border: 1px solid #ddd;
padding: 8px;
}
@media (max-width: 768px) {
.editor-row {
flex-direction: column;
}
}
</style>
{% endblock %}

157
templates/user_loans.html Normal file
View file

@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block content %}
<h2>User Loans</h2>
<p class="relationship-note">
<strong>Relationship:</strong> One user can loan multiple tablets (one-to-many).
This page demonstrates the many-to-many relationship between users and tablets via the loans table.
</p>
<div class="user-loans-container">
{% for user_data in users_with_loans %}
{% set user = user_data.user %}
{% set loans = user_data.loans %}
<div class="user-card">
<div class="user-header">
<h3>{{ user.name }} ({{ user.identification }})</h3>
<span class="loan-count">{{ loans|length }} tablet{{ 's' if loans|length != 1 else '' }} loaned</span>
</div>
{% if loans %}
<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 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>
{% else %}
<p class="no-loans">No loans recorded for this user.</p>
{% endif %}
</div>
{% endfor %}
{% if users_with_loans|length == 0 %}
<p class="no-data">No users found. Add users and loan tablets to see data here.</p>
{% endif %}
</div>
<style>
.relationship-note {
background-color: #e8f5e9;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border-left: 4px solid #4CAF50;
}
.user-loans-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.user-card {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.user-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.user-header h3 {
margin: 0;
color: #333;
}
.loan-count {
color: #666;
font-style: italic;
}
.loans-table {
width: 100%;
border-collapse: collapse;
}
.loans-table th,
.loans-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
.loans-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.loan-row.status-active {
background-color: #fff8e1;
}
.loan-row.status-returned {
background-color: #e8f5e9;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
text-transform: capitalize;
}
.status-badge.status-active {
background-color: #ffc107;
color: #000;
}
.status-badge.status-returned {
background-color: #4CAF50;
color: white;
}
.no-loans {
color: #999;
font-style: italic;
text-align: center;
padding: 20px;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
}
</style>
{% endblock %}