Compare commits

...

22 Commits

Author SHA1 Message Date
Aiirondev_dev f3c7b6cd55 implementation of password visibility changing fuinction for the own password change 2026-09-24 17:26:25 +02:00
Aiirondev_dev c2bfed710d changes to the upload function 2026-09-24 16:46:32 +02:00
Aiirondev_dev 5d90eab154 changes to the edeting filter displaying 2026-09-24 16:40:21 +02:00
Aiirondev_dev 1eadfff9e1 slight changes 2026-09-24 16:33:18 +02:00
Aiirondev_dev 18f096b3f2 changes to the filter change 2026-09-24 16:29:05 +02:00
Aiirondev_dev 4b895a52ab changes to the Liught / Darkmode Toggle to move it to the personal settings 2026-09-23 22:23:42 +02:00
Aiirondev_dev 3a504afe37 Test change for the Darkmode and light mode toggle 2026-09-23 22:13:28 +02:00
Aiirondev_dev fb2ba86140 changes to be implementet to fix the crutial mistake of the variable passing 2026-09-23 21:36:44 +02:00
Aiirondev_dev 72e8a8cf5d fix for the issue implementet 2026-09-23 21:35:34 +02:00
Aiirondev_dev a158d16f62 changes to the passed variables 2026-09-23 21:20:53 +02:00
Aiirondev_dev b8347b80f2 Tryd some changes to the generate_ausweis_id() 2026-09-23 21:17:54 +02:00
Aiirondev_dev 31356e7178 chgange of the Titel Medien / Ausleihe to Ausleihe / Rückgabe 2026-09-23 21:05:52 +02:00
Aiirondev_dev 0409813cbe changes to the style for the Manuell input fields for the Libnrary borrowings. 2026-09-23 17:11:08 +02:00
Aiirondev_dev 95e02b7a75 changes to descriptors 2026-09-23 16:53:59 +02:00
Aiirondev_dev 3b0d499958 changes to implement a filter option for the student cards 2026-09-23 16:25:30 +02:00
Aiirondev_dev a7494648d8 changes to implement an option to view the password beeing typed in 2026-09-23 16:11:12 +02:00
Aiirondev_dev 8d8609fd70 removal of the debug 2026-09-21 15:57:54 +02:00
Aiirondev_dev 18c831eb42 debugging for the next function 2026-09-21 15:39:55 +02:00
Aiirondev_dev 8895283904 debugging for the user generation from the csv import 2026-09-21 15:26:57 +02:00
Aiirondev_dev a357881a20 changes to the upload of users process. 2026-09-21 15:12:07 +02:00
Aiirondev_dev b151dae3a8 changes to the descriptor 2026-09-21 14:51:15 +02:00
Aiirondev_dev 6167bfca82 changes to the main page 2026-09-21 14:36:03 +02:00
12 changed files with 335 additions and 124 deletions
+37 -21
View File
@@ -2301,7 +2301,12 @@ def _upload_student_cards_excel():
row_errors.append('Vorname und Nachname fehlen')
if not ausweis_id and student_name:
ausweis_id = generate_ausweis_id(existing_ids)
ausweis_id = generate_ausweis_id()
if ausweis_id in existing_ids:
row_errors.append(f'Automatisch erzeugte Ausweis-ID {ausweis_id} existiert bereits')
else:
while ausweis_id in existing_ids:
ausweis_id = generate_ausweis_id()
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
existing_ids.add(ausweis_id.upper())
elif ausweis_id and not rollover_mode:
@@ -4850,7 +4855,8 @@ def student_cards_admin():
if ausweis_id:
existing = student_cards.find_one({'AusweisId': ausweis_id})
else:
ausweis_id = generate_ausweis_id()
generated_ausweis_id = generate_ausweis_id()
ausweis_id = generated_ausweis_id
existing = False
if not ausweis_ident:
ausweis_ident = ausweis_id
@@ -4890,10 +4896,10 @@ def student_cards_admin():
return render_template(
'student_cards_admin.html',
student_cards=all_cards, # Hier all_cards übergeben
student_cards=all_cards,
edit_mode=edit_mode,
form_data=form_data,
available_classes=available_classes, # Enthält nun die lesbaren Klassen
available_classes=available_classes,
config=cfg.get_school_info(),
email_service_enabled=cfg.MODULES.is_enabled('mail')
)
@@ -8529,20 +8535,18 @@ def parse_csv_users(file_bytes):
return parsed_users
def generate_compliant_password(length=16):
def generate_compliant_password(length=6):
lowers = string.ascii_lowercase
uppers = string.ascii_uppercase
digits = string.digits
symbols = "!@#$%^&*()_+~|}{[]:;?><,.-="
# Ensure at least one character from each required category
pwd = [
secrets.choice(lowers),
secrets.choice(uppers),
secrets.choice(digits),
secrets.choice(symbols)
secrets.choice(digits)
]
all_chars = lowers + uppers + digits + symbols
all_chars = lowers + uppers + digits
pwd += [secrets.choice(all_chars) for _ in range(length - 4)]
# Shuffle so guaranteed types aren't always at the start
@@ -8666,19 +8670,31 @@ def register_csv():
# Benutzernamen & Passwort generieren
username = us.build_unique_username_from_name(name, last_name)
password = generate_compliant_password(16)
password = generate_compliant_password(6)
# In DB speichern
success = us.add_user(
username=username,
password=password,
name=name,
last_name=last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
)
password = us.check_password_strength(password) and password or generate_compliant_password(6)
action_permissions = None
page_permissions = None
try:
us.add_user(
username,
password,
name,
last_name,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
action_permissions=action_permissions,
page_permissions=page_permissions,
)
success = True
except Exception as e:
app.logger.error(f"Fehler beim Erstellen des Benutzers {username}: {e}")
success = False
continue
if success:
created_users.append({
+2 -3
View File
@@ -431,15 +431,14 @@ def check_password_strength(password):
if password is None:
return False
if len(password) < 12:
if len(password) < 5:
return False
has_lower = any(char.islower() for char in password)
has_upper = any(char.isupper() for char in password)
has_digit = any(char.isdigit() for char in password)
has_symbol = any(not char.isalnum() for char in password)
if not (has_lower and has_upper and has_digit and has_symbol):
if not (has_lower and has_upper and has_digit):
return False
return True
+2 -2
View File
@@ -78,11 +78,11 @@
<div class="form-row">
<div class="form-group">
<label for="filter_name_1">Name Filter 1</label>
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fachgebiet') }}" placeholder="z. B. Fachgebiet">
</div>
<div class="form-group">
<label for="filter_name_2">Name Filter 2</label>
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'Fach') }}" placeholder="z. B. Fach">
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'Klassenstufe') }}" placeholder="z. B. Klassenstufe">
</div>
<div class="form-group">
<label for="filter_name_3">Name Filter 3</label>
+23 -19
View File
@@ -1135,12 +1135,7 @@
<a class="nav-link quick-link-pill {% if current_path == url_for('terminplaner.configure') %}nav-active{% endif %}" href="{{ url_for('terminplaner.configure') }}" data-tutorial-tip="Neuen Terminplan erstellen und an Ihr Team versenden.">Neue Planung</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="termMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">Mehr Optionen</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown">
@@ -1199,6 +1194,13 @@
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
@@ -1228,12 +1230,6 @@
</li>
{% endif %}
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
@@ -1321,6 +1317,13 @@
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
@@ -1342,7 +1345,7 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
{% if current_permissions.pages.get('library_view', False) %}
<li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien/Ausleihe</a>
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Ausleihe/Rückgabe</a>
</li>
{% endif %}
{% if 'username' in session %}
@@ -1359,12 +1362,6 @@
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
</li>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
@@ -1431,6 +1428,13 @@
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
<li><hr class="dropdown-divider"></li>
<li class="dropdown-item" data-nav-fixed="true">
<button type="button" class="btn btn-link nav-link px-3" data-theme-toggle aria-label="Dark Mode umschalten" aria-pressed="false" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
+38 -5
View File
@@ -26,18 +26,27 @@
<form method="POST" action="{{ url_for('change_password') }}">
<div class="form-group">
<label for="current_password">Aktuelles Passwort:</label>
<input type="password" id="current_password" name="current_password" required>
<div class="input-group">
<input type="password" class="form-control" id="current_password" name="current_password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('current_password', this)">Anzeigen</button>
</div>
</div>
<div class="form-group">
<label for="new_password">Neues Passwort:</label>
<input type="password" id="new_password" name="new_password" required>
<div class="input-group">
<input type="password" class="form-control" id="new_password" name="new_password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('new_password', this)">Anzeigen</button>
</div>
<small class="form-text text-muted">Das Passwort sollte mindestens 6 Zeichen lang sein.</small>
</div>
<div class="form-group">
<label for="confirm_password">Neues Passwort bestätigen:</label>
<input type="password" id="confirm_password" name="confirm_password" required>
<div class="input-group">
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility('confirm_password', this)">Anzeigen</button>
</div>
</div>
<div class="form-actions">
@@ -48,6 +57,20 @@
</div>
</div>
<script>
function togglePasswordVisibility(fieldId, button) {
var passwordInput = document.getElementById(fieldId);
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
button.textContent = 'Verbergen';
} else {
passwordInput.type = 'password';
button.textContent = 'Anzeigen';
}
}
</script>
<style>
.password-change-form {
max-width: 500px;
@@ -74,7 +97,7 @@
font-weight: bold;
}
input[type="password"] {
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
@@ -122,6 +145,16 @@
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-outline-secondary {
border: 1px solid #ced4da;
background-color: #f8f9fa;
color: #495057;
}
.btn-outline-secondary:hover {
background-color: #e2e6ea;
}
.alert {
padding: 12px;
@@ -141,4 +174,4 @@
border: 1px solid #c3e6cb;
}
</style>
{% endblock %}
{% endblock %}
+4 -4
View File
@@ -278,8 +278,8 @@
<div class="multi-filter">
{% for idx in range(4) %}
<div class="form-group">
<label for="filter1-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
<select id="filter1-{{ idx + 1 }}" name="filter" class="filter-dropdown-select" data-selected="{{ item.Filter[idx] if item.Filter and item.Filter|length > idx else '' }}">
<label for="filter2-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
<select id="filter2-{{ idx + 1 }}" name="filter" class="filter-dropdown-select" data-selected="{{ item.Filter[idx] if item.Filter and item.Filter|length > idx else '' }}">
<option value="">-- Optional --</option>
</select>
</div>
@@ -290,8 +290,8 @@
<div class="multi-filter">
{% for idx in range(4) %}
<div class="form-group">
<label for="filter2-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
<select id="filter2-{{ idx + 1 }}" name="filter2" class="filter-dropdown-select" data-selected="{{ item.Filter2[idx] if item.Filter2 and item.Filter2|length > idx else '' }}">
<label for="filter1-{{ idx + 1 }}">Wert {{ idx + 1 }}:</label>
<select id="filter1-{{ idx + 1 }}" name="filter2" class="filter-dropdown-select" data-selected="{{ item.Filter2[idx] if item.Filter2 and item.Filter2|length > idx else '' }}">
<option value="">-- Optional --</option>
</select>
</div>
+31 -4
View File
@@ -133,6 +133,34 @@
background: var(--ui-surface);
}
/* Blue edge styling for inputs and action button */
#activeStudentCard,
#manualItemCode {
border: 2px solid #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
}
#activeStudentCard:focus,
#manualItemCode:focus {
outline: none;
border-color: #2563eb !important;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.25) !important;
}
#manualActionBtn {
background: #4f46e5 !important;
color: white !important;
border: 2px solid #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
#manualActionBtn:hover {
background: #4338ca !important;
border-color: #2563eb !important;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3);
}
.library-scan-status {
margin-top: 10px;
font-size: 0.92em;
@@ -605,7 +633,6 @@
letter-spacing: 2px;
}
.detail-gallery-container {
display: flex;
flex-wrap: wrap;
@@ -665,18 +692,18 @@
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
<option value="quick_toggle">Schnellmodus: Ausweis + Mediencode</option>
<option value="quick_toggle">Schnellmodus: 1x Ausweis + 1x Mediencode</option>
<option value="continuous">Dauermodus: 1x Ausweis, dann N Medien</option>
<option value="return_only">Nur Rückgabe (nur Mediencode)</option>
</select>
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
<button id="toggleScannerBtn" class="button" type="button">Handyscanner starten</button>
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
Physischer Scanner starten
</button>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px;">Code verarbeiten</button>
</div>
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
+10 -10
View File
@@ -2343,7 +2343,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="filter-container">
<div class="filter-group">
<div class="filter-header">
<label>{{ filter_names.get('2', 'Fach') }}</label>
<label>{{ filter_names.get('1', 'Fach') }}</label>
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter1-dropdown')">▼</button>
<button type="button" class="clear-filter" onclick="clearFilter(1)">Clear</button>
</div>
@@ -2357,7 +2357,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="filter-group">
<div class="filter-header">
<label>{{ filter_names.get('1', 'Klassenstufe') }}</label>
<label>{{ filter_names.get('2', 'Klassenstufe') }}</label>
<button type="button" class="filter-toggle" onclick="toggleFilterDropdown('filter2-dropdown')">▼</button>
<button type="button" class="clear-filter" onclick="clearFilter(2)">Clear</button>
</div>
@@ -2420,8 +2420,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div id="table-view-header" class="table-view-header" aria-hidden="true">
<span>Name</span>
<span>Ort</span>
<span>{{ filter_names.get('2', 'Fach') }}</span>
<span>{{ filter_names.get('1', 'Jahrgangsstufe') }}</span>
<span>{{ filter_names.get('1', 'Fach') }}</span>
<span>{{ filter_names.get('2', 'Jahrgangsstufe') }}</span>
<span>{{ filter_names.get('3', 'Schlagwort') }}</span>
<span>Barcode</span>
<span>Anzahl</span>
@@ -3394,9 +3394,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="card-content" data-item-id="${item._id}">
<h3 class="item-col-name">${item.Name}</h3>
<p class="item-col-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
<p class="item-col-filter1"><strong>Unterrichtsfach:</strong> ${filter1Display}${filter1More}</p>
<p class="item-col-filter2"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
<p class="item-col-filter3"><strong>Schlagwort:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-filter1"><strong>{{ filter_names.get('1', 'Jahrgangsstufe') }}:</strong> ${filter1Display}${filter1More}</p>
<p class="item-col-filter2"><strong>{{ filter_names.get('2', 'Jahrgangsstufe') }}:</strong> ${filter2Display}${filter2More}</p>
<p class="item-col-filter3"><strong>{{ filter_names.get('3', 'Jahrgangsstufe') }}:</strong> ${filter3Display}${filter3More}</p>
<p class="item-col-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</p>
${hasDamage ? `<div class="damage-badge">${damageCount > 0 ? `Schäden gemeldet: ${damageCount}` : 'Schäden gemeldet'}</div>` : ''}
@@ -4112,17 +4112,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
<div class="detail-group">
<div class="detail-label">Unterrichtsfach:</div>
<div class="detail-label">{{ filter_names.get('1', 'Unterrichtsfach') }}:</div>
<div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Jahrgangsstufe:</div>
<div class="detail-label">{{ filter_names.get('2', 'Jahrgangsstufe') }}:</div>
<div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Schlagwort:</div>
<div class="detail-label">{{ filter_names.get('3', 'Schlagwort') }}:</div>
<div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
</div>
+3 -4
View File
@@ -88,11 +88,10 @@
<div class="password-rules" id="password-rules" aria-live="polite">
<p class="password-rules-title">Passwort-Anforderungen:</p>
<ul>
<li id="pw-rule-length" class="pw-rule">Mindestens 12 Zeichen</li>
<li id="pw-rule-length" class="pw-rule">Mindestens 5 Zeichen</li>
<li id="pw-rule-lower" class="pw-rule">Mindestens ein Kleinbuchstabe</li>
<li id="pw-rule-upper" class="pw-rule">Mindestens ein Grossbuchstabe</li>
<li id="pw-rule-digit" class="pw-rule">Mindestens eine Zahl</li>
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul>
</div>
@@ -105,7 +104,7 @@
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{5,}">
<button type="button" id="toggle-pw-btn" class="toggle-pw-btn" onclick="togglePasswordVisibility()" style="background:none; border:none; cursor:pointer; padding-right:10px;">👁️</button>
</div>
</div>
@@ -201,7 +200,7 @@ function generateUsername() {
// PASSWORT GENERATOR
function generateSecurePassword() {
const length = 12;
const length = 5;
const charsetLower = "abcdefghijklmnpqrstuvwxyz";
const charsetUpper = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
const charsetNum = "123456789";
+121 -15
View File
@@ -1,11 +1,3 @@
<!--
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 %}Bibliotheksausweise - Inventarsystem{% endblock %}
@@ -21,7 +13,6 @@
gap: 15px;
}
/* Neu strukturiertes Layout für Formular & Import */
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
@@ -145,6 +136,38 @@
background: #5a6268;
}
/* Filter Controls Styling */
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 15px;
flex-wrap: wrap;
align-items: center;
background: var(--ui-bg);
padding: 12px;
border-radius: 8px;
border: 1px solid #ddd;
}
.filter-bar input,
.filter-bar select {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.filter-bar input {
flex: 1;
min-width: 200px;
}
.filter-counter {
font-size: 13px;
color: #666;
margin-left: auto;
}
.cards-table {
width: 100%;
border-collapse: collapse;
@@ -255,7 +278,8 @@
}
:root[data-theme="dark"] .student-card-form,
:root[data-theme="dark"] .import-card {
:root[data-theme="dark"] .import-card,
:root[data-theme="dark"] .filter-bar {
background: var(--ui-surface) !important;
color: var(--ui-text) !important;
border-color: var(--ui-border) !important;
@@ -269,7 +293,8 @@
:root[data-theme="dark"] .import-card p,
:root[data-theme="dark"] .rollover-hint,
:root[data-theme="dark"] .empty-state,
:root[data-theme="dark"] .empty-state p {
:root[data-theme="dark"] .empty-state p,
:root[data-theme="dark"] .filter-counter {
color: var(--ui-text-muted) !important;
}
@@ -286,6 +311,8 @@
:root[data-theme="dark"] .form-group input,
:root[data-theme="dark"] .form-group select,
:root[data-theme="dark"] .form-group textarea,
:root[data-theme="dark"] .filter-bar input,
:root[data-theme="dark"] .filter-bar select,
:root[data-theme="dark"] .export-buttons form,
:root[data-theme="dark"] .export-buttons select {
background: var(--ui-bg) !important;
@@ -297,6 +324,8 @@
:root[data-theme="dark"] .form-group input:focus,
:root[data-theme="dark"] .form-group select:focus,
:root[data-theme="dark"] .form-group textarea:focus,
:root[data-theme="dark"] .filter-bar input:focus,
:root[data-theme="dark"] .filter-bar select:focus,
:root[data-theme="dark"] .export-buttons select:focus {
border-color: #60a5fa !important;
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22) !important;
@@ -334,6 +363,16 @@
.export-buttons > a {
flex: 1 1 100%;
}
.filter-bar {
flex-direction: column;
align-items: stretch;
}
.filter-counter {
margin-left: 0;
text-align: right;
}
}
@media (max-width: 992px) {
@@ -473,7 +512,8 @@
<div>
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
<p style="margin:0 0 12px 0; color:#555; font-size:13px; line-height:1.4;">
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden Name, Nachname, Klasse, Ausweis-ID, lokales Differenzierungsmerkmal, Notizen & Ausleihdauer.
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden Rufame, Nachname, Klasse*, Ausweis-ID*, lokales Differenzierungsmerkmal*, Notizen* & Ausleihdauer*.
Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!
</p>
</div>
@@ -503,7 +543,20 @@
<div>
<h2 style="margin-bottom: 15px;">Registrierte Ausweise</h2>
{% if student_cards %}
<table class="cards-table">
<!-- Filter Options -->
<div class="filter-bar">
<input type="text" id="cardSearchInput" placeholder="🔍 Name oder Ausweis-ID suchen...">
<select id="classFilterSelect">
<option value="">Alle Klassen</option>
{% for cls in available_classes %}
<option value="{{ cls }}">{{ cls }}</option>
{% endfor %}
</select>
<button type="button" id="clearFilterBtn" class="btn-cancel" style="padding: 8px 12px; font-size: 13px;">Zurücksetzen</button>
<span class="filter-counter" id="filterCounter">Angezeigt: {{ student_cards|length }} / {{ student_cards|length }}</span>
</div>
<table class="cards-table" id="cardsTable">
<thead>
<tr>
<th>Ausweis-ID</th>
@@ -516,7 +569,7 @@
</thead>
<tbody>
{% for card in student_cards %}
<tr>
<tr class="card-row" data-id="{{ card.AusweisId|lower }}" data-name="{{ card.SchülerName|lower }}" data-class="{{ card.Klasse or '' }}">
<td><strong>{{ card.AusweisId }}</strong></td>
<td>{{ card.SchülerName }}</td>
<td>{{ card.Klasse or '—' }}</td>
@@ -546,6 +599,10 @@
{% endfor %}
</tbody>
</table>
<div id="noResultsMsg" class="empty-state" style="display: none;">
<p>Keine passende Ausweise für diesen Filter gefunden.</p>
</div>
{% else %}
<div class="empty-state">
<p>Keine Bibliotheksausweise registriert.</p>
@@ -557,7 +614,56 @@
<script src="https://cdn.jsdelivr.net/npm/qrcode.js"></script>
<script>
// All PDF exports now go through backend routes
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('cardSearchInput');
const classSelect = document.getElementById('classFilterSelect');
const clearBtn = document.getElementById('clearFilterBtn');
const filterCounter = document.getElementById('filterCounter');
const rows = document.querySelectorAll('.card-row');
const noResultsMsg = document.getElementById('noResultsMsg');
const totalRows = rows.length;
function filterCards() {
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
const selectedClass = classSelect ? classSelect.value : '';
let visibleCount = 0;
rows.forEach(row => {
const id = row.getAttribute('data-id') || '';
const name = row.getAttribute('data-name') || '';
const cardClass = row.getAttribute('data-class') || '';
const matchesQuery = !query || id.includes(query) || name.includes(query);
const matchesClass = !selectedClass || cardClass === selectedClass;
if (matchesQuery && matchesClass) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
if (filterCounter) {
filterCounter.textContent = `Angezeigt: ${visibleCount} / ${totalRows}`;
}
if (noResultsMsg) {
noResultsMsg.style.display = (visibleCount === 0 && totalRows > 0) ? 'block' : 'none';
}
}
if (searchInput) searchInput.addEventListener('input', filterCards);
if (classSelect) classSelect.addEventListener('change', filterCards);
if (clearBtn) {
clearBtn.addEventListener('click', function() {
if (searchInput) searchInput.value = '';
if (classSelect) classSelect.value = '';
filterCards();
});
}
});
</script>
{% endblock %}
+34 -34
View File
@@ -738,7 +738,7 @@
{% if show_library_features %}
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
<h3 style="margin:0 0 8px 0;">Excel-Import Bibliothek (Mehrere Bücher)</h3>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich.</p>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, ISBN, Code, Anzahl). Für den Bibliotheksimport ist eine gültige ISBN je Zeile erforderlich. Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!</p>
<form method="POST" action="{{ url_for('upload_library_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
<input type="file" name="library_excel" accept=".xlsx,.csv" required>
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('library')">Beispiel-CSV herunterladen</button>
@@ -750,7 +750,7 @@
{% else %}
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
<h3 style="margin:0 0 8px 0;">Excel-Import Inventar (Mehrere Artikel)</h3>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl).</p>
<p style="margin:0 0 10px 0; color:#555;">Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch. Spalten werden automatisch erkannt (z.B. Name, Ort, Beschreibung, Filter1/2/3, Kosten, Jahr, Code, Anzahl). Hinweis: Die Daten werden Validiert und müssen dann noch einmal aus sicherheits gründen hochgeladen werden!</p>
<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>
@@ -865,38 +865,6 @@
<!-- Normal Item Mode: Standard Filters -->
<div class="filter-inputs">
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter1-1">Wert 1:</label>
<select id="filter1-1" name="filter" class="filter-dropdown-select" required>
<option value="">-- Bitte auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-2">Wert 2:</label>
<select id="filter1-2" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-3">Wert 3:</label>
<select id="filter1-3" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-4">Wert 4:</label>
<select id="filter1-4" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
</div>
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter2-1">Wert 1:</label>
@@ -928,6 +896,38 @@
</div>
</div>
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
<div class="multi-filter">
<div class="form-group">
<label for="filter1-1">Wert 1:</label>
<select id="filter1-1" name="filter" class="filter-dropdown-select" required>
<option value="">-- Bitte auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-2">Wert 2:</label>
<select id="filter1-2" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-3">Wert 3:</label>
<select id="filter1-3" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="form-group">
<label for="filter1-4">Wert 4:</label>
<select id="filter1-4" name="filter" class="filter-dropdown-select">
<option value="">-- Optional --</option>
<!-- Options will be loaded by JavaScript -->
</select>
</div>
</div>
<h3>{{ filter_names.get('3', 'Schlagwort') }}</h3>
<div class="multi-filter">
<div class="form-group">
+30 -3
View File
@@ -228,7 +228,12 @@
</div>
<div class="mb-3">
<label for="new-password" class="form-label">Neues Passwort:</label>
<input type="password" class="form-control" id="new-password" name="new_password" required>
<div class="input-group">
<input type="password" class="form-control" id="new-password" name="new_password" required>
<button class="btn btn-outline-secondary" type="button" id="togglePasswordBtn" onclick="togglePasswordVisibility()">
Anzeigen
</button>
</div>
<div class="password-requirements small text-muted mt-1">
<p>Das Passwort muss mindestens 6 Zeichen lang sein.</p>
</div>
@@ -397,14 +402,36 @@
modal.show();
}
function togglePasswordVisibility() {
var passwordInput = document.getElementById('new-password');
var toggleBtn = document.getElementById('togglePasswordBtn');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleBtn.textContent = 'Verbergen';
} else {
passwordInput.type = 'password';
toggleBtn.textContent = 'Anzeigen';
}
}
function openResetPasswordModal(username) {
document.getElementById('reset-username').value = username;
document.getElementById('username-display').value = username;
var passwordInput = document.getElementById('new-password');
var toggleBtn = document.getElementById('togglePasswordBtn');
// Reset password field state on modal open
passwordInput.value = '';
passwordInput.type = 'password';
if (toggleBtn) {
toggleBtn.textContent = 'Anzeigen';
}
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
modal.show();
}
function openPermissionsModal(button) {
var username = button.getAttribute('data-username') || '';
var preset = button.getAttribute('data-preset') || 'standard_user';