Compare commits

...

2 Commits

Author SHA1 Message Date
Aiirondev_dev c1a6a145dc The School year rollover button is placed in the school settings.
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 18:00:35 +02:00
Aiirondev_dev 1091854797 change of the manage_filters.html design and removal of irelevant csv info modal
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-24 17:27:49 +02:00
4 changed files with 188 additions and 134 deletions
-63
View File
@@ -228,7 +228,6 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab
client.close()
# Admin route to trigger rollover manually
@app.route('/admin/trigger_school_year_rollover', methods=['POST'])
def admin_trigger_school_year_rollover():
if 'username' not in session:
@@ -3038,42 +3037,6 @@ def optimized_image(filename):
app.logger.error(f"Error serving optimized image {filename}: {str(e)}")
return Response("Optimized image not found", status=404)
# @app.route('/QRCodes/<filename>')
# def qrcode_file(filename):
# """
# Serve QR code files from the QRCodes directory.
#
# Args:
# filename (str): Name of the QR code file to serve
#
# Returns:
# flask.Response: The requested QR code file or placeholder image if not found
# """
# try:
# # Check production path first
# prod_path = "/var/Inventarsystem/Web/QRCodes"
# dev_path = app.config['QR_CODE_FOLDER']
# if os.path.exists(os.path.join(prod_path, filename)):
# return send_from_directory(prod_path, filename)
# if os.path.exists(os.path.join(dev_path, filename)):
# return send_from_directory(dev_path, filename)
#
# # Use a placeholder image if file not found - first try SVG, then PNG
# svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
# png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
#
# if os.path.exists(svg_placeholder_path):
# return send_from_directory(app.static_folder, 'img/no-image.svg')
# elif os.path.exists(png_placeholder_path):
# return send_from_directory(app.static_folder, 'img/no-image.png')
# else:
# return send_from_directory(app.static_folder, 'favicon.ico')
# except Exception as e:
# print(f"Error serving QR code {filename}: {str(e)}")
# return Response("QR code not found", status=404)
@app.route('/<path:filename>')
def catch_all_files(filename):
"""
@@ -3136,32 +3099,6 @@ def test_connection():
"""
return {'status': 'success', 'message': 'Connection successful', 'status_code': 200}
""" if sucess in deployment the funktion can be removed
@app.route('/user_status')
def user_status():
API endpoint to get the current user's status (username, admin status).
Used by JavaScript in templates to personalize the UI.
Returns:
JSON: User status information or error if not authenticated
if 'username' in session:
is_admin = us.check_admin(session['username'])
return jsonify({
'authenticated': True,
'username': session['username'],
'is_admin': is_admin
})
else:
return jsonify({
'authenticated': False,
'error': 'Not logged in'
}), 401
"""
##################################################### changes to be made to account for the new account permison managment system ##############################
@app.route('/')
def home():
+83 -3
View File
@@ -120,9 +120,64 @@
<p>Diese Angaben erscheinen künftig im amtlichen Audit-PDF und in anderen Behördenberichten.</p>
</div>
</div>
<!-- Danger Zone: Schuljahreswechsel -->
<div class="card danger-zone" style="grid-column: 1 / -1; border-color: #fca5a5;">
<div class="card-header" style="background: #fef2f2; border-bottom: 1px solid #fca5a5;">
<h2 style="color: #991b1b;">Erweiterte Aktionen</h2>
</div>
<div class="card-body" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;">
<div>
<strong style="color: #7f1d1d; display: block; font-size: 1.05rem; margin-bottom: 4px;">Schuljahreswechsel durchführen</strong>
<span style="color: #991b1b; font-size: 0.9rem;">Diese Aktion bereitet das System auf das neue Schuljahr vor (z. B. Hochstufen von Klassen).</span>
</div>
<button type="button"
id="btn-rollover"
class="btn btn-danger"
onclick="triggerSchoolYearRollover()">
Schuljahreswechsel auslösen
</button>
</div>
</div>
</div>
</div>
<script>
async function triggerSchoolYearRollover() {
if (!confirm('Möchtest du den Schuljahreswechsel wirklich durchführen? Dies kann nicht rückgängig gemacht werden!')) {
return;
}
const btn = document.getElementById('btn-rollover');
btn.disabled = true;
btn.textContent = 'Wird ausgeführt...';
try {
const response = await fetch('/admin/trigger_school_year_rollover', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.ok) {
alert('Schuljahreswechsel erfolgreich durchgeführt!\n\nZusammenfassung:\n' + JSON.stringify(data.summary, null, 2));
location.reload();
} else {
alert('Fehler: ' + (data.message || 'Schuljahreswechsel konnte nicht durchgeführt werden.'));
}
} catch (error) {
console.error('Rollover error:', error);
alert('Netzwerk- oder Serverfehler beim Ausführen des Schuljahreswechsels.');
} finally {
btn.disabled = false;
btn.textContent = 'Schuljahreswechsel auslösen';
}
}
</script>
<style>
.container {
max-width: 1100px;
@@ -245,12 +300,17 @@
cursor: pointer;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #2563eb;
color: #fff;
}
.btn-primary:hover {
.btn-primary:hover:not(:disabled) {
background: #1d4ed8;
color: #fff;
}
@@ -261,11 +321,21 @@
border-color: #d1d5db;
}
.btn-secondary:hover {
.btn-secondary:hover:not(:disabled) {
background: #e5e7eb;
color: #111827;
}
.btn-danger {
background: #ef4444;
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
color: #fff;
}
.preview-card p {
margin: 0 0 10px;
line-height: 1.45;
@@ -283,5 +353,15 @@
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.danger-zone .card-body {
flex-direction: column;
align-items: flex-start;
}
.danger-zone .btn {
width: 100%;
}
}
</style>
{% endblock %}
{% endblock %}
+105 -67
View File
@@ -1,135 +1,173 @@
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %}
{% block title %}Filter verwalten{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Filterwerte verwalten</h1>
<div class="row">
<!-- Filter 1 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Jahrgangsstufe') }} (Filter 1)</h2>
<div class="container py-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center pb-3 mb-4 border-bottom gap-3">
<div>
<h1 class="h2 fw-bold mb-1">Filterwerte verwalten</h1>
<p class="text-muted mb-0">Verwalten und Bearbeiten Sie die Zuordnungswerte für das Inventar.</p>
</div>
<div>
<a href="{{ url_for('home_admin') }}" class="btn btn-outline-secondary d-inline-flex align-items-center">
<i class="bi bi-arrow-left me-2"></i> Zurück zur Admin-Übersicht
</a>
</div>
</div>
<div class="row g-4">
<div class="col-lg-6">
<div class="card shadow-sm border-0 h-100 rounded-3">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('1', 'Jahrgangsstufe') }}
</h2>
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 1</span>
</div>
<div class="card-body">
<div class="card-body p-4">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
<label class="form-label fw-medium text-secondary">Neuen Wert hinzufügen</label>
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
<input type="text" name="value" class="form-control" placeholder="Wert eingeben..." required>
<button type="submit" class="btn btn-primary d-inline-flex align-items-center">
<i class="bi bi-plus-lg me-1"></i> Hinzufügen
</button>
</div>
</form>
<h5 class="mb-3">Vorhandene Werte</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="h6 text-uppercase text-muted fw-bold mb-0">Vorhandene Werte</h3>
<span class="badge bg-secondary rounded-pill">{{ filter1_values|length if filter1_values else 0 }}</span>
</div>
{% if filter1_values %}
<div class="list-group">
<div class="list-group list-group-flush border rounded-3 overflow-hidden">
{% for value in filter1_values %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-1-{{ loop.index }}')">Bearbeiten</button>
<div class="list-group-item p-3">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-medium">{{ value }}</span>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">
<i class="bi bi-pencil me-1"></i> Bearbeiten
</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
<button type="submit" class="btn btn-outline-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
<i class="bi bi-trash me-1"></i> Entfernen
</button>
</form>
</div>
</div>
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-2">
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-3 pt-3 border-top">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">Abbrechen</button>
<button type="submit" class="btn btn-success" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">
<i class="bi bi-check-lg me-1"></i> Speichern
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">
Abbrechen
</button>
</div>
</form>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">Keine Werte definiert.</div>
<div class="alert alert-light border text-center text-muted mb-0">
<i class="bi bi-info-circle me-1"></i> Keine Werte definiert.
</div>
{% endif %}
</div>
</div>
</div>
<!-- Filter 2 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'Fachgebiet') }} (Filter 2)</h2>
<div class="col-lg-6">
<div class="card shadow-sm border-0 h-100 rounded-3">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h2 class="card-title h5 mb-0 fw-semibold text-primary">
<i class="bi bi-funnel me-2"></i>{{ filter_names.get('2', 'Fachgebiet') }}
</h2>
<span class="badge bg-primary-subtle text-primary rounded-pill">Filter 2</span>
</div>
<div class="card-body">
<div class="card-body p-4">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
<label class="form-label fw-medium text-secondary">Neuen Wert hinzufügen</label>
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
<input type="text" name="value" class="form-control" placeholder="Wert eingeben..." required>
<button type="submit" class="btn btn-primary d-inline-flex align-items-center">
<i class="bi bi-plus-lg me-1"></i> Hinzufügen
</button>
</div>
</form>
<h5 class="mb-3">Vorhandene Werte</h5>
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="h6 text-uppercase text-muted fw-bold mb-0">Vorhandene Werte</h3>
<span class="badge bg-secondary rounded-pill">{{ filter2_values|length if filter2_values else 0 }}</span>
</div>
{% if filter2_values %}
<div class="list-group">
<div class="list-group list-group-flush border rounded-3 overflow-hidden">
{% for value in filter2_values %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-2-{{ loop.index }}')">Bearbeiten</button>
<div class="list-group-item p-3">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-medium">{{ value }}</span>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">
<i class="bi bi-pencil me-1"></i> Bearbeiten
</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
<button type="submit" class="btn btn-outline-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
<i class="bi bi-trash me-1"></i> Entfernen
</button>
</form>
</div>
</div>
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-2">
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-3 pt-3 border-top">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">Abbrechen</button>
<button type="submit" class="btn btn-success" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">
<i class="bi bi-check-lg me-1"></i> Speichern
</button>
<button type="button" class="btn btn-outline-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">
Abbrechen
</button>
</div>
</form>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">Keine Werte definiert.</div>
<div class="alert alert-light border text-center text-muted mb-0">
<i class="bi bi-info-circle me-1"></i> Keine Werte definiert.
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="alert alert-warning">
<strong>Hinweis:</strong> Beim Entfernen eines Filterwertes wird dieser nicht aus bestehenden Objekten entfernt.
Bereits verwendete Werte bleiben in den Objekten erhalten.
</div>
<div class="mt-4">
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
<div class="alert alert-warning border-0 shadow-sm d-flex align-items-center mt-4 rounded-3" role="alert">
<i class="bi bi-exclamation-triangle-fill fs-4 me-3 text-warning"></i>
<div>
<strong>Hinweis:</strong> Beim Entfernen eines Filterwertes wird dieser nicht automatisch aus bestehenden Objekten gelöscht. Bereits zugewiesene Werte bleiben in bestehenden Datensätzen erhalten.
</div>
</div>
</div>
<script>
function toggleEdit(id) {
const el = document.getElementById(id);
if (el.style.display === 'none') {
if (el.style.display === 'none' || el.style.display === '') {
el.style.display = 'block';
const input = el.querySelector('input[type="text"]');
if (input) input.focus();
} else {
el.style.display = 'none';
}
}
</script>
{% endblock %}
{% endblock %}
-1
View File
@@ -670,7 +670,6 @@
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('inventory')">?</button>
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Inventar importieren</button>
</form>