Student data import implementation: added tables, migration, import script, tests, and documentation.
This commit is contained in:
parent
9662f98169
commit
2959cd5299
19 changed files with 3186 additions and 79 deletions
139
templates/add_student.html
Normal file
139
templates/add_student.html
Normal 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 %}
|
||||
|
|
@ -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
147
templates/edit_student.html
Normal 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 %}
|
||||
102
templates/import_students.html
Normal file
102
templates/import_students.html
Normal 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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
183
templates/student_detail.html
Normal file
183
templates/student_detail.html
Normal 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
182
templates/students.html
Normal 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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue