Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95e02b7a75 | |||
| 3b0d499958 | |||
| a7494648d8 | |||
| 8d8609fd70 | |||
| 18c831eb42 | |||
| 8895283904 | |||
| a357881a20 | |||
| b151dae3a8 | |||
| 6167bfca82 | |||
| c44a481382 | |||
| 904972af53 | |||
| de4098ebb2 | |||
| 53b33afa80 | |||
| 0e9c7c6eb2 |
+30
-20
@@ -2224,8 +2224,8 @@ def _upload_student_cards_excel():
|
||||
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
||||
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
||||
'differenzierungsmerkmal'],
|
||||
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
||||
'last_name': ['nachname', 'last_name', 'lastname'],
|
||||
'first_name': ['name', 'vorname', 'first_name', 'firstname', 'rufname'],
|
||||
'last_name': ['nachname', 'last_name', 'lastname', 'familienname'],
|
||||
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
||||
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
||||
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
||||
@@ -8529,20 +8529,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 +8664,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({
|
||||
@@ -10379,7 +10389,7 @@ def admin_reset_user_password():
|
||||
# Reset the password
|
||||
try:
|
||||
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:
|
||||
app.logger.error(f'Error resetting password for {encrypt_text(username)}: {e}')
|
||||
flash('Fehler beim Zurücksetzen des Passworts', 'error')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
{% if not show_library_features %}
|
||||
<!-- ================= SYSTEM FILTERS 1-3 (INVENTORY / OTHER ITEMS ONLY) ================= -->
|
||||
<div class="filter-inputs">
|
||||
<h3>{{ filter_names.get('1', 'Jahrgangsstufe') }}</h3>
|
||||
<h3>{{ filter_names.get('1', 'Fach') }}</h3>
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
@@ -286,7 +286,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h3>{{ filter_names.get('2', 'Fachgebiet') }}</h3>
|
||||
<h3>{{ filter_names.get('2', 'Jahrgangsstufe') }}</h3>
|
||||
<div class="multi-filter">
|
||||
{% for idx in range(4) %}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -665,14 +665,14 @@
|
||||
<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>
|
||||
@@ -1809,7 +1809,7 @@
|
||||
closeButton.addEventListener('click', () => {
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 3000);
|
||||
setTimeout(() => { try { if (el && el.parentNode) el.parentNode.removeChild(el); } catch(e){} }, 15000);
|
||||
}
|
||||
|
||||
async function startNextStudentCardScan() {
|
||||
|
||||
@@ -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>` : ''}
|
||||
|
||||
@@ -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,11 +200,11 @@ function generateUsername() {
|
||||
|
||||
// PASSWORT GENERATOR
|
||||
function generateSecurePassword() {
|
||||
const length = 16;
|
||||
const charsetLower = "abcdefghijklmnopqrstuvwxyz";
|
||||
const charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const charsetNum = "0123456789";
|
||||
const charsetSym = "!@#$%^&*()_+~|}{[]:;?><,.-=";
|
||||
const length = 5;
|
||||
const charsetLower = "abcdefghijklmnpqrstuvwxyz";
|
||||
const charsetUpper = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
|
||||
const charsetNum = "123456789";
|
||||
const charsetSym = "!#$(),.=";
|
||||
|
||||
let password = "";
|
||||
// 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" %}
|
||||
|
||||
{% 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user