feat: enhance file upload functionality and improve CSV import experience
This commit is contained in:
+186
-38
@@ -2028,11 +2028,6 @@ def _upload_excel_items(scope='inventory'):
|
||||
flash('Nicht angemeldet.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
if not _action_access_allowed(permissions, 'can_insert'):
|
||||
flash('Einfüge-Rechte erforderlich.', 'error')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
is_library_scope = scope == 'library'
|
||||
file_field = 'library_excel' if is_library_scope else 'inventory_excel'
|
||||
fallback_route = 'library_admin' if is_library_scope else 'upload_admin'
|
||||
@@ -2044,9 +2039,20 @@ def _upload_excel_items(scope='inventory'):
|
||||
|
||||
excel_file = request.files.get(file_field)
|
||||
if not excel_file or not excel_file.filename:
|
||||
flash('Bitte eine Excel-Datei auswählen.', 'error')
|
||||
flash('Bitte eine Excel- oder CSV-Datei auswählen.', 'error')
|
||||
return redirect(url_for(fallback_route))
|
||||
|
||||
# Permission handling: allow authenticated normal users to import CSV for inventory scope
|
||||
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
|
||||
has_insert = _action_access_allowed(permissions, 'can_insert')
|
||||
filename_lower = (excel_file.filename or '').lower()
|
||||
is_csv = filename_lower.endswith('.csv')
|
||||
if not has_insert:
|
||||
# Allow CSV imports for authenticated non-admin users only for inventory (non-library)
|
||||
if not (is_csv and not is_library_scope and 'username' in session):
|
||||
flash('Einfüge-Rechte erforderlich.', 'error')
|
||||
return redirect(url_for('home'))
|
||||
|
||||
filename_lower = excel_file.filename.lower()
|
||||
if not filename_lower.endswith(('.xlsx', '.csv')):
|
||||
flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error')
|
||||
@@ -2349,6 +2355,19 @@ def _upload_excel_items(scope='inventory'):
|
||||
flash(f'Excel-Import abgeschlossen: {created_total} Artikel importiert, {len(import_errors)} Zeilen fehlgeschlagen.', 'warning')
|
||||
else:
|
||||
flash(f'Excel-Import erfolgreich: {created_total} Artikel importiert.', 'success')
|
||||
if is_library_scope:
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='library_bulk_import',
|
||||
payload={
|
||||
'channel': 'upload_library_excel',
|
||||
'created_total': created_total,
|
||||
'processed_rows': len(planned_rows),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for library_bulk_import')
|
||||
|
||||
return redirect(url_for('home_admin'))
|
||||
|
||||
|
||||
@@ -3056,20 +3075,13 @@ def api_library_items():
|
||||
|
||||
total_count = items_db.count_documents(query)
|
||||
|
||||
library_items = list(
|
||||
items_db.find(query, projection)
|
||||
.sort([('Name', 1), ('_id', 1)])
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
raw_items = list(items_db.find(query, projection).sort([('Name', 1), ('_id', 1)]).skip(offset).limit(limit))
|
||||
|
||||
item_ids = [str(item.get('_id')) for item in library_items if item.get('_id')]
|
||||
# Build maps for grouping by parent-child relationship (grouped sub-items)
|
||||
all_ids = [str(itm.get('_id')) for itm in raw_items if itm.get('_id')]
|
||||
active_records = []
|
||||
if item_ids:
|
||||
active_records = list(ausleihungen_db.find(
|
||||
{'Item': {'$in': item_ids}, 'Status': 'active'},
|
||||
{'Item': 1, 'User': 1}
|
||||
))
|
||||
if all_ids:
|
||||
active_records = list(ausleihungen_db.find({'Item': {'$in': all_ids}, 'Status': 'active'}, {'Item': 1, 'User': 1}))
|
||||
|
||||
active_item_ids = set()
|
||||
active_user_by_item = {}
|
||||
@@ -3080,32 +3092,115 @@ def api_library_items():
|
||||
active_item_ids.add(item_id)
|
||||
if item_id not in active_user_by_item:
|
||||
active_user_by_item[item_id] = rec.get('User', '')
|
||||
|
||||
client.close()
|
||||
|
||||
# Convert ObjectId to string for JSON serialization
|
||||
for item in library_items:
|
||||
item_id = str(item['_id'])
|
||||
item['_id'] = item_id
|
||||
if item.get('Code4') in (None, '') and item.get('Code_4') not in (None, ''):
|
||||
item['Code4'] = item.get('Code_4')
|
||||
|
||||
condition_value = str(item.get('Condition', '')).strip().lower()
|
||||
has_damage = bool(item.get('HasDamage')) or condition_value == 'destroyed'
|
||||
has_active_borrow = item_id in active_item_ids
|
||||
# Organize children under their parent (ParentItemId) and prepare parent list
|
||||
items_by_id = {}
|
||||
children_by_parent = {}
|
||||
parent_ids = set()
|
||||
for itm in raw_items:
|
||||
iid = str(itm.get('_id'))
|
||||
items_by_id[iid] = itm
|
||||
parent = str(itm.get('ParentItemId') or '')
|
||||
if parent:
|
||||
children_by_parent.setdefault(parent, []).append(itm)
|
||||
else:
|
||||
parent_ids.add(iid)
|
||||
|
||||
# Build aggregated list: for each parent id, include parent + children as one entry
|
||||
aggregated = []
|
||||
processed = set()
|
||||
for pid in list(parent_ids):
|
||||
parent = items_by_id.get(pid)
|
||||
if not parent:
|
||||
continue
|
||||
children = children_by_parent.get(pid, [])
|
||||
|
||||
# Compute grouped counts and availability
|
||||
grouped_units = [parent] + children
|
||||
grouped_all_codes = []
|
||||
available_units = []
|
||||
for unit in grouped_units:
|
||||
unit_id = str(unit.get('_id'))
|
||||
code = unit.get('Code_4') or unit.get('Code4') or ''
|
||||
if code:
|
||||
grouped_all_codes.append(code)
|
||||
if unit.get('Verfuegbar', True):
|
||||
available_units.append({'id': unit_id, 'code': code, 'label': f"{code} ({unit.get('Name','')})"})
|
||||
|
||||
# Convert ObjectId to string for JSON serialization on parent doc copy
|
||||
doc = dict(parent)
|
||||
doc['_id'] = str(parent.get('_id'))
|
||||
if doc.get('Code4') in (None, '') and doc.get('Code_4') not in (None, ''):
|
||||
doc['Code4'] = doc.get('Code_4')
|
||||
|
||||
condition_value = str(doc.get('Condition', '')).strip().lower()
|
||||
has_damage = bool(doc.get('HasDamage')) or condition_value == 'destroyed'
|
||||
has_active_borrow = any(str(unit.get('_id')) in active_item_ids for unit in grouped_units)
|
||||
|
||||
if has_damage and not has_active_borrow:
|
||||
item['LibraryDisplayStatus'] = 'damaged'
|
||||
elif has_active_borrow or item.get('Verfuegbar') is False:
|
||||
item['LibraryDisplayStatus'] = 'borrowed'
|
||||
doc['LibraryDisplayStatus'] = 'damaged'
|
||||
elif has_active_borrow or doc.get('Verfuegbar') is False:
|
||||
doc['LibraryDisplayStatus'] = 'borrowed'
|
||||
else:
|
||||
item['LibraryDisplayStatus'] = 'available'
|
||||
doc['LibraryDisplayStatus'] = 'available'
|
||||
|
||||
item['BorrowedBy'] = active_user_by_item.get(item_id) or item.get('User', '')
|
||||
# Determine borrower: prefer any active borrow on group units, fallback to parent.User
|
||||
borrower = active_user_by_item.get(str(parent.get('_id'))) or ''
|
||||
if not borrower:
|
||||
for unit in grouped_units:
|
||||
u_id = str(unit.get('_id'))
|
||||
if active_user_by_item.get(u_id):
|
||||
borrower = active_user_by_item.get(u_id)
|
||||
break
|
||||
doc['BorrowedBy'] = borrower or doc.get('User', '')
|
||||
|
||||
count = len(library_items)
|
||||
doc['GroupedDisplayCount'] = 1 + len(children)
|
||||
doc['Quantity'] = doc['GroupedDisplayCount']
|
||||
doc['AvailableGroupedCount'] = len(available_units)
|
||||
doc['GroupedAvailableUnits'] = available_units
|
||||
doc['GroupedAllCodes'] = grouped_all_codes
|
||||
|
||||
aggregated.append(doc)
|
||||
processed.add(pid)
|
||||
for c in children:
|
||||
processed.add(str(c.get('_id')))
|
||||
|
||||
# Include any remaining items (orphans or standalones not parented)
|
||||
for itm in raw_items:
|
||||
iid = str(itm.get('_id'))
|
||||
if iid in processed:
|
||||
continue
|
||||
doc = dict(itm)
|
||||
doc['_id'] = iid
|
||||
if doc.get('Code4') in (None, '') and doc.get('Code_4') not in (None, ''):
|
||||
doc['Code4'] = doc.get('Code_4')
|
||||
|
||||
condition_value = str(doc.get('Condition', '')).strip().lower()
|
||||
has_damage = bool(doc.get('HasDamage')) or condition_value == 'destroyed'
|
||||
has_active_borrow = iid in active_item_ids
|
||||
|
||||
if has_damage and not has_active_borrow:
|
||||
doc['LibraryDisplayStatus'] = 'damaged'
|
||||
elif has_active_borrow or doc.get('Verfuegbar') is False:
|
||||
doc['LibraryDisplayStatus'] = 'borrowed'
|
||||
else:
|
||||
doc['LibraryDisplayStatus'] = 'available'
|
||||
|
||||
doc['BorrowedBy'] = active_user_by_item.get(iid) or doc.get('User', '')
|
||||
# Single item: grouped count = 1
|
||||
doc['GroupedDisplayCount'] = 1
|
||||
doc['Quantity'] = 1
|
||||
doc['AvailableGroupedCount'] = 1 if doc.get('Verfuegbar', True) else 0
|
||||
doc['GroupedAvailableUnits'] = [{'id': iid, 'code': doc.get('Code_4') or doc.get('Code4') or '', 'label': f"{doc.get('Code_4') or doc.get('Code4') or '-'} ({doc.get('Name','')})"}] if doc.get('Verfuegbar', True) else []
|
||||
doc['GroupedAllCodes'] = [doc.get('Code_4') or doc.get('Code4') or '']
|
||||
|
||||
aggregated.append(doc)
|
||||
|
||||
client.close()
|
||||
|
||||
count = len(aggregated)
|
||||
return jsonify({
|
||||
'items': library_items,
|
||||
'items': aggregated,
|
||||
'offset': offset,
|
||||
'limit': limit,
|
||||
'count': count,
|
||||
@@ -3390,6 +3485,19 @@ def api_library_item_update(item_id):
|
||||
update_doc['ISBN'] = ''
|
||||
|
||||
items_col.update_one({'_id': ObjectId(item_id)}, {'$set': update_doc})
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='library_item_updated',
|
||||
payload={
|
||||
'channel': 'library_table',
|
||||
'item_id': item_id,
|
||||
'updated_fields': list(update_doc.keys()),
|
||||
'isbn': update_doc.get('ISBN', ''),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for library_item_updated')
|
||||
|
||||
client.close()
|
||||
|
||||
return jsonify({'ok': True, 'message': 'Bibliotheksmedium aktualisiert.'}), 200
|
||||
@@ -5609,7 +5717,21 @@ def upload_item():
|
||||
# Create QR code for the item (deactivated)
|
||||
# create_qr_code(str(item_id))
|
||||
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
|
||||
|
||||
if upload_mode == 'library':
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='library_item_created',
|
||||
payload={
|
||||
'channel': 'upload_admin',
|
||||
'created_item_ids': created_item_ids,
|
||||
'created_count': len(created_item_ids),
|
||||
'isbn': item_isbn,
|
||||
'codes': created_item_ids and [it.get_item(cid).get('Code_4') for cid in created_item_ids] or []
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for library_item_created')
|
||||
|
||||
if is_mobile:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -6160,6 +6282,20 @@ def report_damage(id):
|
||||
except Exception as log_err:
|
||||
app.logger.warning(f"Damage report log write failed for item {id}: {log_err}")
|
||||
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='item_damage_reported',
|
||||
payload={
|
||||
'item_id': id,
|
||||
'item_name': item_doc.get('Name', ''),
|
||||
'reported_by': session.get('username'),
|
||||
'description': description,
|
||||
'damage_count': damage_count,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for item_damage_reported')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Schaden erfolgreich erfasst.',
|
||||
@@ -6242,6 +6378,18 @@ def mark_damage_repaired(id):
|
||||
except Exception as log_err:
|
||||
app.logger.warning(f"Damage repair log write failed for item {id}: {log_err}")
|
||||
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='item_damage_repaired',
|
||||
payload={
|
||||
'item_id': id,
|
||||
'resolved_by': session.get('username'),
|
||||
'resolved_count': len(open_reports),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for item_damage_repaired')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Schäden als repariert markiert.',
|
||||
|
||||
@@ -155,10 +155,33 @@ def _get_int_env(name, default):
|
||||
return int(default)
|
||||
|
||||
def get_version():
|
||||
with open(os.path.join(BASE_DIR, '..', '..', '..', '.docker-build.env'), 'r') as f:
|
||||
for l in f:
|
||||
if l.startswith('INVENTAR_APP_IMAGE='):
|
||||
return l.split(':', 1)[1].strip()
|
||||
# Prefer an explicit release marker if present (created by release process).
|
||||
project_root = os.path.abspath(os.path.join(BASE_DIR, '..', '..', '..'))
|
||||
release_file = os.path.join(project_root, '.release-version')
|
||||
try:
|
||||
if os.path.isfile(release_file):
|
||||
with open(release_file, 'r', encoding='utf-8') as f:
|
||||
val = f.read().strip()
|
||||
if val:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to .docker-build.env (legacy behaviour)
|
||||
env_path = os.path.join(project_root, '.docker-build.env')
|
||||
try:
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for l in f:
|
||||
if l.startswith('INVENTAR_APP_IMAGE='):
|
||||
return l.split(':', 1)[1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Final fallback: use config.json value or packaged default
|
||||
try:
|
||||
return _get(_conf, ['ver'], DEFAULTS.get('version', '0.0.0'))
|
||||
except Exception:
|
||||
return DEFAULTS.get('version', '0.0.0')
|
||||
|
||||
# Expose settings
|
||||
APP_VERSION = get_version()
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Table Header (only in table mode) -->
|
||||
<div class="library-table-wrapper">
|
||||
<div class="table-header" id="tableHeader" style="display: none;">
|
||||
<div class="table-row">
|
||||
<div class="table-cell title-cell">Titel</div>
|
||||
@@ -166,6 +167,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
@@ -720,5 +722,20 @@ function onScanError(error) {
|
||||
background: #fce8e6;
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
/* Mobile: allow horizontal scrolling for table-mode views */
|
||||
@media (max-width: 900px) {
|
||||
.library-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
}
|
||||
.library-table-wrapper .table-row {
|
||||
min-width: 720px; /* allow table to have intrinsic width and be scrolled */
|
||||
}
|
||||
.table-header, .item-content.table-mode {
|
||||
display: block; /* keep rows stacked but allow horizontal scroll */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -513,12 +513,13 @@
|
||||
<table class="library-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%;">Titel</th>
|
||||
<th style="width: 15%;">Autor/Künstler</th>
|
||||
<th style="width: 24%;">Titel</th>
|
||||
<th style="width: 14%;">Autor/Künstler</th>
|
||||
<th style="width: 12%;">ISBN/Code</th>
|
||||
<th style="width: 10%;">Typ</th>
|
||||
<th style="width: 8%;">Typ</th>
|
||||
<th style="width: 8%;">Anzahl</th>
|
||||
<th style="width: 12%;">Status</th>
|
||||
<th style="width: 26%;">Aktionen</th>
|
||||
<th style="width: 22%;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsTableBody">
|
||||
@@ -603,7 +604,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading library items:', error);
|
||||
document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="6" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>';
|
||||
document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="7" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>';
|
||||
} finally {
|
||||
pagingState.loading = false;
|
||||
}
|
||||
@@ -707,6 +708,7 @@
|
||||
<td>${escapeHtml(item.Autor || item.Author || '-')}</td>
|
||||
<td>${escapeHtml(item.ISBN || item.Code_4 || item.Code4 || '-')}</td>
|
||||
<td>${getItemTypeLabel(item.ItemType || 'book')}</td>
|
||||
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
|
||||
<td>
|
||||
<span class="table-status ${statusClass}">
|
||||
${statusText}
|
||||
|
||||
@@ -716,6 +716,8 @@
|
||||
<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>
|
||||
<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>
|
||||
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('library')">?</button>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Bibliothek importieren</button>
|
||||
</form>
|
||||
@@ -726,6 +728,8 @@
|
||||
<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>
|
||||
<form method="POST" action="{{ url_for('upload_inventory_excel') }}" enctype="multipart/form-data" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center;">
|
||||
<input type="file" name="inventory_excel" accept=".xlsx,.csv" required>
|
||||
<button type="button" class="btn btn-link" onclick="downloadSampleCsv('inventory')">Beispiel-CSV herunterladen</button>
|
||||
<button type="button" class="btn btn-outline-secondary" title="Format-Hilfe" onclick="showCsvHelp('inventory')">?</button>
|
||||
<button type="submit" class="btn btn-secondary" name="excel_action" value="validate">Nur validieren</button>
|
||||
<button type="submit" class="btn btn-primary" name="excel_action" value="import">Inventar importieren</button>
|
||||
</form>
|
||||
@@ -920,6 +924,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Help Modal -->
|
||||
<div id="csv-help-modal" class="popup-overlay" style="display:none;">
|
||||
<div class="duplicate-code-popup" style="max-width:760px;">
|
||||
<button class="popup-close-x" onclick="hideCsvHelp()">×</button>
|
||||
<div class="popup-content">
|
||||
<h3>CSV Import Format</h3>
|
||||
<p id="csv-help-text" style="text-align:left; color:var(--ui-text);"></p>
|
||||
<div style="margin-top:12px; text-align:center;">
|
||||
<button class="popup-close-button" onclick="hideCsvHelp()">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function downloadSampleCsv(scope) {
|
||||
let headers = ['name','ort','beschreibung','filter1','filter2','filter3','anschaffungsjahr','anschaffungskosten','code_4','anzahl','isbn','item_type'];
|
||||
let exampleInventory = ['Projektor','Aula','HD-Projektor für Präsentationen','Medien','Technik','','2019','450.00','PRJ-001','1','','general'];
|
||||
let exampleLibrary = ['Harry Potter und der Stein der Weisen','Bibliothek','Kinderbuch von J.K. Rowling','Belletristik','','','1997','12.99','HP-001','1','9783551354013','book'];
|
||||
let row = exampleInventory;
|
||||
if (scope === 'library') row = exampleLibrary;
|
||||
|
||||
const csvContent = [headers.join(','), row.map(v=> '"'+String(v).replace(/"/g,'""')+'"').join(',')].join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = scope === 'library' ? 'sample_library_import.csv' : 'sample_inventory_import.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function showCsvHelp(scope) {
|
||||
const invText = `Erlaubte Spalten (Reihenfolge beliebig): name, ort, beschreibung, filter1, filter2, filter3, anschaffungsjahr, anschaffungskosten, code_4, anzahl, isbn, item_type.\n\n` +
|
||||
`Hinweise:\n- Trenne Felder per Komma. Textfelder sollten in Anführungszeichen stehen, wenn sie Kommas enthalten.\n- Für Bibliotheks-Import ist eine gültige ISBN pro Zeile erforderlich (ISBN-10 oder ISBN-13).\n- 'code_4' kann leer bleiben; das System erzeugt in diesem Fall Codes.\n- 'anzahl' legt die Anzahl physischer Exemplare für diese Zeile fest.`;
|
||||
const libText = invText;
|
||||
document.getElementById('csv-help-text').textContent = scope === 'library' ? libText : invText;
|
||||
document.getElementById('csv-help-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideCsvHelp() {
|
||||
document.getElementById('csv-help-modal').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/html5-qrcode@2.0.9/dist/html5-qrcode.min.js"></script>
|
||||
<script>
|
||||
const libraryModuleEnabled = {{ 'true' if library_module_enabled else 'false' }};
|
||||
|
||||
Reference in New Issue
Block a user