- Add Flask-Babel for internationalization - Configure app for Spanish (default) and English - Add language switcher in navigation - Wrap all UI text in templates with gettext() - Create translation directory structure - Add complete Spanish translations (58 strings) - Add English translations (template) - Compile .po to .mo files - Add extract_translations.py script for future updates - Add compile_translations.py script Spanish is now the default language, with English available via ?lang=en Translated strings include: - Navigation (Home, Add Tablet, Add User, etc.) - User Loans interface (Search, Filter, Status, etc.) - Table headers (User, Tablet, Serial Number, etc.) - Buttons (Loan, Return, Search, etc.) - Messages (No users found, No active loans, etc.) - All UI text across all templates
122 lines
5.2 KiB
HTML
122 lines
5.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<h2 class="text-xl font-semibold text-gray-800">{{ _('User Loans') }}</h2>
|
|
<div class="text-sm text-gray-500">
|
|
<span id="result-count" hx-swap-oob="true">{{ total_users }} {{ _('users') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filters Card -->
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<form id="search-form"
|
|
hx-get="/user_loans"
|
|
hx-target="#results-container"
|
|
hx-swap="innerHTML"
|
|
hx-include="[name='search'], [name='status'], [name='page']"
|
|
class="space-y-4">
|
|
|
|
<!-- Search Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<!-- Search Input -->
|
|
<div class="md:col-span-2">
|
|
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">
|
|
{{ _('Search Users') }}
|
|
</label>
|
|
<div class="relative">
|
|
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"
|
|
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
<input
|
|
type="text"
|
|
id="search"
|
|
name="search"
|
|
value="{{ search or '' }}"
|
|
placeholder="{{ _('Search by name or identification...') }}"
|
|
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Filter -->
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">
|
|
{{ _('Loan Status') }}
|
|
</label>
|
|
<select
|
|
id="status"
|
|
name="status"
|
|
class="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
|
>
|
|
<option value="" {% if not status %}selected{% endif %}>{{ _('All Users') }}</option>
|
|
<option value="with_loans" {% if status == 'with_loans' %}selected{% endif %}>{{ _('With Active Loans') }}</option>
|
|
<option value="no_loans" {% if status == 'no_loans' %}selected{% endif %}>{{ _('No Active Loans') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<button type="submit"
|
|
class="w-full sm:w-auto px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors flex items-center justify-center gap-2">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
{{ _('Search') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Loading Indicator -->
|
|
<div id="loading" class="htmx-indicator hidden"></div>
|
|
|
|
<!-- Results Container -->
|
|
<div id="results-container">
|
|
{% include 'components/user_loans_results.html' %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HTMX Script to handle URL updates -->
|
|
<script>
|
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
|
if (evt.detail.requestConfig.method === 'get' && evt.detail.successful) {
|
|
// Update URL without reload
|
|
const url = new URL(window.location);
|
|
const form = document.getElementById('search-form');
|
|
if (form) {
|
|
const formData = new FormData(form);
|
|
for (let [key, value] of formData.entries()) {
|
|
if (value) {
|
|
url.searchParams.set(key, value);
|
|
} else {
|
|
url.searchParams.delete(key);
|
|
}
|
|
}
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const form = document.getElementById('search-form');
|
|
if (form) {
|
|
for (let [key, value] of urlParams.entries()) {
|
|
const input = form.querySelector(`[name="${key}"]`);
|
|
if (input) {
|
|
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
input.checked = input.value === value;
|
|
} else {
|
|
input.value = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|