Compare commits

...

6 Commits

9 changed files with 112 additions and 37 deletions
+8
View File
@@ -556,6 +556,14 @@ sudo chmod 644 certs/inventarsystem.crt
## Fehlerbehebung
### Logs Auslesen
```bash
docker logs inventarsystem-app-1
docker exec inventarsystem-app-1 cat /data/logs/application.log | tail -n 100
docker compose exec mongodb mongosh
```
### Webserver startet nicht
```bash
+27 -21
View File
@@ -731,6 +731,7 @@ AUDIT_INDEXES_READY = False
def _ensure_audit_indexes_once():
"""Ensure audit indexes exist once per process."""
global AUDIT_INDEXES_READY
if AUDIT_INDEXES_READY:
return
@@ -3301,7 +3302,13 @@ def api_library_items():
borrower = doc.get('User', '')
# Decrypt if value is encrypted (decrypt_text returns original if not encrypted)
try:
borrower = decrypt_text(borrower) if borrower else ''
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
student_cards = db['student_cards']
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
card = _decrypt_student_card_doc(card)
client.close()
borrower = card['SchülerName']
except Exception:
# Fallback: keep original string if decryption fails
borrower = borrower or ''
@@ -3820,7 +3827,7 @@ def library_admin():
show_library_features=True,
upload_mode='library',
page_title='Bücher hochladen',
back_target='home_admin'
back_target='library'
)
@@ -5073,7 +5080,7 @@ def upload_item():
Enhanced for mobile browser compatibility.
Returns:
flask.Response: Redirect to admin homepage or JSON response
flask.Response: Redirect to admin homepage
"""
# Check if the user is authenticated
if 'username' not in session:
@@ -5086,7 +5093,12 @@ def upload_item():
return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
can_access_admin_home = _page_access_allowed(permissions, 'home_admin') and _action_access_allowed(permissions, 'can_manage_settings')
success_redirect_endpoint = 'home_admin' if can_access_admin_home else 'home'
if can_access_admin_home:
success_redirect_endpoint = 'home_admin'
elif cfg.MODULES.is_enabled('library') and _page_access_allowed(permissions, 'home_library'):
success_redirect_endpoint = 'home_library'
else:
success_redirect_endpoint = 'home'
# Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
@@ -6043,22 +6055,9 @@ def upload_item():
except Exception:
app.logger.warning('Audit write failed for library_item_created')
if is_mobile:
return jsonify({
'success': True,
'message': success_msg,
'itemId': str(item_id),
'stats': {
'processed': processed_count,
'errors': error_count,
'skipped': skipped_count,
'duplicates': len(duplicate_images) if duplicate_images else 0,
'totalImages': len(image_filenames)
}
})
else:
flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id)))
else:
error_msg = 'Fehler beim Hinzufügen des Elements'
if is_mobile:
@@ -8375,8 +8374,15 @@ def admin_create_invoice(borrow_id):
flash('Für diese Ausleihe existiert bereits eine Rechnung. Bitte Korrekturbuchung verwenden.', 'warning')
return redirect(url_for('admin_borrowings'))
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
student_cards = db['student_cards']
card = student_cards.find_one({'_id': ObjectId(borrow_id)})
card = _decrypt_student_card_doc(card)
client.close()
borrower = card['SchülerName']
invoice_number = existing_invoice.get('invoice_number') or _build_invoice_number(borrow_doc['_id'], now)
borrower = borrow_doc.get('User', '')
item_name = item_doc.get('Name', '')
item_code = item_doc.get('Code_4', '')
+3 -2
View File
@@ -38,7 +38,7 @@ def _active_record_query(extra_query=None):
return base_query
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str=""):
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = ()):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
@@ -54,8 +54,9 @@ def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
'mail': mail,
'note': note,
'title': title,
'custom_fields': custom_fields,
'calendar_enabled': bool(calendar_enabled),
'slots_booked': [], # -> [(start_time, name), ...]the list gets there indexes as the slot 1-defined so is can be counted without an extra variable
'slots_booked': [], # -> [(start_time, name,(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(),
'LastUpdated': datetime.datetime.now()
}
+5 -4
View File
@@ -198,7 +198,7 @@ def build_client_slot_ics(appointment_id: str, slot_start: str, client_name: str
return '\r\n'.join(ics_lines)
def new(date_start: str, date_end: str, time_span: list, slots, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="") -> dict:
def new(date_start: str, date_end: str, time_span: list, slots, slot_length, user: str, mail: list=None, note:str="", calendar_enabled: bool=False, title: str="", custom_fields: list = ()) -> dict:
"""
Generates a link for the executive to send to his clients to book a time Slot
"""
@@ -215,7 +215,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
normalized_time_span = _normalize_time_span(time_span)
normalized_mail = _normalize_mail_list(mail or [])
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title)
id = termin.add(date_start, date_end, normalized_time_span, slots_int, slot_length_int, user, normalized_mail, note, calendar_enabled=calendar_enabled, title=title, custom_fields=custom_fields)
id_str = str(id)
tenant_id = _current_tenant_id()
@@ -254,7 +254,7 @@ def new(date_start: str, date_end: str, time_span: list, slots, slot_length, use
}
def book_slot(id, date_start_time, name):
def book_slot(id, date_start_time, name, custom:list=()):
try:
item = termin.get_item(id)
if not item:
@@ -273,7 +273,7 @@ def book_slot(id, date_start_time, name):
if existing[0] == date_start_time and existing[1] == name:
return False
slots.append((date_start_time, name))
slots.append((date_start_time, name, custom))
success = termin.update(id, slots)
return bool(success)
except Exception as e:
@@ -418,6 +418,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
'link': link,
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
'title': str(item.get('title') or ''),
'custom_fields': list(item.get('custom_fields')),
}
)
+14 -5
View File
@@ -50,6 +50,10 @@ def client(appointment_id):
current_user = str(session.get('username', '') or '').strip()
appointment_item = termin.get_item(appointment_id) or {}
appointment_owner = str(appointment_item.get('user', '') or '').strip()
# Extract the custom fields defined by the appointment owner
custom_fields = appointment_item.get('custom_fields', [])
can_view_booking_names = False
if current_user:
try:
@@ -72,6 +76,9 @@ def client(appointment_id):
if request.method == 'POST':
start_daytime = request.form.get('start_day_time')
username = request.form.get('client_name')
custom_answers = request.form.getlist('custom_answers')
if not start_daytime or not username:
flash('Bitte Name und gewünschte Uhrzeit angeben.', 'error')
return render_template(
@@ -81,9 +88,10 @@ def client(appointment_id):
current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names,
custom_fields=custom_fields
)
if appointment_service.book_slot(appointment_id, start_daytime, username):
if appointment_service.book_slot(appointment_id, start_daytime, username, custom=custom_answers):
return redirect(
url_for(
'terminplaner.client_success',
@@ -103,6 +111,7 @@ def client(appointment_id):
current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
can_view_booking_names=can_view_booking_names,
custom_fields=custom_fields
)
@@ -169,13 +178,13 @@ def configure():
end = request.form.get('end_date')
time = request.form.get('time_frame')
slots_amount = request.form.get('slots_amounts')
slot_length = request.form.get('slot_length') # Korrigiert: slot_length
slot_length = request.form.get('slot_length')
mail = request.form.get('mail', '')
note = request.form.get('note', '')
add_to_calendar = request.form.get('add_to_calendar') == 'on'
title = request.form.get('title', '').strip() # Korrigiert: title statt titel
title = request.form.get('title', '').strip()
custom = request.form.getlist('custom_fields')
# Abfrage ebenfalls auf title und slot_length angepasst
if not start or not end or not time or not slots_amount or not slot_length or not title:
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
return render_template(
@@ -186,7 +195,7 @@ def configure():
)
# Variablen im Funktionsaufruf aktualisiert
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title)
result = appointment_service.new(start, end, time, slots_amount, slot_length, session["username"], mail, note, calendar_enabled=add_to_calendar, title=title, custom_fields=custom)
flash('Der Terminplan wurde angelegt.', 'success')
return render_template(
'termin_configure.html',
+1 -4
View File
@@ -1321,10 +1321,7 @@
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', True) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
{% endif %}
{% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
+10
View File
@@ -109,6 +109,16 @@
<label for="client_name" class="form-label fw-semibold">Ihr Name</label>
<input type="text" id="client_name" name="client_name" class="form-control form-control-lg" placeholder="Vor- und Nachname" required>
</div>
{% if custom_fields %}
{% for field_label in custom_fields %}
<div>
<label class="form-label fw-semibold">{{ field_label }}</label>
<input type="text" name="custom_answers" class="form-control form-control-lg" placeholder="{{ field_label }} eingeben" required>
</div>
{% endfor %}
{% endif %}
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
+43
View File
@@ -77,6 +77,16 @@
<div class="form-text">Wird aus der gebuchten Zeit (abzüglich Pausen) und der Slot-Länge berechnet.</div>
</div>
</div>
<div id="custom-fields-container" class="mt-4">
<h3 class="mb-3">Custom Fields</h3>
<div class="mb-3 custom-field-row">
<input type="text"
name="custom_fields"
class="form-control form-control-lg custom-dynamic-input"
placeholder="Zusatzfeld hinzufügen (z.B. Firma, Kundennummer...)">
</div>
</div>
{% if mail_service_enabled %}
<div>
@@ -351,6 +361,39 @@
syncTextarea();
}
document.addEventListener('DOMContentLoaded', function () {
const container = document.getElementById('custom-fields-container');
// Monitor inputs inside the container using event delegation
container.addEventListener('input', function (event) {
// Only trigger if the user is typing inside our dynamic inputs
if (event.target.classList.contains('custom-dynamic-input')) {
const allInputs = container.querySelectorAll('.custom-dynamic-input');
const lastInput = allInputs[allInputs.length - 1];
// If the user typed something into the absolute last input field, spawn a new one
if (lastInput.value.trim() !== '') {
createNewFieldRow(container);
}
}
});
// Helper function to generate and append a clean new input row
function createNewFieldRow(targetContainer) {
const rowDiv = document.createElement('div');
rowDiv.className = 'mb-3 custom-field-row';
rowDiv.innerHTML = `
<input type="text"
name="custom_fields"
class="form-control form-control-lg custom-dynamic-input"
placeholder="Nächstes Zusatzfeld...">
`;
targetContainer.appendChild(rowDiv);
}
});
// Event Listeners Registration
buildButton.addEventListener('click', renderRows);
startDateInput.addEventListener('change', renderRows);
+1 -1
View File
@@ -954,7 +954,7 @@
<div class="isbn-input-group">
<input type="text" id="isbn" name="isbn" placeholder="ISBN oder Barcode eingeben..." required>
<button type="button" id="scan-isbn-btn" class="fetch-isbn-button">Barcode scannen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Bild abrufen</button>
<button type="button" class="fetch-isbn-button" onclick="fetchBookInfo('upload')">Informationen abrufen</button>
</div>
<div id="isbn-scanner" style="width:100%; max-width:520px; display:none; margin-top:10px;"></div>
<small id="isbn-scan-status" style="display:block; color:#666; margin-top:6px;">Scannen oder manuell eingeben. Gültige ISBNs helfen beim Abruf von Buchdaten, andere Codes werden trotzdem akzeptiert.</small>