Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b09a6f7720 | |||
| 8c5185bd8c | |||
| 0b30f8463f | |||
| 671a9e8e85 | |||
| 3d9e5c470a | |||
| e636242542 | |||
| d61aeebb8f | |||
| 375e9c46eb | |||
| 8dc773d202 | |||
| 1b2b462c52 | |||
| bd7ef61f5a | |||
| df0f7d3066 |
@@ -1,6 +1,6 @@
|
|||||||
# Inventarsystem
|
# Inventarsystem
|
||||||
|
|
||||||
[](https://github.com/AIIrondev/legendary-octo-garbanzo/actions/workflows/release-docker.yml)
|
[](https://git.invario-software.eu/Invario/Inventarsystem/actions/workflows/release-docker.yml)
|
||||||
|
|
||||||
[](https://wakatime.com/badge/user/30b8509f-5e17-4d16-b6b8-3ca0f3f936d3/project/8a380b7f-389f-4a7e-8877-0fe9e1a4c243)
|
[](https://wakatime.com/badge/user/30b8509f-5e17-4d16-b6b8-3ca0f3f936d3/project/8a380b7f-389f-4a7e-8877-0fe9e1a4c243)
|
||||||
|
|
||||||
|
|||||||
+115
-53
@@ -2150,9 +2150,8 @@ def generate_ausweis_id_excel(existing_ids_set):
|
|||||||
else:
|
else:
|
||||||
print(f"Already found: {new_id}, trying another...")
|
print(f"Already found: {new_id}, trying another...")
|
||||||
|
|
||||||
|
|
||||||
def _upload_student_cards_excel():
|
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:
|
if 'username' not in session:
|
||||||
flash('Nicht angemeldet.', 'error')
|
flash('Nicht angemeldet.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
@@ -2177,6 +2176,8 @@ def _upload_student_cards_excel():
|
|||||||
flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error')
|
flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error')
|
||||||
return redirect(url_for('student_cards_admin'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
|
|
||||||
|
rollover_mode = request.form.get('rollover_mode') in ['true', '1', 'on']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
header_row, data_rows = _load_tabular_upload(excel_file)
|
header_row, data_rows = _load_tabular_upload(excel_file)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -2191,12 +2192,14 @@ def _upload_student_cards_excel():
|
|||||||
|
|
||||||
synonyms = {
|
synonyms = {
|
||||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||||
'ausweis_ident': ['lokales Differenzierungsmerkmal', 'lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident', 'differenzierungsmerkmal'],
|
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
||||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname', 'Vorname'],
|
'differenzierungsmerkmal'],
|
||||||
'last_name': ['nachname', 'last_name', 'lastname', 'Nachname'],
|
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse', 'Jahrgang', 'Klasse'],
|
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise', 'Notizen', 'Bemerkung', 'Hinweis', 'Hinweise'],
|
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||||
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage', 'max_borrow_days', 'Ausleihdauer'],
|
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||||
|
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
||||||
|
'max_borrow_days'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def col_index(key):
|
def col_index(key):
|
||||||
@@ -2227,8 +2230,9 @@ def _upload_student_cards_excel():
|
|||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
try:
|
try:
|
||||||
db = client[cfg.MONGODB_DB]
|
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(
|
existing_ids.update(
|
||||||
str(card.get('AusweisId', '')).strip().upper()
|
str(card.get('AusweisId', '')).strip().upper()
|
||||||
for card in student_cards_cursor
|
for card in student_cards_cursor
|
||||||
@@ -2250,34 +2254,27 @@ def _upload_student_cards_excel():
|
|||||||
|
|
||||||
ausweis_id = sanitize_form_value(val('ausweis_id'))
|
ausweis_id = sanitize_form_value(val('ausweis_id'))
|
||||||
ausweis_ident = sanitize_form_value(val('ausweis_ident'))
|
ausweis_ident = sanitize_form_value(val('ausweis_ident'))
|
||||||
class_name = sanitize_form_value(val('class_name'))
|
class_name = sanitize_form_value(val('class_name')) or ""
|
||||||
notes = sanitize_form_value(val('notes'))
|
notes = sanitize_form_value(val('notes'))
|
||||||
|
|
||||||
# Ausleihdauer extrahieren und standardmäßig auf 14 setzen
|
|
||||||
default_borrow_days = _excel_int(val('default_borrow_days'))
|
|
||||||
if not default_borrow_days:
|
|
||||||
default_borrow_days = 14
|
|
||||||
|
|
||||||
# Vor- und Nachname sicher auslesen und zusammensetzen
|
default_borrow_days = _excel_int(val('default_borrow_days')) or 14
|
||||||
|
|
||||||
first_name = sanitize_form_value(val('first_name')) or ""
|
first_name = sanitize_form_value(val('first_name')) or ""
|
||||||
last_name = sanitize_form_value(val('last_name')) or ""
|
last_name = sanitize_form_value(val('last_name')) or ""
|
||||||
student_name = f"{first_name} {last_name}".strip()
|
student_name = f"{first_name} {last_name}".strip()
|
||||||
|
|
||||||
# Leere Zeilen überspringen
|
|
||||||
if not ausweis_id and not student_name and not class_name:
|
if not ausweis_id and not student_name and not class_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
row_errors = []
|
row_errors = []
|
||||||
|
|
||||||
if not student_name:
|
if not student_name:
|
||||||
row_errors.append('Vorname und Nachname fehlen')
|
row_errors.append('Vorname und Nachname fehlen')
|
||||||
|
|
||||||
# Logik für die Haupt-AusweisID
|
|
||||||
if not ausweis_id and student_name:
|
if not ausweis_id and student_name:
|
||||||
ausweis_id = generate_ausweis_id(existing_ids)
|
ausweis_id = generate_ausweis_id(existing_ids)
|
||||||
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
||||||
existing_ids.add(ausweis_id.upper())
|
existing_ids.add(ausweis_id.upper())
|
||||||
elif ausweis_id:
|
elif ausweis_id and not rollover_mode:
|
||||||
ausweis_id = str(ausweis_id).strip().upper()
|
ausweis_id = str(ausweis_id).strip().upper()
|
||||||
if ausweis_id in existing_ids:
|
if ausweis_id in existing_ids:
|
||||||
row_errors.append(f'Ausweis-ID {ausweis_id} existiert bereits')
|
row_errors.append(f'Ausweis-ID {ausweis_id} existiert bereits')
|
||||||
@@ -2300,29 +2297,92 @@ def _upload_student_cards_excel():
|
|||||||
'notes': notes,
|
'notes': notes,
|
||||||
'default_borrow_days': default_borrow_days,
|
'default_borrow_days': default_borrow_days,
|
||||||
})
|
})
|
||||||
finally:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
if validation_errors:
|
if validation_errors:
|
||||||
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
||||||
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
||||||
return redirect(url_for('student_cards_admin'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
|
|
||||||
if validation_only:
|
matched_db_doc_ids = set()
|
||||||
warning_text = ''
|
rows_to_create = []
|
||||||
if validation_warnings:
|
matched_count = 0
|
||||||
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'))
|
|
||||||
|
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
if rollover_mode:
|
||||||
try:
|
raw_db_cards = list(student_cards_col.find())
|
||||||
db = client[cfg.MONGODB_DB]
|
decrypted_db_cards = []
|
||||||
student_cards = db['student_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
|
created_total = 0
|
||||||
for row in planned_rows:
|
for row in rows_to_create:
|
||||||
encrypted_payload = encrypt_document_fields(
|
encrypted_payload = encrypt_document_fields(
|
||||||
{
|
{
|
||||||
'ausweis_ident': row['ausweis_ident'],
|
'ausweis_ident': row['ausweis_ident'],
|
||||||
@@ -2332,29 +2392,33 @@ def _upload_student_cards_excel():
|
|||||||
},
|
},
|
||||||
STUDENT_CARD_ENCRYPTED_FIELDS
|
STUDENT_CARD_ENCRYPTED_FIELDS
|
||||||
)
|
)
|
||||||
student_cards.insert_one({
|
student_cards_col.insert_one({
|
||||||
'AusweisId': row['ausweis_id'],
|
'AusweisId': row['ausweis_id'],
|
||||||
'StandardAusleihdauer': int(row['default_borrow_days']),
|
'StandardAusleihdauer': int(row['default_borrow_days']),
|
||||||
'Erstellt': datetime.datetime.now(),
|
'Erstellt': datetime.datetime.now(),
|
||||||
**encrypted_payload,
|
**encrypted_payload,
|
||||||
})
|
})
|
||||||
created_total += 1
|
created_total += 1
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
app.logger.error(f'Error importing student cards from Excel: {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'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
if validation_warnings:
|
warning_details = f" Hinweise: {'; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])}" if validation_warnings else ""
|
||||||
warning_details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])
|
if rollover_mode:
|
||||||
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert. Hinweise: {warning_details}', 'warning')
|
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:
|
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'))
|
return redirect(url_for('student_cards_admin'))
|
||||||
|
|
||||||
|
|
||||||
def _upload_excel_items(scope='inventory'):
|
def _upload_excel_items(scope='inventory'):
|
||||||
"""Bulk import inventory/library items from Excel with validation-first workflow."""
|
"""Bulk import inventory/library items from Excel with validation-first workflow."""
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
@@ -3262,9 +3326,6 @@ def library_loans_admin():
|
|||||||
|
|
||||||
_ensure_audit_indexes_once()
|
_ensure_audit_indexes_once()
|
||||||
|
|
||||||
# IMPORT HINZUGEFÜGT: Entschlüsselungs-Tool importieren
|
|
||||||
from modules.inventarsystem.data_protection import decrypt_text
|
|
||||||
|
|
||||||
def fmt_dt(dt):
|
def fmt_dt(dt):
|
||||||
try:
|
try:
|
||||||
return dt.strftime('%d.%m.%Y %H:%M') if dt else ''
|
return dt.strftime('%d.%m.%Y %H:%M') if dt else ''
|
||||||
@@ -3321,6 +3382,7 @@ def library_loans_admin():
|
|||||||
'item_code': item_doc.get('Code_4', ''),
|
'item_code': item_doc.get('Code_4', ''),
|
||||||
'item_author': item_doc.get('Author', ''),
|
'item_author': item_doc.get('Author', ''),
|
||||||
'item_isbn': item_doc.get('ISBN', ''),
|
'item_isbn': item_doc.get('ISBN', ''),
|
||||||
|
'item_cost_raw': item_doc.get('Anschaffungskosten', ''),
|
||||||
'user': decrypted_user,
|
'user': decrypted_user,
|
||||||
'status': record.get('Status', ''),
|
'status': record.get('Status', ''),
|
||||||
'start': fmt_dt(record.get('Start')),
|
'start': fmt_dt(record.get('Start')),
|
||||||
@@ -9065,15 +9127,15 @@ def library_item_invoices(item_id):
|
|||||||
ausleihungen = db['ausleihungen']
|
ausleihungen = db['ausleihungen']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
item_doc = ausleihungen.find_one({'_id': ObjectId(item_id)})
|
||||||
except Exception:
|
except Exception:
|
||||||
item_doc = items_col.find_one({'_id': item_id})
|
item_doc = ausleihungen.find_one({'_id': item_id})
|
||||||
|
|
||||||
if not item_doc:
|
if not item_doc:
|
||||||
flash('Bibliotheksmedium nicht gefunden.', 'error')
|
flash('Bibliotheksmedium nicht gefunden.', 'error')
|
||||||
return redirect(url_for('library_loans_admin'))
|
return redirect(url_for('library_loans_admin'))
|
||||||
|
|
||||||
borrow_docs = list(ausleihungen.find(
|
borrow_docs = list(items_col.find(
|
||||||
{
|
{
|
||||||
'Item': str(item_doc.get('_id')),
|
'Item': str(item_doc.get('_id')),
|
||||||
'InvoiceData': {'$exists': True, '$ne': {}}
|
'InvoiceData': {'$exists': True, '$ne': {}}
|
||||||
|
|||||||
@@ -1261,10 +1261,10 @@
|
|||||||
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_permissions.pages.get('admin_borrowings', False) %}
|
{% if current_permissions.pages.get('admin_borrowings', False) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Alle Ausleihen</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_permissions.pages.get('admin_damaged_items', False) %}
|
{% if current_permissions.pages.get('admin_damaged_items', False) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Alle defekten Items</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
|
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
||||||
@@ -1372,7 +1372,7 @@
|
|||||||
{% if 'username' in session and current_permissions.actions.get('can_manage_settings', False) %}
|
{% if 'username' in session and current_permissions.actions.get('can_manage_settings', False) %}
|
||||||
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
||||||
{% if current_permissions.pages.get('library_loans_admin', False) %}
|
{% if current_permissions.pages.get('library_loans_admin', False) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Alle Ausleihen/Alle Defekten Items</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if student_cards_module_enabled %}
|
{% if student_cards_module_enabled %}
|
||||||
{% if current_permissions.actions.get('can_manage_users', False) %}
|
{% if current_permissions.actions.get('can_manage_users', False) %}
|
||||||
|
|||||||
@@ -407,14 +407,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="damage-invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
|
<div id="damage-invoice-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
|
||||||
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="margin:0;">Rechnung erstellen</h2>
|
<h2 id="modal-title" style="margin:0;">Rechnung erstellen</h2>
|
||||||
<p style="margin:6px 0 0; color:#666;">Die Rechnung nutzt das bestehende Rechnungssystem und kann direkt nach der Schadensmeldung erstellt werden.</p>
|
<p style="margin:6px 0 0; color:#666;">Die Rechnung nutzt das bestehende Rechnungssystem und kann direkt nach der Schadensmeldung erstellt werden.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-secondary" onclick="closeDamageInvoiceModal()">Schließen</button>
|
<button type="button" class="btn btn-secondary" onclick="closeDamageInvoiceModal()" aria-label="Modal schließen">Schließen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="damage-invoice-form" method="post" action="">
|
<form id="damage-invoice-form" method="post" action="">
|
||||||
@@ -432,7 +432,10 @@
|
|||||||
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="damage-invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
|
||||||
|
<label for="damage-invoice-amount" style="font-weight:700; margin:0;">Preis</label>
|
||||||
|
<button type="button" id="damage-invoice-replace-btn" class="btn btn-outline-secondary btn-sm" style="padding: 2px 8px; font-size: 0.75rem;">Komplett ersetzen</button>
|
||||||
|
</div>
|
||||||
<input id="damage-invoice-amount" name="invoice_amount" type="text" required style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;" placeholder="z.B. 12,50">
|
<input id="damage-invoice-amount" name="invoice_amount" type="text" required style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;" placeholder="z.B. 12,50">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,6 +480,7 @@
|
|||||||
const damageInvoiceCode = document.getElementById('damage-invoice-code');
|
const damageInvoiceCode = document.getElementById('damage-invoice-code');
|
||||||
const damageInvoiceAmount = document.getElementById('damage-invoice-amount');
|
const damageInvoiceAmount = document.getElementById('damage-invoice-amount');
|
||||||
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
|
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
|
||||||
|
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
|
||||||
|
|
||||||
function openDamageReportPrompt(button) {
|
function openDamageReportPrompt(button) {
|
||||||
const row = button.closest('.loan-row');
|
const row = button.closest('.loan-row');
|
||||||
@@ -494,41 +498,48 @@
|
|||||||
|
|
||||||
const description = noteInput.trim() || 'Schaden erneut gemeldet';
|
const description = noteInput.trim() || 'Schaden erneut gemeldet';
|
||||||
|
|
||||||
|
// Visuelles Feedback: Button deaktivieren und Text ändern
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Speichere...';
|
||||||
|
|
||||||
fetch(`/report_damage/${itemId}`, {
|
fetch(`/report_damage/${itemId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ description })
|
body: JSON.stringify({ description })
|
||||||
})
|
})
|
||||||
.then(response => response.json().then(data => ({ ok: response.ok, data })))
|
.then(async response => {
|
||||||
.then(({ ok, data }) => {
|
// Robustes JSON-Parsing (verhindert Absturz, falls der Server kein JSON zurückgibt)
|
||||||
if (!ok || !data.success) {
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.');
|
throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.');
|
||||||
}
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) {
|
if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) {
|
||||||
openDamageInvoiceModal(row, description);
|
openDamageInvoiceModal(row, description);
|
||||||
|
// Button wieder zurücksetzen, da die Seite nicht neu geladen wird
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = originalText;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bei "Abbrechen" im Confirm -> Neuladen der Tabelle
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
alert(error.message || 'Fehler beim Speichern der Schadensmeldung.');
|
alert(error.message || 'Ein unbekannter Fehler ist aufgetreten.');
|
||||||
|
// Fehlerbehandlung: Button wieder aktiv schalten
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = originalText;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.openDamageReportPrompt = openDamageReportPrompt;
|
window.openDamageReportPrompt = openDamageReportPrompt;
|
||||||
|
|
||||||
function openDamageInvoiceModal(row, description) {
|
function openDamageInvoiceModal(row, description) {
|
||||||
const modal = document.getElementById('damage-invoice-modal');
|
if (!damageInvoiceModal || !damageInvoiceForm) {
|
||||||
const form = document.getElementById('damage-invoice-form');
|
|
||||||
const inputItem = document.getElementById('damage-invoice-item');
|
|
||||||
const inputBorrower = document.getElementById('damage-invoice-borrower');
|
|
||||||
const inputCode = document.getElementById('damage-invoice-code');
|
|
||||||
const inputAmount = document.getElementById('damage-invoice-amount');
|
|
||||||
const inputReason = document.getElementById('damage-invoice-reason');
|
|
||||||
|
|
||||||
if (!modal || !form) {
|
|
||||||
console.error("Modal oder Formular nicht gefunden.");
|
console.error("Modal oder Formular nicht gefunden.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -537,29 +548,47 @@
|
|||||||
const itemName = row.dataset.itemName || '';
|
const itemName = row.dataset.itemName || '';
|
||||||
const borrower = row.dataset.userName || '';
|
const borrower = row.dataset.userName || '';
|
||||||
const itemCode = row.dataset.itemCode || '';
|
const itemCode = row.dataset.itemCode || '';
|
||||||
|
|
||||||
|
// KORREKTUR: Jetzt greifen wir auf das richtige dataset-Attribut zu
|
||||||
const itemCost = row.dataset.itemCost || '';
|
const itemCost = row.dataset.itemCost || '';
|
||||||
|
|
||||||
form.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
|
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
|
||||||
|
|
||||||
inputItem.value = itemName;
|
damageInvoiceItem.value = itemName;
|
||||||
inputBorrower.value = borrower;
|
damageInvoiceBorrower.value = borrower;
|
||||||
inputCode.value = itemCode;
|
damageInvoiceCode.value = itemCode;
|
||||||
|
|
||||||
inputAmount.value = String(itemCost).replace(' EUR', '').trim();
|
// Feld zunächst leeren, damit der Ersetzen-Button genutzt werden kann
|
||||||
|
damageInvoiceAmount.value = '';
|
||||||
|
|
||||||
inputReason.value = description || `Schaden gemeldet für ${itemName}`;
|
// Original-Preis im Button als data-Attribut hinterlegen
|
||||||
|
if (damageInvoiceReplaceBtn) {
|
||||||
|
damageInvoiceReplaceBtn.dataset.acquisition_costs = String(itemCost).replace(' EUR', '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
modal.style.display = 'block';
|
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
|
||||||
inputAmount.focus();
|
|
||||||
|
damageInvoiceModal.style.display = 'block';
|
||||||
|
|
||||||
|
// Accessibility: Fokus ins erste aktivierbare Feld setzen
|
||||||
|
damageInvoiceAmount.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDamageInvoiceModal() {
|
function closeDamageInvoiceModal() {
|
||||||
const modal = document.getElementById('damage-invoice-modal');
|
if (damageInvoiceModal) {
|
||||||
if (modal) {
|
damageInvoiceModal.style.display = 'none';
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event-Listener für den Ersetzen-Button
|
||||||
|
if (damageInvoiceReplaceBtn) {
|
||||||
|
damageInvoiceReplaceBtn.addEventListener('click', function() {
|
||||||
|
if (this.dataset.acquisition_costs) {
|
||||||
|
damageInvoiceAmount.value = this.dataset.acquisition_costs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.openDamageInvoiceModal = openDamageInvoiceModal;
|
window.openDamageInvoiceModal = openDamageInvoiceModal;
|
||||||
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
|
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
|
||||||
|
|
||||||
@@ -571,8 +600,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
|
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const search = (searchInput.value || '').trim().toLowerCase();
|
const search = (searchInput.value || '').trim().toLowerCase();
|
||||||
const status = statusFilter.value;
|
const status = statusFilter.value;
|
||||||
@@ -613,4 +640,4 @@
|
|||||||
applyFilters();
|
applyFilters();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
|
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
|
||||||
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
||||||
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
|
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
|
||||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
||||||
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
||||||
<input type="checkbox" id="keyboardScannerToggle">
|
<input type="checkbox" id="keyboardScannerToggle">
|
||||||
|
|||||||
@@ -45,6 +45,33 @@
|
|||||||
justify-content: space-between;
|
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 {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -359,16 +386,29 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Excel-Import -->
|
<!-- Excel-Import mit Abgleich (Rollover-Modus) -->
|
||||||
<div class="import-card">
|
<div class="import-card">
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
||||||
<p style="margin:0 0 15px 0; color:#555; font-size:13px; line-height:1.4;">
|
<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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('upload_student_cards_excel') }}" enctype="multipart/form-data" style="display:flex; flex-direction:column; gap:12px;">
|
<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;">
|
<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;">
|
<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-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>
|
<button type="submit" class="btn btn-primary" name="excel_action" value="import" style="flex:1;">Importieren</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user