2026-06-03 10:43:17 +01:00
|
|
|
{% 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>
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
<!-- 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">
|
2026-06-03 10:43:17 +01:00
|
|
|
{% for user_data in users_with_loans %}
|
|
|
|
|
{% set user = user_data.user %}
|
|
|
|
|
{% set loans = user_data.loans %}
|
2026-06-04 01:00:17 +01:00
|
|
|
{% set active_loans = loans|selectattr('status', 'equalto', 'active')|list %}
|
|
|
|
|
{% set returned_loans = loans|selectattr('status', 'equalto', 'returned')|list %}
|
2026-06-03 10:43:17 +01:00
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
<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 %}">
|
2026-06-03 10:43:17 +01:00
|
|
|
<div class="user-header">
|
|
|
|
|
<h3>{{ user.name }} ({{ user.identification }})</h3>
|
2026-06-04 01:00:17 +01:00
|
|
|
<span class="loan-count">
|
|
|
|
|
{{ active_loans|length }} active, {{ returned_loans|length }} past
|
|
|
|
|
</span>
|
2026-06-03 10:43:17 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
{% if active_loans %}
|
|
|
|
|
<div class="loans-section current-loans">
|
|
|
|
|
<h4>Current Loans</h4>
|
2026-06-20 01:20:39 +01:00
|
|
|
<div class="table-container">
|
2026-06-04 01:00:17 +01:00
|
|
|
<table class="loans-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Tablet</th>
|
|
|
|
|
<th>Serial Number</th>
|
|
|
|
|
<th>Loan Date</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2026-06-20 01:20:39 +01:00
|
|
|
{% for loan in active_loans %}
|
2026-06-04 01:00:17 +01:00
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
2026-06-20 01:20:39 +01:00
|
|
|
{% 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;">
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
{% if not active_loans and not returned_loans %}
|
2026-06-03 10:43:17 +01:00
|
|
|
<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>
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
<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>
|
|
|
|
|
|
2026-06-03 10:43:17 +01:00
|
|
|
<style>
|
|
|
|
|
.relationship-note {
|
|
|
|
|
background-color: #e8f5e9;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
border-left: 4px solid #4CAF50;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 10:43:17 +01:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 01:00:17 +01:00
|
|
|
.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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 10:43:17 +01:00
|
|
|
.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 {
|
2026-06-04 01:00:17 +01:00
|
|
|
background-color: #f5f5f5;
|
2026-06-03 10:43:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-06-04 01:00:17 +01:00
|
|
|
background-color: #9E9E9E;
|
2026-06-03 10:43:17 +01:00
|
|
|
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 %}
|