feat(loans): add search and expandable past loans to user loans view

- Add search box to filter by user name, ID, tablet brand/model/serial
- Split loans into Current Loans (always visible) and Past Loans (collapsible)
- Past loans toggle with ▶/▼ indicator showing count
- All searchable data embedded in data-search attributes

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
ijuanes 2026-06-04 01:00:17 +01:00
parent 8a8d2f02fa
commit ec79bc2ca3

View file

@ -7,18 +7,67 @@
This page demonstrates the many-to-many relationship between users and tablets via the loans table.
</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 %}
{% set user = user_data.user %}
{% 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">
<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>
{% if loans %}
{% if active_loans %}
<div class="loans-section current-loans">
<h4>Current Loans</h4>
<table class="loans-table">
<thead>
<tr>
<th>Tablet</th>
<th>Serial Number</th>
<th>Loan Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for loan in active_loans %}
<tr class="loan-row status-{{ loan.status }}">
<td>{{ loan.brand }} {{ loan.model }}</td>
<td>{{ loan.serial_number }}</td>
<td>{{ loan.loan_date }}</td>
<td>
<span class="status-badge status-{{ loan.status }}">
{{ loan.status }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if returned_loans %}
<div class="loans-section past-loans">
<h4>
<button class="expand-toggle" onclick="togglePastLoans(this)">
▶ Past Loans ({{ returned_loans|length }})
</button>
</h4>
<div class="past-loans-content" style="display: none;">
<table class="loans-table">
<thead>
<tr>
@ -30,7 +79,7 @@
</tr>
</thead>
<tbody>
{% for loan in loans %}
{% for loan in returned_loans %}
<tr class="loan-row status-{{ loan.status }}">
<td>{{ loan.brand }} {{ loan.model }}</td>
<td>{{ loan.serial_number }}</td>
@ -45,7 +94,11 @@
{% endfor %}
</tbody>
</table>
{% else %}
</div>
</div>
{% endif %}
{% if not active_loans and not returned_loans %}
<p class="no-loans">No loans recorded for this user.</p>
{% endif %}
</div>
@ -56,6 +109,32 @@
{% endif %}
</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>
.relationship-note {
background-color: #e8f5e9;
@ -65,6 +144,23 @@
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 {
display: flex;
flex-direction: column;
@ -98,6 +194,28 @@
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 {
width: 100%;
border-collapse: collapse;
@ -120,7 +238,7 @@
}
.loan-row.status-returned {
background-color: #e8f5e9;
background-color: #f5f5f5;
}
.status-badge {
@ -137,7 +255,7 @@
}
.status-badge.status-returned {
background-color: #4CAF50;
background-color: #9E9E9E;
color: white;
}