Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10ae98245c | |||
| 359a8f8ab1 | |||
| 3171455f55 | |||
| a35bb3e048 | |||
| a0409a03dc | |||
| 627b1b76c4 | |||
| cd199f105d | |||
| f77f7024dd | |||
| 5202c5e25e | |||
| 2b1b01f4c1 | |||
| 55c21ac851 | |||
| 8069c6011c | |||
| 4535800a34 | |||
| e3aea734bb | |||
| ad77514574 | |||
| 9870726c76 | |||
| 683059f3de | |||
| ffab9b7fdf | |||
| 2f2c71d0a4 | |||
| 5388562b46 | |||
| df9367a19d | |||
| 0f0f0ebefd | |||
| 65fadf7f5f | |||
| 6f9994c4ce | |||
| 6496716572 | |||
| f750739b51 | |||
| 4c3545f4ba | |||
| c0bf0c0b8e | |||
| 2cd95ef808 | |||
| 17b58ff7ca | |||
| c67f64a45a | |||
| 7727bfd94a | |||
| 55f34e0462 | |||
| 5a3a180b88 | |||
| 07ad592bd9 | |||
| fa299cde06 | |||
| bb2c3f52e8 | |||
| 58ea4e5716 | |||
| e46ee80cf2 | |||
| f0b112b34c | |||
| 01637c31ee | |||
| 52b2628adb | |||
| a51c4c4cbe |
+1045
-241
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,8 @@ import json
|
|||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
import Web.modules.inventarsystem.data_protection as dp
|
import Web.modules.inventarsystem.data_protection as dp
|
||||||
|
import Web.modules.database.user as us
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
def _get_client():
|
def _get_client():
|
||||||
@@ -79,7 +81,7 @@ def get_current_status(ausleihung, log_changes=False, user=None):
|
|||||||
if original_status == 'completed':
|
if original_status == 'completed':
|
||||||
return 'completed'
|
return 'completed'
|
||||||
|
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
start_time = ausleihung.get('Start')
|
start_time = ausleihung.get('Start')
|
||||||
end_time = ausleihung.get('End')
|
end_time = ausleihung.get('End')
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ def create_backup_database():
|
|||||||
os.makedirs(backup_dir)
|
os.makedirs(backup_dir)
|
||||||
|
|
||||||
# Aktuelles Datum für den Dateinamen
|
# Aktuelles Datum für den Dateinamen
|
||||||
current_date = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')
|
||||||
backup_file = os.path.join(backup_dir, f'ausleihungen_backup_{current_date}.json')
|
backup_file = os.path.join(backup_dir, f'ausleihungen_backup_{current_date}.json')
|
||||||
|
|
||||||
# Ausleihungen abrufen und als JSON speichern
|
# Ausleihungen abrufen und als JSON speichern
|
||||||
@@ -175,7 +177,7 @@ def create_backup_database():
|
|||||||
|
|
||||||
log_file = os.path.join(log_dir, 'ausleihungen_error.log')
|
log_file = os.path.join(log_dir, 'ausleihungen_error.log')
|
||||||
with open(log_file, 'a', encoding='utf-8') as f:
|
with open(log_file, 'a', encoding='utf-8') as f:
|
||||||
f.write(f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}: Backup-Fehler: {str(e)}\n")
|
f.write(f"{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')}: Backup-Fehler: {str(e)}\n")
|
||||||
|
|
||||||
print(f"Fehler beim Erstellen des Backups: {e}")
|
print(f"Fehler beim Erstellen des Backups: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -183,7 +185,7 @@ def create_backup_database():
|
|||||||
|
|
||||||
# === AUSLEIHUNG MANAGEMENT ===
|
# === AUSLEIHUNG MANAGEMENT ===
|
||||||
|
|
||||||
def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None):
|
def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None, due_date=None):
|
||||||
"""
|
"""
|
||||||
Add a new borrowing record for an item.
|
Add a new borrowing record for an item.
|
||||||
|
|
||||||
@@ -204,7 +206,7 @@ def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="a
|
|||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
ausleihungen = db['ausleihungen']
|
ausleihungen = db['ausleihungen']
|
||||||
|
|
||||||
ausleihung = {
|
ausleihung = {
|
||||||
'Item': item_id,
|
'Item': item_id,
|
||||||
'User': dp.encrypt_text(user),
|
'User': dp.encrypt_text(user),
|
||||||
@@ -223,6 +225,9 @@ def add_ausleihung(item_id, user, start_date, end_date=None, notes="", status="a
|
|||||||
|
|
||||||
if exemplar_data:
|
if exemplar_data:
|
||||||
ausleihung['ExemplarData'] = exemplar_data
|
ausleihung['ExemplarData'] = exemplar_data
|
||||||
|
|
||||||
|
if due_date:
|
||||||
|
ausleihung['DueDate'] = due_date
|
||||||
|
|
||||||
result = ausleihungen.insert_one(ausleihung)
|
result = ausleihungen.insert_one(ausleihung)
|
||||||
ausleihung_id = result.inserted_id
|
ausleihung_id = result.inserted_id
|
||||||
@@ -270,7 +275,7 @@ def update_ausleihung(id, item_id=None, user_id=None, start=None, end=None, note
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# UTC Zeitstempel nutzen
|
# UTC Zeitstempel nutzen
|
||||||
update_data['LastUpdated'] = datetime.datetime.now(datetime.timezone.utc)
|
update_data['LastUpdated'] = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
result = ausleihungen.update_one(
|
result = ausleihungen.update_one(
|
||||||
{'_id': doc_id},
|
{'_id': doc_id},
|
||||||
@@ -300,7 +305,7 @@ def complete_ausleihung(id, end_time=None):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if end_time is None:
|
if end_time is None:
|
||||||
end_time = datetime.datetime.now()
|
end_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
@@ -312,7 +317,7 @@ def complete_ausleihung(id, end_time=None):
|
|||||||
{'$set': {
|
{'$set': {
|
||||||
'End': end_time,
|
'End': end_time,
|
||||||
'Status': 'completed',
|
'Status': 'completed',
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -320,7 +325,7 @@ def complete_ausleihung(id, end_time=None):
|
|||||||
{'_id': ObjectId(id)},
|
{'_id': ObjectId(id)},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'Verfuegbar': True,
|
'Verfuegbar': True,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -351,7 +356,7 @@ def cancel_ausleihung(id):
|
|||||||
{'_id': ObjectId(id)},
|
{'_id': ObjectId(id)},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'Status': 'cancelled',
|
'Status': 'cancelled',
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -376,7 +381,7 @@ def remove_ausleihung(id):
|
|||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
ausleihungen = db['ausleihungen']
|
ausleihungen = db['ausleihungen']
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
result = ausleihungen.update_one(
|
result = ausleihungen.update_one(
|
||||||
{'_id': ObjectId(id), 'Status': {'$ne': 'deleted'}},
|
{'_id': ObjectId(id), 'Status': {'$ne': 'deleted'}},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
@@ -770,7 +775,7 @@ def activate_ausleihung(id):
|
|||||||
{'_id': doc_id, 'Status': 'planned'},
|
{'_id': doc_id, 'Status': 'planned'},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'Status': 'active',
|
'Status': 'active',
|
||||||
'LastUpdated': datetime.datetime.now(datetime.timezone.utc)
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
return result.modified_count > 0
|
return result.modified_count > 0
|
||||||
@@ -794,7 +799,7 @@ def reset_item_completely(item_id):
|
|||||||
return {'success': False, 'message': 'Item nicht gefunden'}
|
return {'success': False, 'message': 'Item nicht gefunden'}
|
||||||
|
|
||||||
item_name = item.get('Name', 'Unbekannt')
|
item_name = item.get('Name', 'Unbekannt')
|
||||||
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
now_utc = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
# 1. Bulk update active borrowings
|
# 1. Bulk update active borrowings
|
||||||
update_res = ausleihungen_collection.update_many(
|
update_res = ausleihungen_collection.update_many(
|
||||||
@@ -862,7 +867,7 @@ def mark_booking_active(booking_id, ausleihung_id=None):
|
|||||||
doc_id = ObjectId(booking_id) if isinstance(booking_id, str) else booking_id
|
doc_id = ObjectId(booking_id) if isinstance(booking_id, str) else booking_id
|
||||||
update_data = {
|
update_data = {
|
||||||
'Status': 'active',
|
'Status': 'active',
|
||||||
'LastUpdated': datetime.datetime.now(datetime.timezone.utc)
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
if ausleihung_id:
|
if ausleihung_id:
|
||||||
update_data['AusleihungId'] = ausleihung_id
|
update_data['AusleihungId'] = ausleihung_id
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import Web.modules.database.settings as cfg
|
|||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
import Web.modules.inventarsystem.data_protection as dp
|
import Web.modules.inventarsystem.data_protection as dp
|
||||||
import logging
|
import logging
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
def is_library_item(item):
|
def is_library_item(item):
|
||||||
@@ -177,8 +178,8 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
|
|||||||
'SeriesPosition': series_position,
|
'SeriesPosition': series_position,
|
||||||
'IsGroupedSubItem': is_grouped_sub_item,
|
'IsGroupedSubItem': is_grouped_sub_item,
|
||||||
'ParentItemId': parent_item_id,
|
'ParentItemId': parent_item_id,
|
||||||
'Created': datetime.datetime.now(),
|
'Created': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
result = items.insert_one(item)
|
result = items.insert_one(item)
|
||||||
item_id = result.inserted_id
|
item_id = result.inserted_id
|
||||||
@@ -208,8 +209,8 @@ def remove_item(id):
|
|||||||
{'_id': ObjectId(id), 'Deleted': {'$ne': True}},
|
{'_id': ObjectId(id), 'Deleted': {'$ne': True}},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'Deleted': True,
|
'Deleted': True,
|
||||||
'DeletedAt': datetime.datetime.now(),
|
'DeletedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'LastUpdated': datetime.datetime.now(),
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'Verfuegbar': False,
|
'Verfuegbar': False,
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
@@ -304,7 +305,7 @@ def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter
|
|||||||
'is_library': is_lib,
|
'is_library': is_lib,
|
||||||
'library_category': library_category,
|
'library_category': library_category,
|
||||||
'Verfuegbar': bool(verfuegbar),
|
'Verfuegbar': bool(verfuegbar),
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
specific_update = shared_update.copy()
|
specific_update = shared_update.copy()
|
||||||
@@ -346,7 +347,7 @@ def update_item_status(id, verfuegbar, user=None):
|
|||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
'Verfuegbar': verfuegbar,
|
'Verfuegbar': verfuegbar,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
update_query = {'$set': update_data}
|
update_query = {'$set': update_data}
|
||||||
@@ -387,7 +388,7 @@ def update_item_exemplare_status(id, exemplare_status):
|
|||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
'ExemplareStatus': exemplare_status,
|
'ExemplareStatus': exemplare_status,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
result = items.update_one(
|
result = items.update_one(
|
||||||
@@ -734,7 +735,7 @@ def unstuck_item(id):
|
|||||||
{'$set': {
|
{'$set': {
|
||||||
'Status': 'cancelled',
|
'Status': 'cancelled',
|
||||||
'CancelledReason': 'unstuck_reset',
|
'CancelledReason': 'unstuck_reset',
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -745,7 +746,7 @@ def unstuck_item(id):
|
|||||||
{
|
{
|
||||||
'$set': {
|
'$set': {
|
||||||
'Verfuegbar': True,
|
'Verfuegbar': True,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
},
|
},
|
||||||
'$unset': {'User': ""}
|
'$unset': {'User': ""}
|
||||||
}
|
}
|
||||||
@@ -1075,7 +1076,7 @@ def update_item_next_appointment(item_id, appointment_data):
|
|||||||
if appointment_data is None:
|
if appointment_data is None:
|
||||||
update_query = {
|
update_query = {
|
||||||
'$unset': {'NextAppointment': ""},
|
'$unset': {'NextAppointment': ""},
|
||||||
'$set': {'LastUpdated': datetime.datetime.now()}
|
'$set': {'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Create a copy so we don't mutate the original dictionary passed in
|
# Create a copy so we don't mutate the original dictionary passed in
|
||||||
@@ -1088,7 +1089,7 @@ def update_item_next_appointment(item_id, appointment_data):
|
|||||||
update_query = {
|
update_query = {
|
||||||
'$set': {
|
'$set': {
|
||||||
'NextAppointment': data_to_save,
|
'NextAppointment': data_to_save,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1121,7 +1122,7 @@ def clear_item_next_appointment(item_id):
|
|||||||
|
|
||||||
result = items.update_one(
|
result = items.update_one(
|
||||||
{'_id': ObjectId(item_id)},
|
{'_id': ObjectId(item_id)},
|
||||||
{'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now()}}
|
{'$unset': {'NextAppointment': ""}, '$set': {'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}}
|
||||||
)
|
)
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from Web.modules.database.settings import MongoClient
|
|||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
import datetime
|
import datetime
|
||||||
import ast
|
import ast
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
def _get_tenant_db(client):
|
def _get_tenant_db(client):
|
||||||
@@ -99,8 +100,8 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
|||||||
'calendar_enabled': bool(calendar_enabled),
|
'calendar_enabled': bool(calendar_enabled),
|
||||||
'clients_per_slot': clients_p_slot,
|
'clients_per_slot': clients_p_slot,
|
||||||
'slots_booked': [], # -> [(start_time, (names),(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
|
'slots_booked': [], # -> [(start_time, (names),(custom1, custom2,...)), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
|
||||||
'Created': datetime.datetime.now(),
|
'Created': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
result = items.insert_one(item)
|
result = items.insert_one(item)
|
||||||
return result.inserted_id
|
return result.inserted_id
|
||||||
@@ -138,7 +139,7 @@ def update(id, slots_used: list):
|
|||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
'slots_booked': dp.encrypt_text(str(slots_used)),
|
'slots_booked': dp.encrypt_text(str(slots_used)),
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
result = items.update_one(
|
result = items.update_one(
|
||||||
@@ -200,7 +201,7 @@ def remove_slot(id, date_start_time, name):
|
|||||||
{
|
{
|
||||||
'$set': {
|
'$set': {
|
||||||
'slots_booked': dp.encrypt_text(str(updated_slots)),
|
'slots_booked': dp.encrypt_text(str(updated_slots)),
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -740,6 +740,87 @@ def get_user_by_student_ident(student_ident):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_by_student_name(student_name):
|
||||||
|
"""Return user dict by student name by decrypting all cards and matching."""
|
||||||
|
if not student_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Normalisiere den Suchbegriff, den wir finden wollen
|
||||||
|
normalized_target = str(student_name).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_student_name = user_doc.get('SchülerName')
|
||||||
|
|
||||||
|
if encrypted_student_name:
|
||||||
|
try:
|
||||||
|
decrypted_ident = dp.decrypt_text(encrypted_student_name)
|
||||||
|
|
||||||
|
if decrypted_ident and str(decrypted_ident).strip() == normalized_target:
|
||||||
|
return user_doc
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Entschlüsselungsfehler bei ID {user_doc.get('_id')}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Datenbankfehler in get_user_by_student_name: {exc}")
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def student_is_blocked(student_id):
|
||||||
|
"""
|
||||||
|
Überprüft, ob ein Schüler (Nutzer) im System gesperrt ist (z. B. aufgrund von überfälligen Ausleihen).
|
||||||
|
Akzeptiert entweder den Benutzernamen oder die Schülerausweis-ID.
|
||||||
|
|
||||||
|
Rückgabewert:
|
||||||
|
True, wenn der Nutzer gesperrt ist.
|
||||||
|
False, wenn der Nutzer nicht gesperrt ist oder nicht gefunden wurde.
|
||||||
|
"""
|
||||||
|
if not student_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
normalized_card_id = normalize_student_card_id(student_id)
|
||||||
|
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
try:
|
||||||
|
db = _get_tenant_db(client)
|
||||||
|
users = db['users']
|
||||||
|
|
||||||
|
# Suche nach dem Benutzer anhand des Benutzernamens (groß/klein) oder der StudentCardId
|
||||||
|
query = {
|
||||||
|
'$or': [
|
||||||
|
{'Username': student_id},
|
||||||
|
{'username': student_id},
|
||||||
|
{'StudentCardId': normalized_card_id}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_doc = users.find_one(query)
|
||||||
|
|
||||||
|
if user_doc:
|
||||||
|
# Lese das Feld 'is_blocked' aus, standardmäßig False, falls es nicht existiert
|
||||||
|
return bool(user_doc.get('is_blocked', False))
|
||||||
|
|
||||||
|
# Wenn kein Nutzer gefunden wurde, gehen wir sicherheitshalber davon aus, dass keine Sperre vorliegt
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Datenbankfehler in student_is_blocked: {e}")
|
||||||
|
# Im Fehlerfall False zurückgeben, um Systemblockaden durch Datenbankfehler zu vermeiden
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
def make_admin(username):
|
def make_admin(username):
|
||||||
"""Grant administrator privileges to a user."""
|
"""Grant administrator privileges to a user."""
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def _build_smtp_client():
|
|||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
smtp.ehlo()
|
smtp.ehlo()
|
||||||
if cfg.EMAIL_USERNAME:
|
if cfg.EMAIL_USERNAME:
|
||||||
smtp.login(cfg.EMAIL_USERNAME, cfg.EMAIL_PASSWORD or "")
|
smtp.login("no-reply@invario-software.de", "#,EATwIn,68" or "")
|
||||||
return smtp
|
return smtp
|
||||||
|
|
||||||
|
|
||||||
@@ -68,14 +68,20 @@ def send(email: list | str, subject: str, note: str, sender: str) -> bool:
|
|||||||
|
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["Subject"] = str(subject)
|
msg["Subject"] = str(subject)
|
||||||
msg["From"] = f"{sender} <{cfg.EMAIL_USERNAME}>"
|
msg["From"] = f"{sender} <no-reply@invario-software.de>"
|
||||||
msg["To"] = str(recipient)
|
msg["To"] = str(recipient)
|
||||||
|
|
||||||
msg.attach(MIMEText(text_content, "plain"))
|
msg.attach(MIMEText(text_content, "plain"))
|
||||||
msg.attach(MIMEText(html_content, "html"))
|
msg.attach(MIMEText(html_content, "html"))
|
||||||
|
|
||||||
|
#smtp.sendmail(
|
||||||
|
# from_addr=cfg.EMAIL_USERNAME,
|
||||||
|
# to_addrs=[recipient],
|
||||||
|
# msg=msg.as_string()
|
||||||
|
#)
|
||||||
|
|
||||||
smtp.sendmail(
|
smtp.sendmail(
|
||||||
from_addr=cfg.EMAIL_USERNAME,
|
from_addr="no-reply@invario-software.de",
|
||||||
to_addrs=[recipient],
|
to_addrs=[recipient],
|
||||||
msg=msg.as_string()
|
msg=msg.as_string()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from bson import ObjectId
|
|||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
@@ -153,7 +154,7 @@ def save_push_subscription(username, subscription_obj):
|
|||||||
subs_col.update_one(
|
subs_col.update_one(
|
||||||
{'_id': existing['_id']},
|
{'_id': existing['_id']},
|
||||||
{'$set': {
|
{'$set': {
|
||||||
'LastUsed': datetime.datetime.now(),
|
'LastUsed': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'IsActive': True
|
'IsActive': True
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
@@ -172,8 +173,8 @@ def save_push_subscription(username, subscription_obj):
|
|||||||
'Keys': encrypt_text(keys_str),
|
'Keys': encrypt_text(keys_str),
|
||||||
'SubscriptionHash': sub_hash,
|
'SubscriptionHash': sub_hash,
|
||||||
'IsActive': True,
|
'IsActive': True,
|
||||||
'CreatedAt': datetime.datetime.now(),
|
'CreatedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'LastUsed': datetime.datetime.now(),
|
'LastUsed': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
'UserAgent': subscription_obj.get('userAgent', ''),
|
'UserAgent': subscription_obj.get('userAgent', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +400,7 @@ def cleanup_inactive_subscriptions():
|
|||||||
db = client[cfg.MONGODB_DB]
|
db = client[cfg.MONGODB_DB]
|
||||||
subs_col = get_push_subscriptions_collection(db)
|
subs_col = get_push_subscriptions_collection(db)
|
||||||
|
|
||||||
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=30)
|
cutoff_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")) - datetime.timedelta(days=30)
|
||||||
|
|
||||||
result = subs_col.delete_many({
|
result = subs_col.delete_many({
|
||||||
'IsActive': False,
|
'IsActive': False,
|
||||||
|
|||||||
@@ -1373,6 +1373,7 @@
|
|||||||
<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') }}">Alle Ausleihen/Alle Defekten Items</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Alle Ausleihen/Alle Defekten Items</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('mahnungen_admin') }}">Mahnungen</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) %}
|
||||||
|
|||||||
@@ -72,9 +72,10 @@
|
|||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Angepasst für 4 Filter-Felder statt 3 */
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 220px 220px;
|
grid-template-columns: 1fr 140px 180px 180px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
background: var(--ui-surface-soft);
|
background: var(--ui-surface-soft);
|
||||||
|
user-select: none; /* Verhindert Textmarkierung beim Klicken */
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-table tr:hover td {
|
.library-table tr:hover td {
|
||||||
@@ -149,6 +151,7 @@
|
|||||||
.badge-open { background: #fee2e2; color: #991b1b; }
|
.badge-open { background: #fee2e2; color: #991b1b; }
|
||||||
.badge-paid { background: #dcfce7; color: #166534; }
|
.badge-paid { background: #dcfce7; color: #166534; }
|
||||||
.badge-damaged { background: #fee2e2; color: #991b1b; }
|
.badge-damaged { background: #fee2e2; color: #991b1b; }
|
||||||
|
.badge-class { background: #f1f5f9; color: #475569; border: 1px solid #cbd5e1; } /* Neues Badge für Klasse */
|
||||||
|
|
||||||
.row-actions {
|
.row-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -222,15 +225,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<input id="library-search" type="text" placeholder="Nach Element, Benutzer, Ausweis oder Rechnung suchen...">
|
<input id="library-search" type="text" placeholder="Nach Element, Benutzer, Klasse, Ausweis oder Rechnung suchen...">
|
||||||
|
|
||||||
|
<!-- NEU: Klassen-Filter -->
|
||||||
|
<select id="class-filter">
|
||||||
|
<option value="all">Alle Klassen</option>
|
||||||
|
{% set classes = [] %}
|
||||||
|
{% for e in loan_entries %}
|
||||||
|
{% if e.klasse and e.klasse not in classes %}
|
||||||
|
{% set _ = classes.append(e.klasse) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for c in classes|sort %}
|
||||||
|
<option value="{{ c|lower }}">{{ c }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="loan-status-filter">
|
<select id="loan-status-filter">
|
||||||
<option value="">Alle Ausleihen</option>
|
<option value="">Alle Status</option>
|
||||||
<option value="active">Aktiv</option>
|
<option value="active">Aktiv</option>
|
||||||
<option value="planned">Geplant</option>
|
<option value="planned">Geplant</option>
|
||||||
<option value="completed">Abgeschlossen</option>
|
<option value="completed">Abgeschlossen</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="damage-filter">
|
<select id="damage-filter">
|
||||||
<option value="all">Alle Einträge</option>
|
<option value="all">Alle Zustände</option>
|
||||||
<option value="damage">Nur defekt</option>
|
<option value="damage">Nur defekt</option>
|
||||||
<option value="clean">Nur ohne Schaden</option>
|
<option value="clean">Nur ohne Schaden</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -242,18 +260,31 @@
|
|||||||
<table class="library-table" id="loans-table">
|
<table class="library-table" id="loans-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<!-- NEU: onClick Handler zum Sortieren hinzugefügt -->
|
||||||
<th>Element</th>
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 0)">Status ↕</th>
|
||||||
<th>Benutzer</th>
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 1)">Element ↕</th>
|
||||||
<th>Zeit</th>
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 2)">Benutzer ↕</th>
|
||||||
<th>Rechnung</th>
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 3)">Klasse ↕</th>
|
||||||
<th>Schaden</th>
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 4)">Zeit ↕</th>
|
||||||
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 5)">Rechnung ↕</th>
|
||||||
|
<th style="cursor: pointer;" onclick="sortTable('loans-table', 6)">Schaden ↕</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<!-- NEU: data-klasse hinzugefügt -->
|
||||||
{% for e in loan_entries %}
|
{% for e in loan_entries %}
|
||||||
<tr class="loan-row" data-borrow-id="{{ e.id }}" data-item-id="{{ e.item_id }}" data-item-name="{{ e.item_name }}" data-item-code="{{ e.item_code }}" data-item-cost="{{ e.item_cost_raw }}" data-user-name="{{ e.user }}" data-search="{{ (e.item_name ~ ' ' ~ e.item_code ~ ' ' ~ e.user ~ ' ' ~ e.invoice_number ~ ' ' ~ e.item_author ~ ' ' ~ e.item_isbn)|lower }}" data-status="{{ e.status }}" data-has-damage="{{ '1' if e.has_damage else '0' }}">
|
<tr class="loan-row"
|
||||||
|
data-borrow-id="{{ e.id }}"
|
||||||
|
data-item-id="{{ e.item_id }}"
|
||||||
|
data-item-name="{{ e.item_name }}"
|
||||||
|
data-item-code="{{ e.item_code }}"
|
||||||
|
data-item-cost="{{ e.item_cost_raw }}"
|
||||||
|
data-user-name="{{ e.user }}"
|
||||||
|
data-klasse="{{ (e.klasse|default(''))|lower }}"
|
||||||
|
data-search="{{ (e.item_name ~ ' ' ~ e.item_code ~ ' ' ~ e.user ~ ' ' ~ (e.klasse|default('')) ~ ' ' ~ e.invoice_number ~ ' ' ~ e.item_author ~ ' ' ~ e.item_isbn)|lower }}"
|
||||||
|
data-status="{{ e.status }}"
|
||||||
|
data-has-damage="{{ '1' if e.has_damage else '0' }}">
|
||||||
<td>
|
<td>
|
||||||
{% if e.status == 'active' %}
|
{% if e.status == 'active' %}
|
||||||
<span class="badge-pill badge-active">Aktiv</span>
|
<span class="badge-pill badge-active">Aktiv</span>
|
||||||
@@ -268,24 +299,32 @@
|
|||||||
<div class="muted">{{ e.item_author or '—' }}</div>
|
<div class="muted">{{ e.item_author or '—' }}</div>
|
||||||
<div class="mono">{{ e.item_code or '—' }}</div>
|
<div class="mono">{{ e.item_code or '—' }}</div>
|
||||||
<div style="margin-top:6px;">
|
<div style="margin-top:6px;">
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen</a>
|
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen / Historie</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ e.user }}</div>
|
<div><strong>{{ e.user }}</strong></div>
|
||||||
<div class="muted">{{ e.start }}{% if e.end %} bis {{ e.end }}{% endif %}</div>
|
</td>
|
||||||
|
<!-- NEU: Klasse Spalte -->
|
||||||
|
<td>
|
||||||
|
{% if e.klasse %}
|
||||||
|
<span class="badge-pill badge-class">{{ e.klasse }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="muted">—</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ e.period or '—' }}</div>
|
<div>{{ e.period or '—' }}</div>
|
||||||
|
<div class="muted">{{ e.start }}{% if e.end %} bis {{ e.end }}{% endif %}</div>
|
||||||
{% if e.notes %}<div class="muted">{{ e.notes }}</div>{% endif %}
|
{% if e.notes %}<div class="muted">{{ e.notes }}</div>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if e.invoice_number %}
|
{% if e.invoice_number %}
|
||||||
<div class="mono">{{ e.invoice_number }}</div>
|
<div class="mono">{{ e.invoice_number }}</div>
|
||||||
<div class="muted">{{ e.invoice_amount }}</div>
|
<div class="muted">{{ e.invoice_amount }}</div>
|
||||||
{% if e.invoice_corrections_count %}
|
<!--{% if e.invoice_corrections_count %}
|
||||||
<div class="muted" style="color:#7c2d12;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
|
<div class="muted" style="color:#7c2d12;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
|
||||||
{% endif %}
|
{% endif %} -->
|
||||||
<div style="margin-top:6px;">
|
<div style="margin-top:6px;">
|
||||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_view_invoice_pdf', borrow_id=e.id) }}" target="_blank" rel="noopener">PDF öffnen</a>
|
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_view_invoice_pdf', borrow_id=e.id) }}" target="_blank" rel="noopener">PDF öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,13 +363,13 @@
|
|||||||
<button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ e.item_id }}', '{{ e.item_code }}')">Reparieren / Ersetzen</button>
|
<button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ e.item_id }}', '{{ e.item_code }}')">Reparieren / Ersetzen</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if e.invoice_number %}
|
<!--{% if e.invoice_number %}
|
||||||
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');" style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
|
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');" style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
|
||||||
<input type="text" name="correction_reason" value="Korrektur zu {{ e.invoice_number }}" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:160px; max-width:180px;">
|
<input type="text" name="correction_reason" value="Korrektur zu {{ e.invoice_number }}" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:160px; max-width:180px;">
|
||||||
<input type="text" name="amount_delta" placeholder="z.B. -{{ e.invoice_amount }}" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
|
<input type="text" name="amount_delta" placeholder="z.B. -{{ e.invoice_amount }}" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
|
||||||
<button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
|
<button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}-->
|
||||||
|
|
||||||
{% if e.status in ['active', 'planned'] %}
|
{% if e.status in ['active', 'planned'] %}
|
||||||
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
|
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
|
||||||
@@ -355,10 +394,11 @@
|
|||||||
<table class="library-table" id="damaged-table">
|
<table class="library-table" id="damaged-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Element</th>
|
<!-- NEU: onClick Handler zum Sortieren hinzugefügt -->
|
||||||
<th>Code</th>
|
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 0)">Element ↕</th>
|
||||||
<th>Schaden</th>
|
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 1)">Code ↕</th>
|
||||||
<th>Status</th>
|
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 2)">Schaden ↕</th>
|
||||||
|
<th style="cursor: pointer;" onclick="sortTable('damaged-table', 3)">Status ↕</th>
|
||||||
<th>Aktion</th>
|
<th>Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -489,6 +529,7 @@
|
|||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const searchInput = document.getElementById('library-search');
|
const searchInput = document.getElementById('library-search');
|
||||||
|
const classFilter = document.getElementById('class-filter'); // NEU: Klassenfilter hinzugefügt
|
||||||
const statusFilter = document.getElementById('loan-status-filter');
|
const statusFilter = document.getElementById('loan-status-filter');
|
||||||
const damageFilter = document.getElementById('damage-filter');
|
const damageFilter = document.getElementById('damage-filter');
|
||||||
const loanRows = Array.from(document.querySelectorAll('.loan-row'));
|
const loanRows = Array.from(document.querySelectorAll('.loan-row'));
|
||||||
@@ -505,6 +546,51 @@
|
|||||||
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
|
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
|
||||||
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
|
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
|
||||||
|
|
||||||
|
// NEU: Globale Sortier-Richtungsobjekte
|
||||||
|
let sortDirections = {};
|
||||||
|
|
||||||
|
// NEU: Sortier-Funktion
|
||||||
|
window.sortTable = function(tableId, columnIndex) {
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
const tbody = table.tBodies[0];
|
||||||
|
const rows = Array.from(tbody.querySelectorAll("tr"));
|
||||||
|
|
||||||
|
if (rows.length === 0) return;
|
||||||
|
|
||||||
|
const sortKey = tableId + "-" + columnIndex;
|
||||||
|
if (!(sortKey in sortDirections)) {
|
||||||
|
sortDirections[sortKey] = true;
|
||||||
|
} else {
|
||||||
|
sortDirections[sortKey] = !sortDirections[sortKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAscending = sortDirections[sortKey];
|
||||||
|
const multiplier = isAscending ? 1 : -1;
|
||||||
|
|
||||||
|
// Pfeil im Header aktualisieren
|
||||||
|
const headers = table.querySelectorAll("th");
|
||||||
|
headers.forEach(th => {
|
||||||
|
if(th.innerHTML.includes('↕') || th.innerHTML.includes('▲') || th.innerHTML.includes('▼')) {
|
||||||
|
th.innerHTML = th.innerHTML.replace(/[↕▲▼]/g, '↕');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const currentTh = headers[columnIndex];
|
||||||
|
if (currentTh) {
|
||||||
|
currentTh.innerHTML = currentTh.innerHTML.replace('↕', isAscending ? '▲' : '▼');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeilen sortieren
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const cellA = a.cells[columnIndex].textContent.trim();
|
||||||
|
const cellB = b.cells[columnIndex].textContent.trim();
|
||||||
|
return cellA.localeCompare(cellB, 'de', { numeric: true, sensitivity: 'base' }) * multiplier;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neu einfügen
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Funktionen für das Reparatur-Modal
|
// Funktionen für das Reparatur-Modal
|
||||||
window.openRepairModal = function(itemId, currentCode) {
|
window.openRepairModal = function(itemId, currentCode) {
|
||||||
const modal = document.getElementById('repair-action-modal');
|
const modal = document.getElementById('repair-action-modal');
|
||||||
@@ -512,7 +598,6 @@
|
|||||||
const codeContainer = document.getElementById('new-code-container');
|
const codeContainer = document.getElementById('new-code-container');
|
||||||
const replaceBtn = document.getElementById('submit-replace-btn');
|
const replaceBtn = document.getElementById('submit-replace-btn');
|
||||||
|
|
||||||
// Setze die Route im Formular (passe hier den Endpunkt an deine Backend-Route an, z.B. /admin/items/ID/resolve_repair)
|
|
||||||
form.action = `/admin/items/${itemId}/resolve_repair`;
|
form.action = `/admin/items/${itemId}/resolve_repair`;
|
||||||
|
|
||||||
document.getElementById('repair-action-input').value = 'repair';
|
document.getElementById('repair-action-input').value = 'repair';
|
||||||
@@ -639,20 +724,26 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEU: Kombinierte Filter-Funktion (Suche + Status + Schaden + Klasse)
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const search = (searchInput.value || '').trim().toLowerCase();
|
const search = (searchInput.value || '').trim().toLowerCase();
|
||||||
const status = statusFilter.value;
|
const status = statusFilter.value;
|
||||||
const damage = damageFilter.value;
|
const damage = damageFilter.value;
|
||||||
|
const klasse = classFilter.value; // NEU
|
||||||
|
|
||||||
let visibleLoans = 0;
|
let visibleLoans = 0;
|
||||||
loanRows.forEach(row => {
|
loanRows.forEach(row => {
|
||||||
const haystack = row.dataset.search || '';
|
const haystack = row.dataset.search || '';
|
||||||
const rowStatus = row.dataset.status || '';
|
const rowStatus = row.dataset.status || '';
|
||||||
|
const rowKlasse = row.dataset.klasse || ''; // NEU
|
||||||
const hasDamage = row.dataset.hasDamage === '1';
|
const hasDamage = row.dataset.hasDamage === '1';
|
||||||
|
|
||||||
const searchMatch = !search || haystack.includes(search);
|
const searchMatch = !search || haystack.includes(search);
|
||||||
const statusMatch = !status || rowStatus === status;
|
const statusMatch = !status || rowStatus === status;
|
||||||
|
const classMatch = klasse === 'all' || rowKlasse === klasse; // NEU
|
||||||
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
|
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
|
||||||
const show = searchMatch && statusMatch && damageMatch;
|
|
||||||
|
const show = searchMatch && statusMatch && classMatch && damageMatch; // NEU
|
||||||
row.style.display = show ? '' : 'none';
|
row.style.display = show ? '' : 'none';
|
||||||
if (show) visibleLoans++;
|
if (show) visibleLoans++;
|
||||||
});
|
});
|
||||||
@@ -663,10 +754,17 @@
|
|||||||
const haystack = row.dataset.search || '';
|
const haystack = row.dataset.search || '';
|
||||||
const rowStatus = row.dataset.status || '';
|
const rowStatus = row.dataset.status || '';
|
||||||
const hasDamage = row.dataset.hasDamage === '1';
|
const hasDamage = row.dataset.hasDamage === '1';
|
||||||
|
|
||||||
const searchMatch = !search || haystack.includes(search);
|
const searchMatch = !search || haystack.includes(search);
|
||||||
const statusMatch = !status || rowStatus === status || status === '';
|
const statusMatch = !status || rowStatus === status || status === '';
|
||||||
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
|
const damageMatch = damage === 'all' || (damage === 'damage' && hasDamage) || (damage === 'clean' && !hasDamage);
|
||||||
const show = searchMatch && statusMatch && damageMatch;
|
|
||||||
|
// Defekte-Medien-Tabelle hat keine verknüpfte "Klasse", deshalb blenden wir sie nur bei Klassen-Filter "all" ein,
|
||||||
|
// oder wenn gar nicht nach Klasse gefiltert wird, damit sie nicht verschwindet.
|
||||||
|
// Falls sie bei aktiver Klassensuche komplett verschwinden soll, passe die Bedingung an:
|
||||||
|
const classMatch = klasse === 'all';
|
||||||
|
|
||||||
|
const show = searchMatch && statusMatch && damageMatch && classMatch;
|
||||||
row.style.display = show ? '' : 'none';
|
row.style.display = show ? '' : 'none';
|
||||||
if (show) visibleDamaged++;
|
if (show) visibleDamaged++;
|
||||||
});
|
});
|
||||||
@@ -674,6 +772,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchInput.addEventListener('input', applyFilters);
|
searchInput.addEventListener('input', applyFilters);
|
||||||
|
classFilter.addEventListener('change', applyFilters); // NEU
|
||||||
statusFilter.addEventListener('change', applyFilters);
|
statusFilter.addEventListener('change', applyFilters);
|
||||||
damageFilter.addEventListener('change', applyFilters);
|
damageFilter.addEventListener('change', applyFilters);
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Rechnungen Element - {{ APP_VERSION }}{% endblock %}
|
{% block title %}Rechnungs- & Schadenshistorie - {{ APP_VERSION }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<style>
|
<style>
|
||||||
@@ -39,6 +39,14 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 36px 0 16px 0;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-card {
|
.invoice-card {
|
||||||
background: var(--ui-surface);
|
background: var(--ui-surface);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
@@ -115,7 +123,7 @@
|
|||||||
|
|
||||||
<div class="invoice-history-shell">
|
<div class="invoice-history-shell">
|
||||||
<div class="invoice-history-head">
|
<div class="invoice-history-head">
|
||||||
<h1>Rechnungshistorie pro Element</h1>
|
<h1>Rechnungs- & Schadenshistorie pro Element</h1>
|
||||||
<div class="head-meta">
|
<div class="head-meta">
|
||||||
<span><strong>Element:</strong> {{ item.name or '—' }}</span>
|
<span><strong>Element:</strong> {{ item.name or '—' }}</span>
|
||||||
<span><strong>Code:</strong> {{ item.code or '—' }}</span>
|
<span><strong>Code:</strong> {{ item.code or '—' }}</span>
|
||||||
@@ -124,10 +132,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="head-actions">
|
<div class="head-actions">
|
||||||
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a>
|
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a>
|
||||||
<a class="btn btn-secondary" href="{{ url_for('library_admin') }}">Bibliothek öffnen</a>
|
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- BEREICH: RECHNUNGEN -->
|
||||||
|
<h2 class="section-header">Ausgestellte Rechnungen</h2>
|
||||||
<div class="invoice-card">
|
<div class="invoice-card">
|
||||||
{% if invoices %}
|
{% if invoices %}
|
||||||
<table class="invoice-table">
|
<table class="invoice-table">
|
||||||
@@ -137,7 +147,7 @@
|
|||||||
<th>Betrag</th>
|
<th>Betrag</th>
|
||||||
<th>Ausleihe</th>
|
<th>Ausleihe</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Schaden</th>
|
<th>Schaden / Grund</th>
|
||||||
<th>Aktion</th>
|
<th>Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -184,5 +194,50 @@
|
|||||||
<div class="empty-state">Für dieses Element wurden noch keine Rechnungen gespeichert.</div>
|
<div class="empty-state">Für dieses Element wurden noch keine Rechnungen gespeichert.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- BEREICH: SCHÄDEN -->
|
||||||
|
<h2 class="section-header">Gemeldete Schäden</h2>
|
||||||
|
<div class="invoice-card">
|
||||||
|
{% if damages %}
|
||||||
|
<table class="invoice-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Quelle</th>
|
||||||
|
<th>Nutzer / Verursacher</th>
|
||||||
|
<th>Beschreibung</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in damages %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="muted">{{ row.date }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="mono">{{ row.source }}</div>
|
||||||
|
{% if row.borrow_id %}
|
||||||
|
<div class="muted" style="font-size: 0.8rem;">ID: <span title="{{ row.borrow_id }}">{{ row.borrow_id[:6] }}...</span></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ row.user }}</td>
|
||||||
|
<td>{{ row.description }}</td>
|
||||||
|
<td>
|
||||||
|
<!-- Einfache Logik, um bei reparierten Objekten einen grünen Badge zu zeigen -->
|
||||||
|
{% if row.status|lower in ['repariert', 'erledigt', 'geschlossen', 'bezahlt'] %}
|
||||||
|
<span class="badge-pill badge-completed">{{ row.status }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge-pill badge-open">{{ row.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">Für dieses Element wurden bislang keine Schäden erfasst.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Mahnungsübersicht{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.mahnungen-shell {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-head {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%);
|
||||||
|
border: 1px solid #dbe4ee;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.07);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-head h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.55rem;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-wrapper {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-card {
|
||||||
|
background: var(--ui-surface, #ffffff);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-table th,
|
||||||
|
.mahnungen-table td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid #edf2f7;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-table th {
|
||||||
|
font-size: 0.83rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #64748b;
|
||||||
|
background: var(--ui-surface-soft, #f8fafc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mahnungen-table tr:hover td {
|
||||||
|
background: #fbfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-open { background: #fee2e2; color: #991b1b; }
|
||||||
|
.badge-warning { background: #fef3c7; color: #b45309; }
|
||||||
|
.badge-info { background: #e0f2fe; color: #0369a1; }
|
||||||
|
.badge-paid { background: #dcfce7; color: #166534; }
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 36px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.mahnungen-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-exclamation-triangle-fill me-2"></i>Mahnungs- und Ausleihverwaltung</h5>
|
||||||
|
<span class="badge bg-light text-dark">Übersicht überfälliger Medien</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if overdue_items and overdue_items|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Schüler / Ausweis</th>
|
||||||
|
<th>Klasse</th>
|
||||||
|
<th>Medium</th>
|
||||||
|
<th>Fälligkeitsdatum</th>
|
||||||
|
<th>Überfällig seit</th>
|
||||||
|
<th>Mahnstufe</th>
|
||||||
|
<th class="text-end">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in overdue_items %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ item.schueler_name }}</strong><br>
|
||||||
|
<small class="text-muted">{{ item.ausweis_id }}</small>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.klasse }}</td>
|
||||||
|
<td>
|
||||||
|
{{ item.item_name }}<br>
|
||||||
|
<small class="text-muted">Code: {{ item.item_code or '—' }}</small>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.due_date }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-danger">{{ item.days_overdue }} Tage</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if item.mahnstufe == 2 %}
|
||||||
|
<span class="badge bg-dark text-danger">Stufe 2 (Gesperrt)</span>
|
||||||
|
{% elif item.mahnstufe == 1 %}
|
||||||
|
<span class="badge bg-warning text-dark">Stufe 1</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Stufe 0</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||||
|
onclick="openEmailModal('{{ item.id }}', '{{ item.schueler_name }}', '{{ item.email }}')">
|
||||||
|
<i class="bi bi-envelope"></i> E-Mail
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-warning text-dark"
|
||||||
|
onclick="resetMahnung('{{ item.id }}', '{{ item.schueler_name }}')">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> Zurücksetzen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-check-circle-fill text-success fs-1"></i>
|
||||||
|
<p class="text-muted mt-2">Aktuell gibt es keine überfälligen Mahnungen.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- E-Mail Modal -->
|
||||||
|
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="emailModalLabel">Mahnungs-E-Mail senden</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="emailForm">
|
||||||
|
<input type="hidden" id="modalLoanId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="modalStudentName" class="form-label">Empfänger</label>
|
||||||
|
<input type="text" class="form-control" id="modalStudentName" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="modalEmail" class="form-label">E-Mail-Adresse</label>
|
||||||
|
<input type="email" class="form-control" id="modalEmail" placeholder="E-Mail-Adresse eingeben...">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="modalMessage" class="form-label">Nachricht</label>
|
||||||
|
<textarea class="form-control" id="modalMessage" rows="4" placeholder="Optionaler Text..."></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitEmailMahnung()">E-Mail senden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openEmailModal(loanId, studentName, studentEmail) {
|
||||||
|
document.getElementById('modalLoanId').value = loanId;
|
||||||
|
document.getElementById('modalStudentName').value = studentName;
|
||||||
|
document.getElementById('modalEmail').value = studentEmail || '';
|
||||||
|
document.getElementById('modalMessage').value = '';
|
||||||
|
|
||||||
|
var myModal = new bootstrap.Modal(document.getElementById('emailModal'));
|
||||||
|
myModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitEmailMahnung() {
|
||||||
|
const loanId = document.getElementById('modalLoanId').value;
|
||||||
|
const email = document.getElementById('modalEmail').value;
|
||||||
|
const message = document.getElementById('modalMessage').value;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
alert('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/mahnungen_send_email', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ loan_id: loanId, email: email, message: message })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('E-Mail erfolgreich gesendet!');
|
||||||
|
var modalEl = document.getElementById('emailModal');
|
||||||
|
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||||
|
modal.hide();
|
||||||
|
} else {
|
||||||
|
alert('Fehler: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Fehler beim Senden der Anfrage.');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMahnung(loanId, studentName) {
|
||||||
|
if (!confirm('Möchten Sie die Mahnung für "' + studentName + '" wirklich zurücksetzen?\n\nDadurch wird die Mahnstufe auf 0 gesetzt, die Frist um 14 Tage verlängert und der Schülerausweis entsperrt.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/mahnungen_reset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ loan_id: loanId })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(data.message);
|
||||||
|
location.reload(); // Seite neu laden, um die aktualisierte Ansicht zu sehen
|
||||||
|
} else {
|
||||||
|
alert('Fehler: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Fehler beim Senden der Anfrage.');
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1531,22 +1531,22 @@
|
|||||||
}
|
}
|
||||||
const isbnField = document.getElementById('isbn');
|
const isbnField = document.getElementById('isbn');
|
||||||
const infoContainer = document.getElementById('book-info-container');
|
const infoContainer = document.getElementById('book-info-container');
|
||||||
|
|
||||||
if (!isbnField || !infoContainer) {
|
if (!isbnField || !infoContainer) {
|
||||||
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
|
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isbn = normalizeIsbnClient(isbnField.value);
|
const isbn = normalizeIsbnClient(isbnField.value);
|
||||||
|
|
||||||
if (!isbn) {
|
if (!isbn) {
|
||||||
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
|
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
|
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
|
||||||
|
|
||||||
// Make API request to fetch book data
|
// Make API request to fetch book data
|
||||||
fetch(`/fetch_book_info/${isbn}`)
|
fetch(`/fetch_book_info/${isbn}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -1564,10 +1564,10 @@
|
|||||||
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
|
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store book data globally for import functionality
|
// Store book data globally for import functionality
|
||||||
currentBookData = data;
|
currentBookData = data;
|
||||||
|
|
||||||
// Display book information with import button
|
// Display book information with import button
|
||||||
infoContainer.innerHTML = `
|
infoContainer.innerHTML = `
|
||||||
<div class="book-info">
|
<div class="book-info">
|
||||||
@@ -1602,23 +1602,23 @@
|
|||||||
alert('Keine Buchdaten verfügbar zum Import.');
|
alert('Keine Buchdaten verfügbar zum Import.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get form fields
|
// Get form fields
|
||||||
const nameField = document.getElementById('name');
|
const nameField = document.getElementById('name');
|
||||||
const descriptionField = document.getElementById('beschreibung');
|
const descriptionField = document.getElementById('beschreibung');
|
||||||
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
|
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
|
||||||
|
|
||||||
if (!nameField || !descriptionField) {
|
if (!nameField || !descriptionField) {
|
||||||
alert('Fehler: Formularfelder nicht gefunden.');
|
alert('Fehler: Formularfelder nicht gefunden.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate book title
|
// Generate book title
|
||||||
let bookTitle = currentBookData.title || '';
|
let bookTitle = currentBookData.title || '';
|
||||||
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
|
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
|
||||||
bookTitle += ` - ${currentBookData.authors}`;
|
bookTitle += ` - ${currentBookData.authors}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate description
|
// Generate description
|
||||||
let description = '';
|
let description = '';
|
||||||
if (currentBookData.description && currentBookData.description !== 'No description available') {
|
if (currentBookData.description && currentBookData.description !== 'No description available') {
|
||||||
@@ -1639,11 +1639,11 @@
|
|||||||
description += `\nSeiten: ${currentBookData.pageCount}`;
|
description += `\nSeiten: ${currentBookData.pageCount}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import the data into form fields
|
// Import the data into form fields
|
||||||
nameField.value = bookTitle;
|
nameField.value = bookTitle;
|
||||||
descriptionField.value = description;
|
descriptionField.value = description;
|
||||||
|
|
||||||
// Preis in das Formularfeld eintragen
|
// Preis in das Formularfeld eintragen
|
||||||
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
|
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
|
||||||
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
|
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
|
||||||
@@ -1655,14 +1655,14 @@
|
|||||||
if (currentBookData.thumbnail) {
|
if (currentBookData.thumbnail) {
|
||||||
downloadBookCover(currentBookData.thumbnail);
|
downloadBookCover(currentBookData.thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const infoContainer = document.getElementById('book-info-container');
|
const infoContainer = document.getElementById('book-info-container');
|
||||||
const successMessage = document.createElement('div');
|
const successMessage = document.createElement('div');
|
||||||
successMessage.className = 'import-success-message';
|
successMessage.className = 'import-success-message';
|
||||||
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
|
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
|
||||||
infoContainer.appendChild(successMessage);
|
infoContainer.appendChild(successMessage);
|
||||||
|
|
||||||
// Remove success message after 3 seconds
|
// Remove success message after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (successMessage.parentNode) {
|
if (successMessage.parentNode) {
|
||||||
|
|||||||
+3
-2
@@ -17,6 +17,7 @@ import re
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -325,7 +326,7 @@ def get_tenant_trial_status(tenant_id=None, now=None):
|
|||||||
"expires_after_days", "ttl_days", or "days".
|
"expires_after_days", "ttl_days", or "days".
|
||||||
"""
|
"""
|
||||||
trial_config = get_tenant_trial_config(tenant_id)
|
trial_config = get_tenant_trial_config(tenant_id)
|
||||||
now = now or datetime.datetime.now()
|
now = now or datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
enabled = bool(trial_config.get('enabled') or trial_config.get('active'))
|
enabled = bool(trial_config.get('enabled') or trial_config.get('active'))
|
||||||
if not enabled:
|
if not enabled:
|
||||||
@@ -447,7 +448,7 @@ def delete_tenant(tenant_id, *, drop_database=True, remove_from_config=True):
|
|||||||
|
|
||||||
def purge_expired_trial_tenants(now=None):
|
def purge_expired_trial_tenants(now=None):
|
||||||
"""Delete expired trial tenants that opted into auto-delete."""
|
"""Delete expired trial tenants that opted into auto-delete."""
|
||||||
now = now or datetime.datetime.now()
|
now = now or datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
purged_tenants = []
|
purged_tenants = []
|
||||||
|
|
||||||
for tenant_id in list(TENANT_REGISTRY.keys()):
|
for tenant_id in list(TENANT_REGISTRY.keys()):
|
||||||
|
|||||||
Reference in New Issue
Block a user