Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b2b462c52 | |||
| bd7ef61f5a | |||
| df0f7d3066 | |||
| 2215fc76d8 | |||
| e177936359 | |||
| ae4a7226fa | |||
| 16a10a8c09 | |||
| 3d4048d23b |
+121
-37
@@ -68,6 +68,7 @@ import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import secrets
|
||||
import importlib
|
||||
import atexit
|
||||
try:
|
||||
redis = importlib.import_module('redis')
|
||||
except Exception:
|
||||
@@ -152,7 +153,7 @@ def print(*args, **kwargs):
|
||||
app.logger.info(message)
|
||||
|
||||
|
||||
STUDENT_CARD_ENCRYPTED_FIELDS = ('SchülerName', 'Klasse', 'Notizen')
|
||||
STUDENT_CARD_ENCRYPTED_FIELDS = ('ausweis_ident', 'SchülerName', 'Klasse', 'Notizen')
|
||||
|
||||
|
||||
def _decrypt_student_card_doc(card_doc):
|
||||
@@ -2149,9 +2150,8 @@ def generate_ausweis_id_excel(existing_ids_set):
|
||||
else:
|
||||
print(f"Already found: {new_id}, trying another...")
|
||||
|
||||
|
||||
def _upload_student_cards_excel():
|
||||
"""Bulk import student cards from Excel with automatic name/class mapping."""
|
||||
"""Bulk import student cards from Excel with automatic name/class mapping, rollover support, and encryption."""
|
||||
if 'username' not in session:
|
||||
flash('Nicht angemeldet.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
@@ -2176,6 +2176,8 @@ def _upload_student_cards_excel():
|
||||
flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
|
||||
rollover_mode = request.form.get('rollover_mode') in ['true', '1', 'on']
|
||||
|
||||
try:
|
||||
header_row, data_rows = _load_tabular_upload(excel_file)
|
||||
except Exception as exc:
|
||||
@@ -2190,6 +2192,8 @@ def _upload_student_cards_excel():
|
||||
|
||||
synonyms = {
|
||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
||||
'differenzierungsmerkmal'],
|
||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||
@@ -2207,6 +2211,7 @@ def _upload_student_cards_excel():
|
||||
|
||||
mapped_indices = {
|
||||
'ausweis_id': col_index('ausweis_id'),
|
||||
'ausweis_ident': col_index('ausweis_ident'),
|
||||
'first_name': col_index('first_name'),
|
||||
'last_name': col_index('last_name'),
|
||||
'class_name': col_index('class_name'),
|
||||
@@ -2215,7 +2220,7 @@ def _upload_student_cards_excel():
|
||||
}
|
||||
|
||||
validation_only = (request.form.get('excel_action') or '').strip().lower() == 'validate'
|
||||
max_rows = 15000
|
||||
max_rows = 1500
|
||||
|
||||
planned_rows = []
|
||||
validation_errors = []
|
||||
@@ -2225,8 +2230,9 @@ def _upload_student_cards_excel():
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
try:
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards_col = db['student_cards']
|
||||
|
||||
student_cards_cursor = db['student_cards'].find({}, {'AusweisId': 1})
|
||||
student_cards_cursor = student_cards_col.find({}, {'AusweisId': 1})
|
||||
existing_ids.update(
|
||||
str(card.get('AusweisId', '')).strip().upper()
|
||||
for card in student_cards_cursor
|
||||
@@ -2247,12 +2253,11 @@ def _upload_student_cards_excel():
|
||||
return row_values[idx]
|
||||
|
||||
ausweis_id = sanitize_form_value(val('ausweis_id'))
|
||||
class_name = sanitize_form_value(val('class_name'))
|
||||
ausweis_ident = sanitize_form_value(val('ausweis_ident'))
|
||||
class_name = sanitize_form_value(val('class_name')) or ""
|
||||
notes = sanitize_form_value(val('notes'))
|
||||
|
||||
default_borrow_days = _excel_int(val('default_borrow_days'))
|
||||
if not default_borrow_days:
|
||||
default_borrow_days = 14
|
||||
default_borrow_days = _excel_int(val('default_borrow_days')) or 14
|
||||
|
||||
first_name = sanitize_form_value(val('first_name')) or ""
|
||||
last_name = sanitize_form_value(val('last_name')) or ""
|
||||
@@ -2262,21 +2267,23 @@ def _upload_student_cards_excel():
|
||||
continue
|
||||
|
||||
row_errors = []
|
||||
|
||||
if not student_name:
|
||||
row_errors.append('Vorname und Nachname fehlen')
|
||||
|
||||
if not ausweis_id and student_name:
|
||||
ausweis_id = generate_ausweis_id_excel(existing_ids)
|
||||
ausweis_id = generate_ausweis_id(existing_ids)
|
||||
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
||||
existing_ids.add(ausweis_id.upper())
|
||||
elif ausweis_id:
|
||||
elif ausweis_id and not rollover_mode:
|
||||
ausweis_id = str(ausweis_id).strip().upper()
|
||||
if ausweis_id in existing_ids:
|
||||
row_errors.append(f'Ausweis-ID {ausweis_id} existiert bereits')
|
||||
else:
|
||||
existing_ids.add(ausweis_id)
|
||||
|
||||
if not ausweis_ident:
|
||||
ausweis_ident = ausweis_id
|
||||
|
||||
if row_errors:
|
||||
validation_errors.append((row_number, '; '.join(row_errors)))
|
||||
continue
|
||||
@@ -2284,65 +2291,134 @@ def _upload_student_cards_excel():
|
||||
planned_rows.append({
|
||||
'row_number': row_number,
|
||||
'ausweis_id': ausweis_id,
|
||||
'ausweis_ident': ausweis_ident,
|
||||
'student_name': student_name,
|
||||
'class_name': class_name,
|
||||
'notes': notes,
|
||||
'default_borrow_days': default_borrow_days,
|
||||
})
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if validation_errors:
|
||||
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
||||
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
if validation_errors:
|
||||
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
||||
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
|
||||
if validation_only:
|
||||
warning_text = ''
|
||||
if validation_warnings:
|
||||
warning_details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])
|
||||
warning_text = f' Hinweise: {warning_details}'
|
||||
flash(f'Validierung erfolgreich: {len(planned_rows)} Ausweise würden importiert.{warning_text}', 'success')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
matched_db_doc_ids = set()
|
||||
rows_to_create = []
|
||||
matched_count = 0
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
try:
|
||||
db = client[cfg.MONGODB_DB]
|
||||
student_cards = db['student_cards']
|
||||
if rollover_mode:
|
||||
raw_db_cards = list(student_cards_col.find())
|
||||
decrypted_db_cards = []
|
||||
|
||||
for doc in raw_db_cards:
|
||||
dec_name = decrypt_text(doc.get('SchülerName')) if doc.get('SchülerName') else ""
|
||||
dec_class = decrypt_text(doc.get('Klasse')) if doc.get('Klasse') else ""
|
||||
|
||||
raw_ident = doc.get('ausweis_ident') or doc.get('AusweisIdent') or ""
|
||||
dec_ident = raw_ident
|
||||
if raw_ident and str(raw_ident).startswith("gAAAAA"):
|
||||
try:
|
||||
dec_ident = decrypt_text(raw_ident)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
decrypted_db_cards.append({
|
||||
'_id': doc['_id'],
|
||||
'AusweisId': str(doc.get('AusweisId', '')).strip().upper(),
|
||||
'AusweisIdent': str(dec_ident or '').strip().upper(),
|
||||
'SchülerName': str(dec_name or '').strip().lower(),
|
||||
'Klasse': str(dec_class or '').strip().lower(),
|
||||
})
|
||||
|
||||
for excel_row in planned_rows:
|
||||
ex_ident = str(excel_row['ausweis_ident'] or '').strip().upper()
|
||||
ex_name = str(excel_row['student_name'] or '').strip().lower()
|
||||
ex_class = str(excel_row['class_name'] or '').strip().lower()
|
||||
|
||||
match_found = None
|
||||
for db_card in decrypted_db_cards:
|
||||
if db_card['_id'] in matched_db_doc_ids:
|
||||
continue
|
||||
|
||||
ident_matches = ex_ident and (ex_ident == db_card['AusweisIdent'])
|
||||
secondary_matches = (ex_name and ex_name == db_card['SchülerName']) and \
|
||||
(ex_class and ex_class == db_card['Klasse'])
|
||||
|
||||
if ident_matches or secondary_matches:
|
||||
match_found = db_card
|
||||
break
|
||||
|
||||
if match_found:
|
||||
matched_db_doc_ids.add(match_found['_id'])
|
||||
matched_count += 1
|
||||
else:
|
||||
rows_to_create.append(excel_row)
|
||||
|
||||
db_ids_to_delete = [
|
||||
doc['_id'] for doc in raw_db_cards
|
||||
if doc['_id'] not in matched_db_doc_ids
|
||||
]
|
||||
else:
|
||||
rows_to_create = planned_rows
|
||||
db_ids_to_delete = []
|
||||
|
||||
if validation_only:
|
||||
warning_text = f" Hinweise: {'; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])}" if validation_warnings else ""
|
||||
if rollover_mode:
|
||||
flash(
|
||||
f'Validierung erfolgreich ({len(planned_rows)} Excel-Zeilen). '
|
||||
f'Abgleich-Vorschau: {matched_count} unverändert, {len(rows_to_create)} neu, {len(db_ids_to_delete)} zum Löschen.{warning_text}',
|
||||
'success'
|
||||
)
|
||||
else:
|
||||
flash(f'Validierung erfolgreich: {len(planned_rows)} Ausweise würden importiert.{warning_text}',
|
||||
'success')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
|
||||
deleted_count = 0
|
||||
if db_ids_to_delete:
|
||||
res = student_cards_col.delete_many({'_id': {'$in': db_ids_to_delete}})
|
||||
deleted_count = res.deleted_count
|
||||
|
||||
created_total = 0
|
||||
for row in planned_rows:
|
||||
for row in rows_to_create:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
{
|
||||
'ausweis_ident': row['ausweis_ident'],
|
||||
'SchülerName': row['student_name'],
|
||||
'Klasse': row['class_name'],
|
||||
'Notizen': row['notes'],
|
||||
},
|
||||
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||
)
|
||||
student_cards.insert_one({
|
||||
student_cards_col.insert_one({
|
||||
'AusweisId': row['ausweis_id'],
|
||||
'StandardAusleihdauer': int(row['default_borrow_days']),
|
||||
'Erstellt': datetime.datetime.now(),
|
||||
**encrypted_payload,
|
||||
})
|
||||
created_total += 1
|
||||
|
||||
except Exception as exc:
|
||||
app.logger.error(f'Error importing student cards from Excel: {exc}')
|
||||
flash(f'Fehler beim Import der Bibliotheksausweise', 'error')
|
||||
flash('Fehler beim Import der Bibliotheksausweise.', 'error')
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if validation_warnings:
|
||||
warning_details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])
|
||||
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert. Hinweise: {warning_details}', 'warning')
|
||||
warning_details = f" Hinweise: {'; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])}" if validation_warnings else ""
|
||||
if rollover_mode:
|
||||
flash(
|
||||
f'Schuljahres-Abgleich erfolgreich: {matched_count} Schüler beibehalten, '
|
||||
f'{created_total} neu hinzugefügt, {deleted_count} alte Einträge gelöscht.{warning_details}',
|
||||
'success'
|
||||
)
|
||||
else:
|
||||
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert.', 'success')
|
||||
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert.{warning_details}', 'success')
|
||||
|
||||
return redirect(url_for('student_cards_admin'))
|
||||
|
||||
|
||||
def _upload_excel_items(scope='inventory'):
|
||||
"""Bulk import inventory/library items from Excel with validation-first workflow."""
|
||||
if 'username' not in session:
|
||||
@@ -4085,6 +4161,7 @@ def student_cards_admin():
|
||||
edit_mode = True
|
||||
form_data = {
|
||||
'card_id': str(card['_id']),
|
||||
'ausweis_ident': card.get('ausweis_ident', ''),
|
||||
'ausweis_id': card.get('AusweisId', ''),
|
||||
'student_name': card.get('SchülerName', ''),
|
||||
'default_borrow_days': card.get('StandardAusleihdauer', 14),
|
||||
@@ -4099,6 +4176,7 @@ def student_cards_admin():
|
||||
if request.method == 'POST':
|
||||
action = request.form.get('action', 'add')
|
||||
ausweis_id = request.form.get('ausweis_id', '').strip().upper()
|
||||
ausweis_ident = request.form.get('ausweis_ident', '').strip()
|
||||
student_name = request.form.get('student_name', '').strip()
|
||||
student_name_alias = student_name
|
||||
default_borrow_days = request.form.get('default_borrow_days', 14)
|
||||
@@ -4124,9 +4202,12 @@ def student_cards_admin():
|
||||
existing = student_cards.find_one({'AusweisId': ausweis_id, '_id': {'$ne': ObjectId(card_id)}})
|
||||
if existing:
|
||||
flash('Diese Ausweis-ID existiert bereits.', 'error')
|
||||
if not ausweis_ident:
|
||||
ausweis_ident = ausweis_id
|
||||
else:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
{
|
||||
'ausweis_ident': ausweis_ident,
|
||||
'SchülerName': student_name_alias,
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
@@ -4158,6 +4239,8 @@ def student_cards_admin():
|
||||
else:
|
||||
ausweis_id = generate_ausweis_id()
|
||||
existing = False
|
||||
if not ausweis_ident:
|
||||
ausweis_ident = ausweis_id
|
||||
|
||||
if existing:
|
||||
flash('Diese ID existiert bereits.', 'error')
|
||||
@@ -4165,6 +4248,7 @@ def student_cards_admin():
|
||||
try:
|
||||
encrypted_payload = encrypt_document_fields(
|
||||
{
|
||||
'ausweis_ident': ausweis_ident,
|
||||
'SchülerName': student_name_alias,
|
||||
'Klasse': class_name,
|
||||
'Notizen': notes,
|
||||
|
||||
@@ -704,6 +704,41 @@ def get_user_by_student_card(student_card_id):
|
||||
# Do not call dp.decrypt_text() here because found_user is a MongoDB dictionary.
|
||||
return found_user
|
||||
|
||||
def get_user_by_student_ident(student_ident):
|
||||
"""Return user dict by student ident by decrypting all cards and matching."""
|
||||
if not student_ident:
|
||||
return None
|
||||
|
||||
# Normalisiere den Suchbegriff, den wir finden wollen
|
||||
normalized_target = str(student_ident).strip()
|
||||
if not normalized_target:
|
||||
return None
|
||||
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
try:
|
||||
db = _get_tenant_db(client)
|
||||
all_student_cards = db['student_cards'].find()
|
||||
|
||||
for user_doc in all_student_cards:
|
||||
encrypted_ident = user_doc.get('AusweisIdent')
|
||||
|
||||
if encrypted_ident:
|
||||
try:
|
||||
decrypted_ident = dp.decrypt_text(encrypted_ident)
|
||||
|
||||
if decrypted_ident and str(decrypted_ident).strip() == normalized_target:
|
||||
return user_doc
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Entschlüsselungsfehler bei ID {user_doc.get('_id')}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as exc:
|
||||
app.logger.error(f"Datenbankfehler in get_user_by_student_ident: {exc}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return None
|
||||
|
||||
def make_admin(username):
|
||||
"""Grant administrator privileges to a user."""
|
||||
|
||||
@@ -1342,7 +1342,7 @@
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||||
{% if current_permissions.pages.get('library_view', False) %}
|
||||
<li class="nav-item" data-nav-fixed="true">
|
||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
|
||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien/Ausleihe</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'username' in session %}
|
||||
|
||||
@@ -18,14 +18,58 @@
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Neu strukturiertes Layout für Formular & Import */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.student-card-form {
|
||||
background: var(--ui-bg);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.import-card {
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #f8fbff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rollover-box {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeeba;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rollover-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #856404;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rollover-hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: #856404;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
@@ -35,6 +79,10 @@
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-row.full-width {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -47,7 +95,8 @@
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
@@ -55,7 +104,8 @@
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
|
||||
@@ -65,14 +115,16 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-actions button {
|
||||
.form-actions button, .form-actions a {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
@@ -126,15 +178,18 @@
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-edit, .btn-delete {
|
||||
.btn-edit, .btn-delete, .btn-export {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
@@ -164,9 +219,6 @@
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-print {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
@@ -188,6 +240,7 @@
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -201,6 +254,12 @@
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -222,14 +281,16 @@
|
||||
|
||||
.card-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header mit Aktionen -->
|
||||
<div class="student-card-header">
|
||||
<div>
|
||||
<h1>📚 Bibliotheksausweise (Bibliothek)</h1>
|
||||
<h1 style="margin: 0;">📚 Bibliotheksausweise</h1>
|
||||
</div>
|
||||
<div class="export-buttons">
|
||||
<form method="GET" action="{{ url_for('student_card_class_barcode_download') }}" style="display: inline-flex; gap: 5px; align-items: center; background: white; padding: 2px; border-radius: 4px; border: 1px solid #ddd;">
|
||||
@@ -239,7 +300,7 @@
|
||||
<option value="{{ cls }}">{{ cls }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn-print" style="background: #17a2b8; padding: 8px 12px; margin: 0;">📤 PDF</button>
|
||||
<button type="submit" class="btn-print" style="padding: 8px 12px; margin: 0;">📤 PDF</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ url_for('student_card_barcode_download') }}" class="btn-print" style="background: #28a745;">📥 Alle Ausweise (PDF)</a>
|
||||
@@ -247,87 +308,118 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliotheksausweise</h3>
|
||||
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch, zum Beispiel aus <strong>ASV (Amtliche Schuldaten)</strong>. Erkannt werden automatisch Spalten wie <strong>Name</strong>, <strong>Nachname</strong, <strong>Klasse</strong>, <strong>Ausweis-ID (optional)</strong>, <strong>Notizen (optional)</strong> und <strong>Standard-Ausleihdauer (optional)</strong>.</p>
|
||||
<form method="POST" action="{{ url_for('upload_student_cards_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="student_cards_excel" accept=".xlsx,.csv" required>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Ausweise importieren</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Hauptbereich Grid: Formular + Import -->
|
||||
<div class="dashboard-grid">
|
||||
<!-- Add/Edit Form -->
|
||||
<div class="student-card-form">
|
||||
<h2 style="margin-top:0;">{% if edit_mode %}Ausweis bearbeiten{% else %}Neuer Bibliotheksausweis{% endif %}</h2>
|
||||
|
||||
<!-- Add/Edit Form -->
|
||||
<div class="student-card-form">
|
||||
<h2>{% if edit_mode %}Ausweis bearbeiten{% else %}Neuer Bibliotheksausweis{% endif %}</h2>
|
||||
|
||||
<form method="POST" action="{{ url_for('student_cards_admin') }}">
|
||||
{% if edit_mode %}
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="card_id" value="{{ form_data.get('card_id', '') }}">
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="add">
|
||||
{% endif %}
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="ausweis_id">Ausweis-ID</label>
|
||||
<input type="text" id="ausweis_id" name="ausweis_id"
|
||||
value="{{ form_data.get('ausweis_id', '') }}"
|
||||
placeholder="z.B. SIS2024001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="student_name">Schüler Name *</label>
|
||||
<input type="text" id="student_name" name="student_name" required
|
||||
value="{{ form_data.get('student_name', '') }}"
|
||||
placeholder="z.B. Max Mustermann">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="default_borrow_days">Standard Ausleihdauer (Tage) *</label>
|
||||
<input type="number" id="default_borrow_days" name="default_borrow_days"
|
||||
min="1" max="365" required
|
||||
value="{{ form_data.get('default_borrow_days', config.get('default', 14)) }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="class_name">Klasse</label>
|
||||
<input type="text" id="class_name" name="class_name" list="class_list"
|
||||
value="{{ form_data.get('class_name', '') }}"
|
||||
placeholder="z.B. 10A (Tippen oder Auswählen)">
|
||||
|
||||
<datalist id="class_list">
|
||||
{% for cls in available_classes %}
|
||||
<option value="{{ cls }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="notes">Notizen</label>
|
||||
<textarea id="notes" name="notes" rows="3"
|
||||
placeholder="Optionale Notizen..."
|
||||
style="padding: 10px; border: 1px solid #ddd; border-radius: 4px;">{{ form_data.get('notes', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<form method="POST" action="{{ url_for('student_cards_admin') }}">
|
||||
{% if edit_mode %}
|
||||
<a href="{{ url_for('student_cards_admin') }}" class="btn-cancel">Abbrechen</a>
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="card_id" value="{{ form_data.get('card_id', '') }}">
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="add">
|
||||
{% endif %}
|
||||
<button type="submit" class="btn-save">
|
||||
{% if edit_mode %}Speichern{% else %}Hinzufügen{% endif %}
|
||||
</button>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="student_name">Schüler Name *</label>
|
||||
<input type="text" id="student_name" name="student_name" required
|
||||
value="{{ form_data.get('student_name', '') }}"
|
||||
placeholder="z.B. Max Mustermann">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="class_name">Klasse</label>
|
||||
<input type="text" id="class_name" name="class_name" list="class_list"
|
||||
value="{{ form_data.get('class_name', '') }}"
|
||||
placeholder="z.B. 10A (Tippen oder Auswählen)">
|
||||
|
||||
<datalist id="class_list">
|
||||
{% for cls in available_classes %}
|
||||
<option value="{{ cls }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="ausweis_id">Ausweis-ID</label>
|
||||
<input type="text" id="ausweis_id" name="ausweis_id"
|
||||
value="{{ form_data.get('ausweis_id', '') }}"
|
||||
placeholder="z.B. SIS2024001">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ausweis_ident">Lokales Differenzierungsmerkmal</label>
|
||||
<input type="text" id="ausweis_ident" name="ausweis_ident"
|
||||
value="{{ form_data.get('ausweis_ident', '') }}"
|
||||
placeholder="z.B. xw5oo123bbe">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="default_borrow_days">Standard Ausleihdauer (Tage) *</label>
|
||||
<input type="number" id="default_borrow_days" name="default_borrow_days"
|
||||
min="1" max="365" required
|
||||
value="{{ form_data.get('default_borrow_days', config.get('default', 14)) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row full-width">
|
||||
<div class="form-group">
|
||||
<label for="notes">Notizen</label>
|
||||
<textarea id="notes" name="notes" rows="2"
|
||||
placeholder="Optionale Notizen...">{{ form_data.get('notes', '') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
{% if edit_mode %}
|
||||
<a href="{{ url_for('student_cards_admin') }}" class="btn-cancel">Abbrechen</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn-save">
|
||||
{% if edit_mode %}Speichern{% else %}Hinzufügen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Excel-Import mit Abgleich (Rollover-Modus) -->
|
||||
<div class="import-card">
|
||||
<div>
|
||||
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
||||
<p style="margin:0 0 12px 0; color:#555; font-size:13px; line-height:1.4;">
|
||||
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden Name, Nachname, Klasse, Ausweis-ID, lokales Differenzierungsmerkmal, Notizen & Ausleihdauer.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('upload_student_cards_excel') }}" enctype="multipart/form-data" style="display:flex; flex-direction:column; gap:12px;">
|
||||
<input type="file" name="student_cards_excel" accept=".xlsx,.csv" required style="font-size:13px;">
|
||||
|
||||
<!-- Rollover / Abgleich Option -->
|
||||
<div class="rollover-box">
|
||||
<label class="rollover-label">
|
||||
<input type="checkbox" name="rollover_mode" value="true">
|
||||
<span>Rollover-Modus (Abgleich / Destruktiv)</span>
|
||||
</label>
|
||||
<span class="rollover-hint">
|
||||
⚠️ <strong>Warnung:</strong> Nicht mehr vorhandene Ausweise/Schüler werden beim Import entfernt bzw. abgeglichen.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate" style="flex:1;">Validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import" style="flex:1;">Importieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards List -->
|
||||
<div>
|
||||
<h2>Registrierte Ausweise</h2>
|
||||
<h2 style="margin-bottom: 15px;">Registrierte Ausweise</h2>
|
||||
{% if student_cards %}
|
||||
<table class="cards-table">
|
||||
<thead>
|
||||
@@ -354,7 +446,7 @@
|
||||
<input type="hidden" name="edit" value="{{ card._id }}">
|
||||
<button type="submit" class="btn-edit">Bearbeiten</button>
|
||||
</form>
|
||||
<a href="{{ url_for('student_card_single_barcode_download', card_id=card._id) }}" class="btn-export" style="text-decoration: none; display: inline-block;">📥 PDF</a>
|
||||
<a href="{{ url_for('student_card_single_barcode_download', card_id=card._id) }}" class="btn-export">📥 PDF</a>
|
||||
<form method="POST" style="display: inline;" onsubmit="return confirm('Wirklich löschen?');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="card_id" value="{{ card._id }}">
|
||||
@@ -380,5 +472,4 @@
|
||||
// All PDF exports now go through backend routes
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user