Compare commits
17 Commits
v1.0.0
...
v1.0.5-dev.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0409813cbe | |||
| 95e02b7a75 | |||
| 3b0d499958 | |||
| a7494648d8 | |||
| 8d8609fd70 | |||
| 18c831eb42 | |||
| 8895283904 | |||
| a357881a20 | |||
| b151dae3a8 | |||
| 6167bfca82 | |||
| c44a481382 | |||
| 904972af53 | |||
| de4098ebb2 | |||
| 53b33afa80 | |||
| 0e9c7c6eb2 | |||
| f442648dc9 | |||
| d2e2339d21 |
+33
-21
@@ -2224,8 +2224,8 @@ def _upload_student_cards_excel():
|
|||||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||||
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
||||||
'differenzierungsmerkmal'],
|
'differenzierungsmerkmal'],
|
||||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
'first_name': ['name', 'vorname', 'first_name', 'firstname', 'rufname'],
|
||||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
'last_name': ['nachname', 'last_name', 'lastname', 'familienname'],
|
||||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||||
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
||||||
@@ -8529,20 +8529,18 @@ def parse_csv_users(file_bytes):
|
|||||||
|
|
||||||
return parsed_users
|
return parsed_users
|
||||||
|
|
||||||
def generate_compliant_password(length=16):
|
def generate_compliant_password(length=6):
|
||||||
lowers = string.ascii_lowercase
|
lowers = string.ascii_lowercase
|
||||||
uppers = string.ascii_uppercase
|
uppers = string.ascii_uppercase
|
||||||
digits = string.digits
|
digits = string.digits
|
||||||
symbols = "!@#$%^&*()_+~|}{[]:;?><,.-="
|
|
||||||
|
|
||||||
# Ensure at least one character from each required category
|
# Ensure at least one character from each required category
|
||||||
pwd = [
|
pwd = [
|
||||||
secrets.choice(lowers),
|
secrets.choice(lowers),
|
||||||
secrets.choice(uppers),
|
secrets.choice(uppers),
|
||||||
secrets.choice(digits),
|
secrets.choice(digits)
|
||||||
secrets.choice(symbols)
|
|
||||||
]
|
]
|
||||||
all_chars = lowers + uppers + digits + symbols
|
all_chars = lowers + uppers + digits
|
||||||
pwd += [secrets.choice(all_chars) for _ in range(length - 4)]
|
pwd += [secrets.choice(all_chars) for _ in range(length - 4)]
|
||||||
|
|
||||||
# Shuffle so guaranteed types aren't always at the start
|
# Shuffle so guaranteed types aren't always at the start
|
||||||
@@ -8666,19 +8664,31 @@ def register_csv():
|
|||||||
|
|
||||||
# Benutzernamen & Passwort generieren
|
# Benutzernamen & Passwort generieren
|
||||||
username = us.build_unique_username_from_name(name, last_name)
|
username = us.build_unique_username_from_name(name, last_name)
|
||||||
password = generate_compliant_password(16)
|
password = generate_compliant_password(6)
|
||||||
|
|
||||||
# In DB speichern
|
password = us.check_password_strength(password) and password or generate_compliant_password(6)
|
||||||
success = us.add_user(
|
|
||||||
username=username,
|
action_permissions = None
|
||||||
password=password,
|
page_permissions = None
|
||||||
name=name,
|
|
||||||
last_name=last_name,
|
try:
|
||||||
is_student=False,
|
us.add_user(
|
||||||
student_card_id=None,
|
username,
|
||||||
max_borrow_days=None,
|
password,
|
||||||
permission_preset=permission_preset,
|
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:
|
if success:
|
||||||
created_users.append({
|
created_users.append({
|
||||||
@@ -10379,7 +10389,7 @@ def admin_reset_user_password():
|
|||||||
# Reset the password
|
# Reset the password
|
||||||
try:
|
try:
|
||||||
us.update_password(username, new_password)
|
us.update_password(username, new_password)
|
||||||
flash(f'Passwort für {encrypt_text(username)} wurde erfolgreich zurückgesetzt auf: {new_password}', 'success')
|
flash(f'Passwort für {username} wurde erfolgreich zurückgesetzt auf: {new_password}', 'success')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f'Error resetting password for {encrypt_text(username)}: {e}')
|
app.logger.error(f'Error resetting password for {encrypt_text(username)}: {e}')
|
||||||
flash('Fehler beim Zurücksetzen des Passworts', 'error')
|
flash('Fehler beim Zurücksetzen des Passworts', 'error')
|
||||||
@@ -13759,6 +13769,7 @@ def upload_csv_batch():
|
|||||||
"images_failed": error_count
|
"images_failed": error_count
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
"""
|
||||||
@app.route('/test_email')
|
@app.route('/test_email')
|
||||||
def test_email():
|
def test_email():
|
||||||
|
|
||||||
@@ -13769,4 +13780,5 @@ def test_email():
|
|||||||
send(email="maximiliangruendinger@gmail.com", subject="Test Email from Inventarsystem", note="This is a test email sent from the Inventarsystem application.", sender="Inventarsystem")
|
send(email="maximiliangruendinger@gmail.com", subject="Test Email from Inventarsystem", note="This is a test email sent from the Inventarsystem application.", sender="Inventarsystem")
|
||||||
return "Test email sent successfully."
|
return "Test email sent successfully."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Failed to send test email: {str(e)}", 500
|
return f"Failed to send test email: {str(e)}", 500
|
||||||
|
"""
|
||||||
@@ -431,15 +431,14 @@ def check_password_strength(password):
|
|||||||
if password is None:
|
if password is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(password) < 12:
|
if len(password) < 5:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
has_lower = any(char.islower() for char in password)
|
has_lower = any(char.islower() for char in password)
|
||||||
has_upper = any(char.isupper() for char in password)
|
has_upper = any(char.isupper() for char in password)
|
||||||
has_digit = any(char.isdigit() 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 False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@
|
|||||||
{% if not show_library_features %}
|
{% if not show_library_features %}
|
||||||
<!-- ================= SYSTEM FILTERS 1-3 (INVENTORY / OTHER ITEMS ONLY) ================= -->
|
<!-- ================= SYSTEM FILTERS 1-3 (INVENTORY / OTHER ITEMS ONLY) ================= -->
|
||||||
<div class="filter-inputs">
|
<div class="filter-inputs">
|
||||||
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
|
<h3>{{ filter_names.get('1', 'Fach') }}</h3>
|
||||||
<div class="multi-filter">
|
<div class="multi-filter">
|
||||||
{% for idx in range(4) %}
|
{% for idx in range(4) %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -286,7 +286,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
<h3>{{ filter_names.get('2', 'Jahrgangsstufe') }}</h3>
|
||||||
<div class="multi-filter">
|
<div class="multi-filter">
|
||||||
{% for idx in range(4) %}
|
{% for idx in range(4) %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
1. Modals & Overlays
|
1. Modals & Overlays
|
||||||
========================================= */
|
========================================= */
|
||||||
.modal {
|
.modal {
|
||||||
display: none; /* Managed by JS (none/flex) */
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0; /* Modern shorthand for top: 0, right: 0, bottom: 0, left: 0 */
|
inset: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 900px; /* Consolidated from conflicting 600px/900px rules */
|
max-width: 900px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
background-color: var(--ui-surface, #fff);
|
background-color: var(--ui-surface, #fff);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -133,6 +133,34 @@
|
|||||||
background: var(--ui-surface);
|
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 {
|
.library-scan-status {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 0.92em;
|
font-size: 0.92em;
|
||||||
@@ -419,31 +447,136 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================
|
/* =========================================
|
||||||
7. Mobile Responsiveness
|
7. Mobile Responsiveness (Updated for Cards)
|
||||||
========================================= */
|
========================================= */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.library-table-container { padding: 10px; }
|
.library-table-container { padding: 10px; }
|
||||||
.library-search-bar { flex-direction: column; }
|
.library-search-bar { flex-direction: column; }
|
||||||
.library-search-input { width: 100%; }
|
.library-search-input { width: 100%; }
|
||||||
.library-items-table { font-size: 0.85em; }
|
|
||||||
|
/* Enlarge touch targets */
|
||||||
|
.library-search-input,
|
||||||
|
.library-scan-controls select,
|
||||||
|
.library-scan-controls input,
|
||||||
|
.library-filter-toggle-btn,
|
||||||
|
.filter-item input,
|
||||||
|
.filter-item select,
|
||||||
|
.button {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack Scan & Controls */
|
||||||
|
.library-scan-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.library-scan-controls > * {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons .button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons > div[style*="flex-grow"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert Table to Cards */
|
||||||
|
.library-items-table,
|
||||||
|
.library-items-table thead,
|
||||||
|
.library-items-table tbody,
|
||||||
.library-items-table th,
|
.library-items-table th,
|
||||||
|
.library-items-table td,
|
||||||
|
.library-items-table tr {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-items-table thead tr {
|
||||||
|
position: absolute;
|
||||||
|
top: -9999px;
|
||||||
|
left: -9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-items-table tr {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid var(--ui-border, #ddd);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.library-items-table td {
|
.library-items-table td {
|
||||||
padding: 8px 10px;
|
position: relative;
|
||||||
|
padding: 12px 16px 12px 35% !important;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom: 1px solid #f4f4f4;
|
||||||
|
font-size: 0.95em;
|
||||||
|
min-height: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-items-table td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
width: 30%;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles specific for the Actions cell */
|
||||||
|
.library-items-table td:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 16px !important;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-items-table td:last-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-actions .button {
|
.table-actions .button {
|
||||||
padding: 4px 8px;
|
width: 100%;
|
||||||
font-size: 0.75em;
|
margin: 0 !important;
|
||||||
}
|
padding: 12px;
|
||||||
|
font-size: 1em;
|
||||||
.filter-row,
|
justify-content: center;
|
||||||
.library-scan-controls {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-scan-reader-wrap { max-width: 100%; }
|
.library-scan-reader-wrap { max-width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-scan-reader-wrap {
|
.library-scan-reader-wrap {
|
||||||
display: none;
|
display: none;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
@@ -500,7 +633,6 @@
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.detail-gallery-container {
|
.detail-gallery-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -534,13 +666,12 @@
|
|||||||
|
|
||||||
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
|
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<!-- Search and Filter Toggle -->
|
|
||||||
<!-- Customizable Filter Panel -->
|
|
||||||
<div class="library-header">
|
<div class="library-header">
|
||||||
<h1>📚 Bibliothek</h1>
|
<h1>📚 Bibliothek</h1>
|
||||||
<p>Bücher, CDs, DVDs und weitere Medien</p>
|
<p>Bücher, CDs, DVDs und weitere Medien</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Student card / quick scan workflow -->
|
|
||||||
|
<!-- Search and Filter Toggle -->
|
||||||
<div class="library-search-bar">
|
<div class="library-search-bar">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -556,26 +687,31 @@
|
|||||||
🔍 Filter
|
🔍 Filter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
<div class="library-scan-controls">
|
<div class="library-scan-controls">
|
||||||
<select id="scanModeSelect" aria-label="Scan-Modus">
|
<select id="scanModeSelect" aria-label="Scan-Modus">
|
||||||
<option value="card_only">Nur Ausweis erfassen</option>
|
<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="continuous">Dauermodus: 1x Ausweis, dann N Medien</option>
|
||||||
<option value="return_only">Nur Rückgabe (nur Mediencode)</option>
|
<option value="return_only">Nur Rückgabe (nur Mediencode)</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
|
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
|
||||||
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
||||||
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
|
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
|
||||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
<button id="toggleScannerBtn" class="button" type="button">Handyscanner starten</button>
|
||||||
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
|
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
|
||||||
Physischer Scanner starten
|
Physischer Scanner starten
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
|
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
|
||||||
<div class="library-scan-reader" style="width: 100%; max-width: 640px; margin: 0 auto; overflow: hidden; border-radius: 8px; border: 2px solid #ccc;">
|
<div class="library-scan-reader" style="width: 100%; max-width: 640px; margin: 0 auto; overflow: hidden; border-radius: 8px; border: 2px solid #ccc;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Customizable Filter Panel -->
|
||||||
<div id="filterPanel" class="library-filter-panel">
|
<div id="filterPanel" class="library-filter-panel">
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
@@ -674,7 +810,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// CUSTOM MODAL HELPERS (Replaces native browser alert, confirm & prompt)
|
// CUSTOM MODAL HELPERS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
function createModalOverlay() {
|
function createModalOverlay() {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
@@ -867,6 +1003,7 @@
|
|||||||
input.select();
|
input.select();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 1. GLOBAL STATE DEFINITIONS
|
// 1. GLOBAL STATE DEFINITIONS
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -891,19 +1028,18 @@
|
|||||||
let renderedCount = INITIAL_RENDER_COUNT;
|
let renderedCount = INITIAL_RENDER_COUNT;
|
||||||
let filterPanelOpen = false;
|
let filterPanelOpen = false;
|
||||||
|
|
||||||
// Scanner Related State Variables
|
|
||||||
let scannerInstance = null;
|
let scannerInstance = null;
|
||||||
let scannerRunning = false;
|
let scannerRunning = false;
|
||||||
let activeScannerCallback = null;
|
let activeScannerCallback = null;
|
||||||
let activeStudentCardId = '';
|
let activeStudentCardId = '';
|
||||||
let lastScanValue = '';
|
let lastScanValue = '';
|
||||||
let lastScanAt = 0;
|
let lastScanAt = 0;
|
||||||
// Keyboard-scanner support (physical scanners that act as keyboard wedges)
|
|
||||||
let keyboardScannerEnabled = false;
|
let keyboardScannerEnabled = false;
|
||||||
let keyboardScanBuffer = '';
|
let keyboardScanBuffer = '';
|
||||||
let keyboardLastKeyAt = 0;
|
let keyboardLastKeyAt = 0;
|
||||||
let physicalScannerModalOpen = false;
|
let physicalScannerModalOpen = false;
|
||||||
const KEYBOARD_SCAN_INTERCHAR_MS = 500; // max time between slower scanner keystrokes
|
const KEYBOARD_SCAN_INTERCHAR_MS = 500;
|
||||||
const physicalScanQueue = [];
|
const physicalScanQueue = [];
|
||||||
let keyboardScanProcessing = false;
|
let keyboardScanProcessing = false;
|
||||||
let editLibraryState = {
|
let editLibraryState = {
|
||||||
@@ -1040,13 +1176,14 @@
|
|||||||
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
|
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
|
||||||
const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
|
const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
|
||||||
|
|
||||||
|
// Using data-label injection for accurate mobile rendering mapping
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
|
<td data-label="Titel" class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
|
||||||
<td>${escapeHtml(item.ISBN || '-')}</td>
|
<td data-label="ISBN/Code">${escapeHtml(item.ISBN || '-')}</td>
|
||||||
<td>${getItemTypeLabel(item.ItemType || 'book')}</td>
|
<td data-label="Typ">${getItemTypeLabel(item.ItemType || 'book')}</td>
|
||||||
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
|
<td data-label="Anzahl" style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
|
||||||
<td>
|
<td data-label="Status">
|
||||||
<span class="table-status ${statusClass}">
|
<span class="table-status ${statusClass}">
|
||||||
${statusText}
|
${statusText}
|
||||||
</span>
|
</span>
|
||||||
@@ -1056,7 +1193,7 @@
|
|||||||
<button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}>
|
<button class="button" style="background: #28a745; color: white;" onclick="borrowItem('${item._id}')" ${actionDisabled}>
|
||||||
${actionLabel}
|
${actionLabel}
|
||||||
</button>
|
</button>
|
||||||
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
|
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff; margin-left:6px;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
|
||||||
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
|
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -1125,26 +1262,22 @@
|
|||||||
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
|
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
|
|
||||||
// 1. CRITICAL FIX: Make the container visible BEFORE Quagga initializes
|
|
||||||
// Quagga cannot calculate video dimensions if display is 'none'
|
|
||||||
if (scannerWrap) {
|
if (scannerWrap) {
|
||||||
scannerWrap.style.display = 'block';
|
scannerWrap.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
setScanStatus('Starte Kamera...', 'warn');
|
setScanStatus('Starte Kamera...', 'warn');
|
||||||
|
|
||||||
// 2. Initialize Quagga with a slight delay to allow the DOM to render the block
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Quagga.init({
|
Quagga.init({
|
||||||
inputStream: {
|
inputStream: {
|
||||||
name: "Live",
|
name: "Live",
|
||||||
type: "LiveStream",
|
type: "LiveStream",
|
||||||
// This MUST match the inner div where the video should appear
|
|
||||||
target: document.querySelector('.library-scan-reader'),
|
target: document.querySelector('.library-scan-reader'),
|
||||||
constraints: {
|
constraints: {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
facingMode: "environment" // Prefer back camera on mobile
|
facingMode: "environment"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
locator: {
|
locator: {
|
||||||
@@ -1152,13 +1285,11 @@
|
|||||||
halfSample: true
|
halfSample: true
|
||||||
},
|
},
|
||||||
decoder: {
|
decoder: {
|
||||||
// Keep only the barcode types you actually use to improve performance
|
|
||||||
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
|
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
|
||||||
},
|
},
|
||||||
locate: true
|
locate: true
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
// This will finally log the actual error (e.g., NotAllowedError) if permissions fail
|
|
||||||
console.error("Quagga initialization failed:", err);
|
console.error("Quagga initialization failed:", err);
|
||||||
setScanStatus('Kamera-Fehler: ' + (err.name || err), 'error');
|
setScanStatus('Kamera-Fehler: ' + (err.name || err), 'error');
|
||||||
|
|
||||||
@@ -1190,7 +1321,6 @@
|
|||||||
scannerRunning = false;
|
scannerRunning = false;
|
||||||
activeScannerCallback = null;
|
activeScannerCallback = null;
|
||||||
|
|
||||||
// Hide the container to free up UI space
|
|
||||||
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
|
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
|
||||||
if (scannerWrap) {
|
if (scannerWrap) {
|
||||||
scannerWrap.style.display = 'none';
|
scannerWrap.style.display = 'none';
|
||||||
@@ -1198,7 +1328,7 @@
|
|||||||
|
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
if (toggleBtn) {
|
if (toggleBtn) {
|
||||||
toggleBtn.textContent = 'Kamera Scanner'; // Reset button text
|
toggleBtn.textContent = 'Kamera Scanner';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1210,7 +1340,6 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// Falls du in Flask CSRF-Protect nutzt, muss der Token mitgesendet werden:
|
|
||||||
'X-CSRFToken': '{{ csrf_token }}',
|
'X-CSRFToken': '{{ csrf_token }}',
|
||||||
'X-CSRF-Token': '{{ csrf_token }}'
|
'X-CSRF-Token': '{{ csrf_token }}'
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1347,6 @@
|
|||||||
|
|
||||||
if (!response.ok) return false;
|
if (!response.ok) return false;
|
||||||
|
|
||||||
// Wir parsen die Antwort. Flask jsonify({"is_student_card": true/false})
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data === true || data.is_student_card === true;
|
return data === true || data.is_student_card === true;
|
||||||
|
|
||||||
@@ -1245,7 +1373,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Keyboard scanner handling (physical scanners that send chars then Enter)
|
// Keyboard scanner handling
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
function getPhysicalScannerInstruction() {
|
function getPhysicalScannerInstruction() {
|
||||||
const mode = document.getElementById('scanModeSelect')?.value || 'card_only';
|
const mode = document.getElementById('scanModeSelect')?.value || 'card_only';
|
||||||
@@ -1299,7 +1427,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function keyboardScanKeydownHandler(e) {
|
function keyboardScanKeydownHandler(e) {
|
||||||
// Only active when explicitly enabled
|
|
||||||
if (!keyboardScannerEnabled || !physicalScannerModalOpen) return;
|
if (!keyboardScannerEnabled || !physicalScannerModalOpen) return;
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -1310,7 +1437,6 @@
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// If Enter/Return pressed -> finalize buffer
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const code = keyboardScanBuffer.trim();
|
const code = keyboardScanBuffer.trim();
|
||||||
@@ -1318,7 +1444,6 @@
|
|||||||
keyboardLastKeyAt = 0;
|
keyboardLastKeyAt = 0;
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
|
|
||||||
// Scanner keystrokes must not be submitted into the focused form field.
|
|
||||||
const displayCode = document.getElementById('physicalScannerCode');
|
const displayCode = document.getElementById('physicalScannerCode');
|
||||||
if (displayCode) displayCode.textContent = code;
|
if (displayCode) displayCode.textContent = code;
|
||||||
updatePhysicalScannerModal('Enter erkannt. Code wird verarbeitet...', 'warn');
|
updatePhysicalScannerModal('Enter erkannt. Code wird verarbeitet...', 'warn');
|
||||||
@@ -1326,9 +1451,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept all printable scanner characters, including letters and punctuation.
|
|
||||||
if (e.key.length === 1) {
|
if (e.key.length === 1) {
|
||||||
// If time gap too big, start new buffer
|
|
||||||
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
|
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
|
||||||
keyboardScanBuffer = '';
|
keyboardScanBuffer = '';
|
||||||
}
|
}
|
||||||
@@ -1336,7 +1459,6 @@
|
|||||||
keyboardLastKeyAt = now;
|
keyboardLastKeyAt = now;
|
||||||
const displayCode = document.getElementById('physicalScannerCode');
|
const displayCode = document.getElementById('physicalScannerCode');
|
||||||
if (displayCode) displayCode.textContent = keyboardScanBuffer;
|
if (displayCode) displayCode.textContent = keyboardScanBuffer;
|
||||||
// Prevent default so scanner input is captured even while an input has focus.
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1365,21 +1487,18 @@
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
||||||
return; // Verhindert doppeltes Scannen in kurzer Zeit
|
return;
|
||||||
}
|
}
|
||||||
lastScanValue = scannedCode;
|
lastScanValue = scannedCode;
|
||||||
lastScanAt = now;
|
lastScanAt = now;
|
||||||
|
|
||||||
// Den aktuellen Modus aus dem Dropdown auslesen
|
|
||||||
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
||||||
|
|
||||||
// 1. Modus: Nur Rückgabe
|
|
||||||
if (mode === 'return_only') {
|
if (mode === 'return_only') {
|
||||||
await returnByCode(scannedCode);
|
await returnByCode(scannedCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Modus: Nur Ausweis
|
|
||||||
if (mode === 'card_only') {
|
if (mode === 'card_only') {
|
||||||
setActiveStudentCard(scannedCode);
|
setActiveStudentCard(scannedCode);
|
||||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Ausweis scannen.`, 'ok');
|
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Ausweis scannen.`, 'ok');
|
||||||
@@ -1388,13 +1507,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Modus: Schnellmodus (1x Ausweis, 1x Buch)
|
|
||||||
if (mode === 'quick_toggle') {
|
if (mode === 'quick_toggle') {
|
||||||
await processQuickToggleScan(scannedCode);
|
await processQuickToggleScan(scannedCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Modus: Dauermodus (1x Ausweis, Nx Bücher)
|
|
||||||
if (mode === 'continuous') {
|
if (mode === 'continuous') {
|
||||||
await processContinuousScan(scannedCode);
|
await processContinuousScan(scannedCode);
|
||||||
return;
|
return;
|
||||||
@@ -1471,7 +1588,6 @@
|
|||||||
updatePhysicalScannerModal('Aktion erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
|
updatePhysicalScannerModal('Aktion erfolgreich. Bereit für den nächsten Mediencode.', 'ok');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabellen-Ansicht aktualisieren
|
|
||||||
if (typeof loadLibraryItems === "function") {
|
if (typeof loadLibraryItems === "function") {
|
||||||
await loadLibraryItems();
|
await loadLibraryItems();
|
||||||
}
|
}
|
||||||
@@ -1491,7 +1607,6 @@
|
|||||||
|
|
||||||
async function processQuickToggleScan(scannedCode) {
|
async function processQuickToggleScan(scannedCode) {
|
||||||
|
|
||||||
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
|
|
||||||
if (!activeStudentCardId) {
|
if (!activeStudentCardId) {
|
||||||
setActiveStudentCard(scannedCode);
|
setActiveStudentCard(scannedCode);
|
||||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Mediencode scannen.`, 'ok');
|
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Mediencode scannen.`, 'ok');
|
||||||
@@ -1500,7 +1615,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ausleihe/Rückgabe verarbeiten (wenn Ausweis vorhanden)
|
|
||||||
try {
|
try {
|
||||||
setScanStatus('Verarbeite Mediencode...', 'warn');
|
setScanStatus('Verarbeite Mediencode...', 'warn');
|
||||||
const response = await fetch('/api/library_scan_action', {
|
const response = await fetch('/api/library_scan_action', {
|
||||||
@@ -1695,7 +1809,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showSmallConfirm(message, kind='ok', action = null) {
|
function showSmallConfirm(message, kind='ok', action = null) {
|
||||||
// Append the small helper text in German
|
|
||||||
const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.';
|
const helper = 'Sie können fortfahren. Dies ist nur eine kleine Benachrichtigung.';
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`;
|
el.className = `small-popup ${kind === 'error' ? 'error' : 'ok'}`;
|
||||||
@@ -1720,12 +1833,10 @@
|
|||||||
closeButton.innerHTML = '×';
|
closeButton.innerHTML = '×';
|
||||||
el.appendChild(closeButton);
|
el.appendChild(closeButton);
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
// close handler
|
|
||||||
closeButton.addEventListener('click', () => {
|
closeButton.addEventListener('click', () => {
|
||||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||||
});
|
});
|
||||||
// auto remove after 3 seconds
|
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 15000);
|
||||||
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startNextStudentCardScan() {
|
async function startNextStudentCardScan() {
|
||||||
@@ -1764,11 +1875,9 @@
|
|||||||
const detailContent = document.getElementById('detailContent');
|
const detailContent = document.getElementById('detailContent');
|
||||||
const detailModal = document.getElementById('detailModal');
|
const detailModal = document.getElementById('detailModal');
|
||||||
|
|
||||||
// Lade-Status anzeigen und Modal öffnen
|
|
||||||
detailContent.innerHTML = '<p>Lade Details...</p>';
|
detailContent.innerHTML = '<p>Lade Details...</p>';
|
||||||
detailModal.style.display = 'flex';
|
detailModal.style.display = 'flex';
|
||||||
|
|
||||||
// Daten vom Backend-API-Endpoint abrufen
|
|
||||||
fetch(`/api/item_detail/${itemId}`)
|
fetch(`/api/item_detail/${itemId}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -1832,15 +1941,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schließt das Modal über den 'x'-Button
|
|
||||||
function closeDetailModal() {
|
function closeDetailModal() {
|
||||||
document.getElementById('detailModal').style.display = 'none';
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the modal if the user clicks the dark background overlay
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('detailModal');
|
const modal = document.getElementById('detailModal');
|
||||||
// If the click happened directly on the dark overlay (not the white box)
|
|
||||||
if (event.target === modal) {
|
if (event.target === modal) {
|
||||||
closeDetailModal();
|
closeDetailModal();
|
||||||
}
|
}
|
||||||
@@ -1905,12 +2011,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run when DOM structure is entirely ready
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
wireScannerUi(); // Setup scanner control buttons
|
wireScannerUi();
|
||||||
loadLibraryItems(); // Fetch your database items right away!
|
loadLibraryItems();
|
||||||
|
|
||||||
// Safely connect standard Filters and Search inputs inside DOMContentLoaded
|
|
||||||
const filterToggleBtn = document.getElementById('filterToggleBtn');
|
const filterToggleBtn = document.getElementById('filterToggleBtn');
|
||||||
if (filterToggleBtn) {
|
if (filterToggleBtn) {
|
||||||
filterToggleBtn.addEventListener('click', () => {
|
filterToggleBtn.addEventListener('click', () => {
|
||||||
@@ -1968,15 +2072,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leitet den manuellen Code an die zentrale Logik weiter,
|
|
||||||
// die prüft, welcher Modus im Dropdown aktiv ist.
|
|
||||||
handleScanSuccess(code);
|
handleScanSuccess(code);
|
||||||
|
|
||||||
// Feld nach Eingabe leeren, um direkt den nächsten Code bereitzuhaben
|
|
||||||
manualItemCode.value = '';
|
manualItemCode.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional: Auch auf "Enter" im Textfeld reagieren
|
|
||||||
manualItemCode.addEventListener('keypress', (e) => {
|
manualItemCode.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -2420,8 +2420,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
<div id="table-view-header" class="table-view-header" aria-hidden="true">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<span>Ort</span>
|
<span>Ort</span>
|
||||||
<span>{{ filter_names.get('2', 'Fach') }}</span>
|
<span>{{ filter_names.get('1', 'Fach') }}</span>
|
||||||
<span>{{ filter_names.get('1', 'Jahrgangsstufe') }}</span>
|
<span>{{ filter_names.get('2', 'Jahrgangsstufe') }}</span>
|
||||||
<span>{{ filter_names.get('3', 'Schlagwort') }}</span>
|
<span>{{ filter_names.get('3', 'Schlagwort') }}</span>
|
||||||
<span>Barcode</span>
|
<span>Barcode</span>
|
||||||
<span>Anzahl</span>
|
<span>Anzahl</span>
|
||||||
@@ -3394,9 +3394,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
<div class="card-content" data-item-id="${item._id}">
|
<div class="card-content" data-item-id="${item._id}">
|
||||||
<h3 class="item-col-name">${item.Name}</h3>
|
<h3 class="item-col-name">${item.Name}</h3>
|
||||||
<p class="item-col-location"><strong>Ort:</strong> ${item.Ort || '-'}</p>
|
<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-filter1"><strong>{{ filter_names.get('1', 'Jahrgangsstufe') }}:</strong> ${filter1Display}${filter1More}</p>
|
||||||
<p class="item-col-filter2"><strong>Jahrgangsstufe:</strong> ${filter2Display}${filter2More}</p>
|
<p class="item-col-filter2"><strong>{{ filter_names.get('2', 'Jahrgangsstufe') }}:</strong> ${filter2Display}${filter2More}</p>
|
||||||
<p class="item-col-filter3"><strong>Schlagwort:</strong> ${filter3Display}${filter3More}</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-code"><strong>Barcode:</strong> ${item.Code_4 || '-'}</p>
|
||||||
<p class="item-col-count"><strong>Anzahl:</strong> ${groupedCount}</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>` : ''}
|
${hasDamage ? `<div class="damage-badge">${damageCount > 0 ? `Schäden gemeldet: ${damageCount}` : 'Schäden gemeldet'}</div>` : ''}
|
||||||
|
|||||||
@@ -88,11 +88,10 @@
|
|||||||
<div class="password-rules" id="password-rules" aria-live="polite">
|
<div class="password-rules" id="password-rules" aria-live="polite">
|
||||||
<p class="password-rules-title">Passwort-Anforderungen:</p>
|
<p class="password-rules-title">Passwort-Anforderungen:</p>
|
||||||
<ul>
|
<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-lower" class="pw-rule">Mindestens ein Kleinbuchstabe</li>
|
||||||
<li id="pw-rule-upper" class="pw-rule">Mindestens ein Grossbuchstabe</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-digit" class="pw-rule">Mindestens eine Zahl</li>
|
||||||
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,7 +104,7 @@
|
|||||||
name="password"
|
name="password"
|
||||||
placeholder="Geben Sie ein sicheres Passwort ein"
|
placeholder="Geben Sie ein sicheres Passwort ein"
|
||||||
required
|
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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,11 +200,11 @@ function generateUsername() {
|
|||||||
|
|
||||||
// PASSWORT GENERATOR
|
// PASSWORT GENERATOR
|
||||||
function generateSecurePassword() {
|
function generateSecurePassword() {
|
||||||
const length = 16;
|
const length = 5;
|
||||||
const charsetLower = "abcdefghijklmnopqrstuvwxyz";
|
const charsetLower = "abcdefghijklmnpqrstuvwxyz";
|
||||||
const charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
const charsetUpper = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
|
||||||
const charsetNum = "0123456789";
|
const charsetNum = "123456789";
|
||||||
const charsetSym = "!@#$%^&*()_+~|}{[]:;?><,.-=";
|
const charsetSym = "!#$(),.=";
|
||||||
|
|
||||||
let password = "";
|
let password = "";
|
||||||
// Garantiert mindestens 1 Zeichen aus jeder Kategorie
|
// Garantiert mindestens 1 Zeichen aus jeder Kategorie
|
||||||
|
|||||||
@@ -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" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Bibliotheksausweise - Inventarsystem{% endblock %}
|
{% block title %}Bibliotheksausweise - Inventarsystem{% endblock %}
|
||||||
@@ -21,7 +13,6 @@
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Neu strukturiertes Layout für Formular & Import */
|
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
@@ -145,6 +136,38 @@
|
|||||||
background: #5a6268;
|
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 {
|
.cards-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -255,7 +278,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] .student-card-form,
|
: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;
|
background: var(--ui-surface) !important;
|
||||||
color: var(--ui-text) !important;
|
color: var(--ui-text) !important;
|
||||||
border-color: var(--ui-border) !important;
|
border-color: var(--ui-border) !important;
|
||||||
@@ -269,7 +293,8 @@
|
|||||||
:root[data-theme="dark"] .import-card p,
|
:root[data-theme="dark"] .import-card p,
|
||||||
:root[data-theme="dark"] .rollover-hint,
|
:root[data-theme="dark"] .rollover-hint,
|
||||||
:root[data-theme="dark"] .empty-state,
|
: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;
|
color: var(--ui-text-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +311,8 @@
|
|||||||
:root[data-theme="dark"] .form-group input,
|
:root[data-theme="dark"] .form-group input,
|
||||||
:root[data-theme="dark"] .form-group select,
|
:root[data-theme="dark"] .form-group select,
|
||||||
:root[data-theme="dark"] .form-group textarea,
|
: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 form,
|
||||||
:root[data-theme="dark"] .export-buttons select {
|
:root[data-theme="dark"] .export-buttons select {
|
||||||
background: var(--ui-bg) !important;
|
background: var(--ui-bg) !important;
|
||||||
@@ -297,6 +324,8 @@
|
|||||||
:root[data-theme="dark"] .form-group input:focus,
|
:root[data-theme="dark"] .form-group input:focus,
|
||||||
:root[data-theme="dark"] .form-group select:focus,
|
:root[data-theme="dark"] .form-group select:focus,
|
||||||
:root[data-theme="dark"] .form-group textarea: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 {
|
:root[data-theme="dark"] .export-buttons select:focus {
|
||||||
border-color: #60a5fa !important;
|
border-color: #60a5fa !important;
|
||||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22) !important;
|
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.22) !important;
|
||||||
@@ -334,6 +363,16 @@
|
|||||||
.export-buttons > a {
|
.export-buttons > a {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-counter {
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
@@ -473,7 +512,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
<h3 style="margin:0 0 8px 0;">Excel-Import</h3>
|
||||||
<p style="margin:0 0 12px 0; color:#555; font-size:13px; line-height:1.4;">
|
<p style="margin:0 0 12px 0; color:#555; font-size:13px; line-height:1.4;">
|
||||||
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden Name, Nachname, Klasse, Ausweis-ID, lokales Differenzierungsmerkmal, Notizen & Ausleihdauer.
|
Laden Sie eine <strong>.xlsx</strong>- oder <strong>.csv</strong>-Datei hoch (z. B. aus <strong>ASV</strong>). Erkannt werden 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -503,7 +543,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 style="margin-bottom: 15px;">Registrierte Ausweise</h2>
|
<h2 style="margin-bottom: 15px;">Registrierte Ausweise</h2>
|
||||||
{% if student_cards %}
|
{% 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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ausweis-ID</th>
|
<th>Ausweis-ID</th>
|
||||||
@@ -516,7 +569,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for card in student_cards %}
|
{% 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><strong>{{ card.AusweisId }}</strong></td>
|
||||||
<td>{{ card.SchülerName }}</td>
|
<td>{{ card.SchülerName }}</td>
|
||||||
<td>{{ card.Klasse or '—' }}</td>
|
<td>{{ card.Klasse or '—' }}</td>
|
||||||
@@ -546,6 +599,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div id="noResultsMsg" class="empty-state" style="display: none;">
|
||||||
|
<p>Keine passende Ausweise für diesen Filter gefunden.</p>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>Keine Bibliotheksausweise registriert.</p>
|
<p>Keine Bibliotheksausweise registriert.</p>
|
||||||
@@ -557,7 +614,56 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrcode.js"></script>
|
||||||
<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>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -738,7 +738,7 @@
|
|||||||
{% if show_library_features %}
|
{% if show_library_features %}
|
||||||
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
<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>
|
<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;">
|
<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>
|
<input type="file" name="library_excel" accept=".xlsx,.csv" required>
|
||||||
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('library')">Beispiel-CSV herunterladen</button>
|
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('library')">Beispiel-CSV herunterladen</button>
|
||||||
@@ -750,7 +750,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="upload-import-panel" style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
|
<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>
|
<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;">
|
<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>
|
<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-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
|
||||||
|
|||||||
@@ -228,7 +228,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new-password" class="form-label">Neues Passwort:</label>
|
<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">
|
<div class="password-requirements small text-muted mt-1">
|
||||||
<p>Das Passwort muss mindestens 6 Zeichen lang sein.</p>
|
<p>Das Passwort muss mindestens 6 Zeichen lang sein.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,14 +402,36 @@
|
|||||||
modal.show();
|
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) {
|
function openResetPasswordModal(username) {
|
||||||
document.getElementById('reset-username').value = username;
|
document.getElementById('reset-username').value = username;
|
||||||
document.getElementById('username-display').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'));
|
var modal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPermissionsModal(button) {
|
function openPermissionsModal(button) {
|
||||||
var username = button.getAttribute('data-username') || '';
|
var username = button.getAttribute('data-username') || '';
|
||||||
var preset = button.getAttribute('data-preset') || 'standard_user';
|
var preset = button.getAttribute('data-preset') || 'standard_user';
|
||||||
|
|||||||
Reference in New Issue
Block a user