Student data import implementation: added tables, migration, import script, tests, and documentation.

This commit is contained in:
ijuanes 2026-06-23 19:57:49 +01:00
parent 9662f98169
commit 2959cd5299
19 changed files with 3186 additions and 79 deletions

139
templates/add_student.html Normal file
View file

@ -0,0 +1,139 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Add Student') }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<h1 class="page-title">{{ gettext('Add Student') }}</h1>
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Student Information') }}</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_student') }}">
<!-- Identification Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Identification') }}</legend>
<div class="row mb-3">
<label for="cial_code" class="col-sm-3 col-form-label">{{ gettext('CIAL Code') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="cial_code" name="cial_code"
value="{{ request.form.cial_code if request.method == 'POST' else '' }}"
required autofocus>
<div class="form-text">{{ gettext('Unique internal identification code') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="nif_nie_passport" class="col-sm-3 col-form-label">{{ gettext('NIF/NIE/Passport') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="nif_nie_passport" name="nif_nie_passport"
value="{{ request.form.nif_nie_passport if request.method == 'POST' else '' }}">
<div class="form-text">{{ gettext('National ID, Foreign ID, or Passport number') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="registration_number" class="col-sm-3 col-form-label">{{ gettext('Registration Number') }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="registration_number" name="registration_number"
value="{{ request.form.registration_number if request.method == 'POST' else '' }}">
</div>
</div>
<div class="row mb-3">
<label for="file_number" class="col-sm-3 col-form-label">{{ gettext('File Number') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="file_number" name="file_number"
value="{{ request.form.file_number if request.method == 'POST' else '' }}">
</div>
</div>
<div class="row mb-3">
<label for="order_number" class="col-sm-3 col-form-label">{{ gettext('Order Number') }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="order_number" name="order_number"
value="{{ request.form.order_number if request.method == 'POST' else '' }}">
</div>
</div>
</fieldset>
<!-- Personal Information Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Personal Information') }}</legend>
<div class="row mb-3">
<label for="last_name" class="col-sm-3 col-form-label">{{ gettext('Last Name') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="last_name" name="last_name"
value="{{ request.form.last_name if request.method == 'POST' else '' }}"
required>
</div>
</div>
<div class="row mb-3">
<label for="first_name" class="col-sm-3 col-form-label">{{ gettext('First Name') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="first_name" name="first_name"
value="{{ request.form.first_name if request.method == 'POST' else '' }}"
required>
</div>
</div>
<div class="row mb-3">
<label for="birth_date" class="col-sm-3 col-form-label">{{ gettext('Birth Date') }}</label>
<div class="col-sm-9">
<input type="date" class="form-control" id="birth_date" name="birth_date"
value="{{ request.form.birth_date if request.method == 'POST' else '' }}">
<div class="form-text">{{ gettext('Format: YYYY-MM-DD') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="gender" class="col-sm-3 col-form-label">{{ gettext('Gender') }}</label>
<div class="col-sm-9">
<select class="form-select" id="gender" name="gender">
<option value="">{{ gettext('Select...') }}</option>
<option value="M" {% if request.method == 'POST' and request.form.gender == 'M' %}selected{% endif %}>{{ gettext('Male') }}</option>
<option value="F" {% if request.method == 'POST' and request.form.gender == 'F' %}selected{% endif %}>{{ gettext('Female') }}</option>
<option value="O" {% if request.method == 'POST' and request.form.gender == 'O' %}selected{% endif %}>{{ gettext('Other') }}</option>
</select>
</div>
</div>
</fieldset>
<!-- Study Information Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Study Information') }}</legend>
<div class="row mb-3">
<label for="study_group" class="col-sm-3 col-form-label">{{ gettext('Study Group') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="study_group" name="study_group"
value="{{ request.form.study_group if request.method == 'POST' else '' }}">
<div class="form-text">{{ gettext('E.g., 1º FPB BÁSICA') }}</div>
</div>
</div>
</fieldset>
<!-- Form Actions -->
<div class="d-flex justify-content-end gap-2">
<a href="{{ url_for('list_students') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> {{ gettext('Cancel') }}
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {{ gettext('Save Student') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -194,6 +194,10 @@
class="px-3 py-2 text-sm sm:text-base text-white rounded hover:bg-cobalt-700 transition-colors whitespace-nowrap">
{{ _('User Loans') }}
</a>
<a href="/students"
class="px-3 py-2 text-sm sm:text-base text-white rounded hover:bg-cobalt-700 transition-colors whitespace-nowrap">
{{ _('Students') }}
</a>
<a href="/non_loanable_devices"
class="px-3 py-2 text-sm sm:text-base text-white rounded hover:bg-cobalt-700 transition-colors whitespace-nowrap">
{{ _('Non-Loanable Devices') }}

147
templates/edit_student.html Normal file
View file

@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Edit Student') }} - {{ student.full_name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('list_students') }}">{{ gettext('Students') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('student_detail', student_id=student.id) }}">{{ student.full_name }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ gettext('Edit') }}</li>
</ol>
</nav>
<h1 class="page-title">{{ gettext('Edit Student') }}: {{ student.full_name }}</h1>
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Student Information') }}</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('edit_student', student_id=student.id) }}">
<!-- Identification Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Identification') }}</legend>
<div class="row mb-3">
<label for="cial_code" class="col-sm-3 col-form-label">{{ gettext('CIAL Code') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="cial_code" name="cial_code"
value="{{ request.form.cial_code if request.method == 'POST' else student.cial_code }}"
required autofocus>
<div class="form-text">{{ gettext('Unique internal identification code') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="nif_nie_passport" class="col-sm-3 col-form-label">{{ gettext('NIF/NIE/Passport') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="nif_nie_passport" name="nif_nie_passport"
value="{{ request.form.nif_nie_passport if request.method == 'POST' else (student.nif_nie_passport or '') }}">
<div class="form-text">{{ gettext('National ID, Foreign ID, or Passport number') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="registration_number" class="col-sm-3 col-form-label">{{ gettext('Registration Number') }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="registration_number" name="registration_number"
value="{{ request.form.registration_number if request.method == 'POST' else (student.registration_number or '') }}">
</div>
</div>
<div class="row mb-3">
<label for="file_number" class="col-sm-3 col-form-label">{{ gettext('File Number') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="file_number" name="file_number"
value="{{ request.form.file_number if request.method == 'POST' else (student.file_number or '') }}">
</div>
</div>
<div class="row mb-3">
<label for="order_number" class="col-sm-3 col-form-label">{{ gettext('Order Number') }}</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="order_number" name="order_number"
value="{{ request.form.order_number if request.method == 'POST' else (student.order_number or '') }}">
</div>
</div>
</fieldset>
<!-- Personal Information Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Personal Information') }}</legend>
<div class="row mb-3">
<label for="last_name" class="col-sm-3 col-form-label">{{ gettext('Last Name') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="last_name" name="last_name"
value="{{ request.form.last_name if request.method == 'POST' else student.last_name }}"
required>
</div>
</div>
<div class="row mb-3">
<label for="first_name" class="col-sm-3 col-form-label">{{ gettext('First Name') }} *</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="first_name" name="first_name"
value="{{ request.form.first_name if request.method == 'POST' else student.first_name }}"
required>
</div>
</div>
<div class="row mb-3">
<label for="birth_date" class="col-sm-3 col-form-label">{{ gettext('Birth Date') }}</label>
<div class="col-sm-9">
<input type="date" class="form-control" id="birth_date" name="birth_date"
value="{{ request.form.birth_date if request.method == 'POST' else (student.birth_date or '') }}">
<div class="form-text">{{ gettext('Format: YYYY-MM-DD') }}</div>
</div>
</div>
<div class="row mb-3">
<label for="gender" class="col-sm-3 col-form-label">{{ gettext('Gender') }}</label>
<div class="col-sm-9">
<select class="form-select" id="gender" name="gender">
<option value="">{{ gettext('Select...') }}</option>
<option value="M" {% if request.method == 'POST' and request.form.gender == 'M' %}selected{% elif student.gender == 'M' %}selected{% endif %}>{{ gettext('Male') }}</option>
<option value="F" {% if request.method == 'POST' and request.form.gender == 'F' %}selected{% elif student.gender == 'F' %}selected{% endif %}>{{ gettext('Female') }}</option>
<option value="O" {% if request.method == 'POST' and request.form.gender == 'O' %}selected{% elif student.gender == 'O' %}selected{% endif %}>{{ gettext('Other') }}</option>
</select>
</div>
</div>
</fieldset>
<!-- Study Information Section -->
<fieldset class="mb-4">
<legend class="text-primary">{{ gettext('Study Information') }}</legend>
<div class="row mb-3">
<label for="study_group" class="col-sm-3 col-form-label">{{ gettext('Study Group') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="study_group" name="study_group"
value="{{ request.form.study_group if request.method == 'POST' else (student.study_group or '') }}">
<div class="form-text">{{ gettext('E.g., 1º FPB BÁSICA') }}</div>
</div>
</div>
</fieldset>
<!-- Form Actions -->
<div class="d-flex justify-content-end gap-2">
<a href="{{ url_for('student_detail', student_id=student.id) }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> {{ gettext('Cancel') }}
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {{ gettext('Update Student') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Import Students from CSV') }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<h1 class="page-title">{{ gettext('Import Students from CSV') }}</h1>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Instructions') }}</h5>
</div>
<div class="card-body">
<ul>
<li>{{ gettext('Upload a CSV file with student data') }}</li>
<li>{{ gettext('The file should be encoded in ISO-8859-1 (Latin-1)') }}</li>
<li>{{ gettext('Required columns: Apellidos, Nombre (will be split), C.I.A.L., Fecha Nac.') }}</li>
<li>{{ gettext('Optional columns: NIF/NIE/Pas., Registro, Expediente, Sexo, Grupo') }}</li>
<li>{{ gettext('The "Estudio" column will be discarded') }}</li>
<li>{{ gettext('Duplicate students (by CIAL code or NIF/NIE) will be skipped') }}</li>
</ul>
<p class="text-muted">
{{ gettext('Example CSV format:') }}<br>
<code>Nº Or.,"Apellidos, Nombre",Fecha Nac.,C.I.A.L.,NIF/NIE/Pas.,Registro,Expediente,Sexo,Grupo,Estudio</code>
</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Upload CSV File') }}</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('import_students') }}" enctype="multipart/form-data">
<div class="mb-3">
<label for="csv_file" class="form-label">{{ gettext('CSV File') }} *</label>
<input class="form-control" type="file" id="csv_file" name="csv_file"
accept=".csv" required>
<div class="form-text">{{ gettext('Select a CSV file to upload') }}</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="{{ url_for('list_students') }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> {{ gettext('Cancel') }}
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload"></i> {{ gettext('Import Students') }}
</button>
</div>
</form>
</div>
</div>
<!-- Sample Data Preview -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Sample Data Format') }}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-sm mb-0">
<thead class="table-light">
<tr>
<th>{{ gettext('Nº Or.') }}</th>
<th>{{ gettext('Apellidos, Nombre') }}</th>
<th>{{ gettext('Fecha Nac.') }}</th>
<th>{{ gettext('C.I.A.L.') }}</th>
<th>{{ gettext('NIF/NIE/Pas.') }}</th>
<th>{{ gettext('Registro') }}</th>
<th>{{ gettext('Expediente') }}</th>
<th>{{ gettext('Sexo') }}</th>
<th>{{ gettext('Grupo') }}</th>
<th>{{ gettext('Estudio') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>PERIQUITA DE LOS PALOTES, JUANITA</td>
<td>24/08/2009</td>
<td>B19L64119K</td>
<td>24587243H</td>
<td>4847</td>
<td></td>
<td>M</td>
<td>1º FPB BÁSICA</td>
<td>1º CFGB Servicios Socioculturales...</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3 text-muted small">
{{ gettext('Note: The "Estudio" field will be discarded during import.') }}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,78 +1,113 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Loan Tablet') }}{% endblock %}
{% 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 class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 offset-lg-2">
<h1 class="page-title">{{ gettext('Loan Tablet') }}</h1>
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Loan Information') }}</h5>
</div>
<div class="card-body">
<form method="POST" action="/loan_tablet">
<!-- Tablet Selection -->
<div class="mb-3">
<label for="tablet_id" class="form-label">{{ gettext('Tablet') }} *</label>
<input type="text" id="tablet_search" class="form-control mb-2"
placeholder="{{ gettext('Search tablets (brand, model, or serial)...') }}"
onkeyup="filterDropdown('tablet_search', 'tablet_id')">
<select id="tablet_id" name="tablet_id" class="form-select" required size="5">
<option value="">{{ gettext('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 class="form-text">{{ gettext('Select which tablet to loan') }}</div>
</div>
<!-- User Selection (Staff) -->
<div class="mb-3">
<label for="user_id" class="form-label">{{ gettext('Staff User') }} *</label>
<input type="text" id="user_search" class="form-control mb-2"
placeholder="{{ gettext('Search users (name or ID)...') }}"
onkeyup="filterDropdown('user_search', 'user_id')">
<select id="user_id" name="user_id" class="form-select" required size="5">
<option value="">{{ gettext('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 class="form-text">{{ gettext('Select the staff member processing the loan') }}</div>
</div>
<!-- Student Selection (Optional) -->
<div class="mb-3">
<label for="student_id" class="form-label">{{ gettext('Student (Optional)') }}</label>
<input type="text" id="student_search" class="form-control mb-2"
placeholder="{{ gettext('Search students (name, CIAL, or NIF)...') }}"
onkeyup="filterDropdown('student_search', 'student_id')">
<select id="student_id" name="student_id" class="form-select" size="5">
<option value="">{{ gettext('Select a student (optional)') }}</option>
{% for student in students %}
<option value="{{ student.id }}"
data-search="{{ student.last_name|lower }} {{ student.first_name|lower }} {{ student.cial_code|lower }} {{ student.nif_nie_passport|lower }}">
{{ student.last_name }}, {{ student.first_name }} ({{ student.cial_code }})
</option>
{% endfor %}
</select>
<div class="form-text">{{ gettext('Optionally associate this loan with a student') }}</div>
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-end gap-2">
<a href="/" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> {{ gettext('Cancel') }}
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {{ gettext('Loan Tablet') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</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>
<script>
function filterDropdown(searchId, selectId) {
const search = document.getElementById(searchId).value.toLowerCase();
const select = document.getElementById(selectId);
const options = select.options;
<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;
}
for (let i = 1; i < options.length; i++) {
const searchText = options[i].getAttribute('data-search') || '';
options[i].style.display = searchText.includes(search) ? '' : 'none';
}
</script>
<style>
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 5px;
// Show first visible option if search is empty, otherwise keep selection
if (search === '') {
select.selectedIndex = 0;
}
select[size] {
width: 100%;
max-height: 200px;
overflow-y: auto;
}
</style>
}
</script>
<style>
select[size] {
width: 100%;
max-height: 200px;
overflow-y: auto;
}
</style>
{% endblock %}

View file

@ -0,0 +1,183 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Student Details') }} - {{ student.full_name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('list_students') }}">{{ gettext('Students') }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ student.full_name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="page-title mb-0">{{ student.full_name }}</h1>
<div class="btn-group" role="group">
<a href="{{ url_for('edit_student', student_id=student.id) }}" class="btn btn-warning">
<i class="bi bi-pencil"></i> {{ gettext('Edit') }}
</a>
<a href="{{ url_for('delete_student', student_id=student.id) }}" class="btn btn-danger"
onclick="return confirm('{{ gettext('Are you sure you want to delete this student?') }}')">
<i class="bi bi-trash"></i> {{ gettext('Delete') }}
</a>
</div>
</div>
<div class="row">
<!-- Student Information Card -->
<div class="col-12 col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">{{ gettext('Student Information') }}</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">{{ gettext('ID') }}:</dt>
<dd class="col-sm-8">{{ student.id }}</dd>
<dt class="col-sm-4">{{ gettext('CIAL Code') }}:</dt>
<dd class="col-sm-8">{{ student.cial_code }}</dd>
<dt class="col-sm-4">{{ gettext('NIF/NIE/Passport') }}:</dt>
<dd class="col-sm-8">{{ student.nif_nie_passport or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Registration Number') }}:</dt>
<dd class="col-sm-8">{{ student.registration_number or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('File Number') }}:</dt>
<dd class="col-sm-8">{{ student.file_number or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Order Number') }}:</dt>
<dd class="col-sm-8">{{ student.order_number or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Full Name') }}:</dt>
<dd class="col-sm-8">{{ student.full_name }}</dd>
<dt class="col-sm-4">{{ gettext('First Name') }}:</dt>
<dd class="col-sm-8">{{ student.first_name }}</dd>
<dt class="col-sm-4">{{ gettext('Last Name') }}:</dt>
<dd class="col-sm-8">{{ student.last_name }}</dd>
<dt class="col-sm-4">{{ gettext('Birth Date') }}:</dt>
<dd class="col-sm-8">{{ student.birth_date or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Gender') }}:</dt>
<dd class="col-sm-8">{{ student.gender or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Study Group') }}:</dt>
<dd class="col-sm-8">{{ student.study_group or '-' }}</dd>
<dt class="col-sm-4">{{ gettext('Created') }}:</dt>
<dd class="col-sm-8">{{ student.created_at }}</dd>
<dt class="col-sm-4">{{ gettext('Updated') }}:</dt>
<dd class="col-sm-8">{{ student.updated_at }}</dd>
</dl>
</div>
</div>
</div>
<!-- Assigned Devices Card -->
<div class="col-12 col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5 class="mb-0">{{ gettext('Assigned Devices') }}</h5>
</div>
<div class="card-body">
<!-- Assigned Tablets -->
<h6>{{ gettext('Tablets') }}</h6>
{% if assigned_tablets %}
<ul class="list-group list-group-flush mb-3">
{% for tablet in assigned_tablets %}
<li class="list-group-item">
<strong>{{ tablet.brand }} {{ tablet.model }}</strong><br>
<small class="text-muted">{{ gettext('Serial:') }} {{ tablet.serial_number }}</small><br>
<small class="text-muted">{{ gettext('Status:') }} {{ tablet.status }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">{{ gettext('No tablets assigned') }}</p>
{% endif %}
<!-- Assigned Non-Loanable Devices -->
<h6>{{ gettext('Other Devices') }}</h6>
{% if assigned_devices %}
<ul class="list-group list-group-flush">
{% for device in assigned_devices %}
<li class="list-group-item">
<strong>{{ device.brand }} {{ device.model }}</strong><br>
<small class="text-muted">{{ gettext('Type:') }} {{ device.device_type }}</small><br>
<small class="text-muted">{{ gettext('Serial:') }} {{ device.serial_number }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">{{ gettext('No other devices assigned') }}</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Loan History Card -->
<div class="col-12 mb-4">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="mb-0">{{ gettext('Loan History') }}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead class="table-light">
<tr>
<th>{{ gettext('ID') }}</th>
<th>{{ gettext('Tablet') }}</th>
<th>{{ gettext('Loan Date') }}</th>
<th>{{ gettext('Return Date') }}</th>
<th>{{ gettext('Status') }}</th>
</tr>
</thead>
<tbody>
{% if loans %}
{% for loan in loans %}
<tr>
<td>{{ loan.id }}</td>
<td>{{ loan.brand }} {{ loan.model }} ({{ loan.serial_number }})</td>
<td>{{ loan.loan_date }}</td>
<td>{{ loan.return_date or '-' }}</td>
<td>
<span class="badge bg-{{ 'success' if loan.status == 'returned' else 'primary' }}">
{{ loan.status }}
</span>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" class="text-center text-muted">
{{ gettext('No loan history') }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="text-center">
<a href="{{ url_for('list_students') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {{ gettext('Back to Students') }}
</a>
</div>
</div>
</div>
</div>
{% endblock %}

182
templates/students.html Normal file
View file

@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block title %}{{ gettext('Students') }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="page-title">{{ gettext('Students') }}</h1>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-muted">{{ gettext('Total students:') }} {{ total_students }}</p>
</div>
<div class="btn-group" role="group">
<a href="{{ url_for('add_student') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> {{ gettext('Add Student') }}
</a>
<a href="{{ url_for('import_students') }}" class="btn btn-secondary">
<i class="bi bi-upload"></i> {{ gettext('Import CSV') }}
</a>
</div>
</div>
</div>
<!-- Search and Filter Form -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Search & Filter') }}</h5>
</div>
<div class="card-body">
<form method="get" action="{{ url_for('list_students') }}" class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">{{ gettext('Search') }}</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="{{ gettext('Name, CIAL, NIF...') }}">
</div>
<div class="col-md-2">
<label for="gender" class="form-label">{{ gettext('Gender') }}</label>
<select class="form-select" id="gender" name="gender">
<option value="">{{ gettext('All') }}</option>
{% for g in genders %}
<option value="{{ g }}" {% if gender == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="group" class="form-label">{{ gettext('Study Group') }}</label>
<select class="form-select" id="group" name="group">
<option value="">{{ gettext('All') }}</option>
{% for g in groups %}
<option value="{{ g }}" {% if group == g %}selected{% endif %}>{{ g }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i> {{ gettext('Search') }}
</button>
</div>
<div class="col-md-1 d-flex align-items-end">
<a href="{{ url_for('list_students') }}" class="btn btn-secondary w-100">
<i class="bi bi-x-circle"></i> {{ gettext('Clear') }}
</a>
</div>
</form>
</div>
</div>
<!-- Students Table -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">{{ gettext('Student List') }}</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead class="table-light">
<tr>
<th>{{ gettext('ID') }}</th>
<th>{{ gettext('Name') }}</th>
<th>{{ gettext('CIAL Code') }}</th>
<th>{{ gettext('NIF/NIE') }}</th>
<th>{{ gettext('Gender') }}</th>
<th>{{ gettext('Study Group') }}</th>
<th>{{ gettext('Birth Date') }}</th>
<th class="text-end">{{ gettext('Actions') }}</th>
</tr>
</thead>
<tbody>
{% if students %}
{% for student in students %}
<tr>
<td>{{ student.id }}</td>
<td>
<a href="{{ url_for('student_detail', student_id=student.id) }}">
{{ student.last_name }}, {{ student.first_name }}
</a>
</td>
<td>{{ student.cial_code }}</td>
<td>{{ student.nif_nie_passport or '-' }}</td>
<td>{{ student.gender or '-' }}</td>
<td>{{ student.study_group or '-' }}</td>
<td>{{ student.birth_date or '-' }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('student_detail', student_id=student.id) }}"
class="btn btn-info btn-sm" title="{{ gettext('View') }}">
<i class="bi bi-eye"></i>
</a>
<a href="{{ url_for('edit_student', student_id=student.id) }}"
class="btn btn-warning btn-sm" title="{{ gettext('Edit') }}">
<i class="bi bi-pencil"></i>
</a>
<a href="{{ url_for('delete_student', student_id=student.id) }}"
class="btn btn-danger btn-sm"
onclick="return confirm('{{ gettext('Are you sure you want to delete this student?') }}')"
title="{{ gettext('Delete') }}">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="8" class="text-center text-muted">
{{ gettext('No students found') }}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="card-footer">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
{% if page > 1 %}
<li class="page-item">
<a class="page-link" href="?page={{ page - 1 }}&search={{ search }}&gender={{ gender }}&group={{ group }}">
{{ gettext('Previous') }}
</a>
</li>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<li class="page-item active"><span class="page-link">{{ p }}</span></li>
{% elif p <= 3 or p > total_pages - 3 or (p >= page - 2 and p <= page + 2) %}
<li class="page-item">
<a class="page-link" href="?page={{ p }}&search={{ search }}&gender={{ gender }}&group={{ group }}">{{ p }}</a>
</li>
{% elif p == 4 or p == total_pages - 3 %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<li class="page-item">
<a class="page-link" href="?page={{ page + 1 }}&search={{ search }}&gender={{ gender }}&group={{ group }}">
{{ gettext('Next') }}
</a>
</li>
{% endif %}
</ul>
</nav>
<div class="text-muted text-center mt-2">
{{ gettext('Showing') }} {{ (page - 1) * per_page + 1 }}-
{{ min(page * per_page, total_students) }} {{ gettext('of') }} {{ total_students }}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}