Compare commits

...

21 Commits

Author SHA1 Message Date
Aiirondev_dev bc5e08142a fix of the json upload format 2026-08-04 22:24:55 +02:00
Aiirondev_dev 373839cf03 fix of the json upload format 2026-08-04 22:17:16 +02:00
Aiirondev_dev 8e31309c55 fix of the json upload format 2026-08-04 21:55:23 +02:00
Aiirondev_dev e8391dbf1e fix of the json upload format 2026-08-04 21:50:17 +02:00
Aiirondev_dev bc274da006 fix of the json upload format 2026-08-04 21:42:52 +02:00
Aiirondev_dev f0b5edff79 fix of the json upload format 2026-08-04 21:11:13 +02:00
Aiirondev_dev 5b95c5202e fix of the json upload format 2026-08-04 21:03:08 +02:00
Aiirondev_dev 51c17d11f2 fix of the json upload format 2026-08-04 20:54:03 +02:00
Aiirondev_dev 91ddc6e864 fix of the json upload format 2026-08-04 20:44:15 +02:00
Aiirondev_dev 88f124c991 fix of the json upload format 2026-08-04 20:42:06 +02:00
Aiirondev_dev 33c65f21b6 fix of the json upload format 2026-08-03 23:55:30 +02:00
Aiirondev_dev fd242a6a0a fix of the json upload format 2026-08-03 23:45:59 +02:00
Aiirondev_dev 2892024969 fix of the json upload format 2026-08-03 23:26:51 +02:00
Aiirondev_dev 27f5280bbf fix of the json upload format 2026-08-03 19:36:56 +02:00
Aiirondev_dev 82898e34cb fix of the json upload format 2026-08-03 19:16:32 +02:00
Aiirondev_dev 44392c2c31 fix of the json upload format 2026-08-03 18:51:47 +02:00
Aiirondev_dev 58d94b716f fix of the json upload format 2026-08-03 18:34:27 +02:00
Aiirondev_dev 579a0ddb75 fix of the json upload format 2026-08-03 18:22:27 +02:00
Aiirondev_dev 0f21e8d9ca fix of the json upload format 2026-08-03 01:02:40 +02:00
Aiirondev_dev 12f7240cd2 fix of the json upload format 2026-08-03 00:52:39 +02:00
Aiirondev_dev 54d8d61358 fix of the json upload format 2026-08-03 00:28:53 +02:00
2 changed files with 359 additions and 237 deletions
+128 -55
View File
@@ -108,7 +108,7 @@ app.config['UPLOAD_FOLDER'] = cfg.UPLOAD_FOLDER
app.config['THUMBNAIL_FOLDER'] = cfg.THUMBNAIL_FOLDER
app.config['PREVIEW_FOLDER'] = cfg.PREVIEW_FOLDER
app.config['ALLOWED_EXTENSIONS'] = set(cfg.ALLOWED_EXTENSIONS)
app.config['MAX_CONTENT_LENGTH'] = max(cfg.MAX_UPLOAD_MB, cfg.IMAGE_MAX_UPLOAD_MB, cfg.VIDEO_MAX_UPLOAD_MB) * 1024 * 1024
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_SECURE'] = cfg.SSL_ENABLED if os.getenv('INVENTAR_SESSION_COOKIE_SECURE') is None else os.getenv('INVENTAR_SESSION_COOKIE_SECURE', '').strip().lower() in ('1', 'true', 'yes', 'on')
@@ -665,11 +665,14 @@ def handle_unexpected_exception(e):
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
# NEU: '/upload_csv_batch' zur Liste hinzufügen, damit Fehler als JSON gesendet werden
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image',
'/log_mobile_issue',
'/upload_csv_batch'}:
return jsonify({'error': message}), 400
flash(message, 'error')
return redirect(url_for('login'))
def _get_current_module(path):
"""Resolve the active UI module for navbar separation."""
mod = cfg.MODULES.get_module_for_path(path)
@@ -11859,23 +11862,58 @@ def batch_upload_page():
return render_template('upload_batch.html')
def clean_db_field(val):
"""Bereinigt Werte, die fälschlicherweise als String-Listen oder mit Klammern aus der CSV kommen."""
import ast
import pandas as pd
if not val or pd.isna(val):
return None
val_str = str(val).strip()
# Wenn es wie eine Liste aussieht (z.B. "['100177']" oder "['']")
if val_str.startswith("[") and val_str.endswith("]"):
try:
parsed = ast.literal_eval(val_str)
if isinstance(parsed, list):
# Nimm das erste Element der Liste, wenn vorhanden
for item in parsed:
cleaned_item = str(item).strip()
if cleaned_item and cleaned_item not in ("", "''", '""', "None", "nan"):
return cleaned_item
return None
except Exception:
# Fallback bei Syntaxfehlern
inner = val_str[1:-1].strip().replace("'", "").replace('"', '')
return inner if inner and inner not in ("''", '""') else None
if val_str in ("[]", "['']", '[""]', "nan", "None", "''", '""'):
return None
return val_str
@app.route('/upload_csv_batch', methods=['POST'])
def upload_csv_batch():
"""
Route for batch adding new items to the inventory via CSV.
Handles CSV parsing, bulk image upload (conversion to WebP), GridFS storage,
and groups identical items based on their Name.
Handles CSV parsing, bulk image upload with deduplication (SHA-256 hash matching),
GridFS storage, code generation, location syncing, and grouped item creation.
"""
import pandas as pd
import ast
if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
import hashlib
username = session['username']
# permissions = _get_current_user_permissions() ... (anpassen wie in Original)
# if not _action_access_allowed(permissions, 'can_insert'):
# return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403
username = session.get('username', 'System')
def generate_unique_batch_code(base_code, position):
if base_code:
return f"{base_code}-{position}"
else:
random_prefix = str(uuid.uuid4())[:6].upper()
return f"BATCH-{random_prefix}-{position}"
fs = get_gridfs()
upload_session_id = str(uuid.uuid4())[:8]
@@ -11890,7 +11928,7 @@ def upload_csv_batch():
# 2. CSV Einlesen und Validieren
try:
df = pd.read_csv(csv_file)
df = pd.read_csv(csv_file, sep=',')
except Exception as e:
app.logger.error(f"[Upload {upload_session_id}] Fehler beim Lesen der CSV: {str(e)}")
return jsonify({"success": False, "message": f"Fehler beim Lesen der CSV: {str(e)}"}), 400
@@ -11898,10 +11936,11 @@ def upload_csv_batch():
if 'Name' not in df.columns:
return jsonify({"success": False, "message": "Die CSV muss zwingend eine 'Name' Spalte enthalten."}), 400
# 3. Bilder verarbeiten, nach WebP konvertieren und in GridFS speichern
# Mapping: Original-Dateiname (ohne Pfad/Erweiterung) -> GridFS Filename (.webp)
# 3. Bilder verarbeiten & Duplikate im selben Durchlauf filtern (Hash-Matching)
image_mapping = {}
processed_hashes = {}
processed_count = 0
dedup_count = 0
error_count = 0
for index, image in enumerate(uploaded_images):
@@ -11913,14 +11952,20 @@ def upload_csv_batch():
image_log_prefix = f"[Upload {upload_session_id}][Image {index + 1}/{len(uploaded_images)}]"
try:
# Annahme: is_allowed, error_message = allowed_file(...)
image.seek(0)
image_bytes = image.read()
if not image_bytes:
error_count += 1
continue
img_hash = hashlib.sha256(image_bytes).hexdigest()
if img_hash in processed_hashes:
existing_filename = processed_hashes[img_hash]
image_mapping[base_name_no_ext] = existing_filename
dedup_count += 1
continue
optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img:
if img.mode not in ('RGB', 'RGBA'):
@@ -11937,8 +11982,7 @@ def upload_csv_batch():
optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
# Speichern in GridFS analog zu upload_item
file_id = fs.put(
fs.put(
optimized_io,
filename=new_filename,
content_type='image/webp',
@@ -11949,7 +11993,7 @@ def upload_csv_batch():
}
)
# Im Mapping speichern (damit wir sie später der CSV zuordnen können)
processed_hashes[img_hash] = new_filename
image_mapping[base_name_no_ext] = new_filename
processed_count += 1
@@ -11957,11 +12001,14 @@ def upload_csv_batch():
app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}")
error_count += 1
# 4. Items gruppieren (Analog zu series_group_id aus upload_item)
# Gruppierung über den Namen: Alle Zeilen mit demselben Namen gehören zur selben Serie
df['Name'] = df['Name'].fillna('Unbenannt').astype(str)
# 4. Predefined Locations laden
try:
predefined_locations = it.get_predefined_locations()
except Exception:
predefined_locations = []
# Optional: Fülle NaN Werte in der CSV mit sinnvollen Defaults für die Datenbank
# 5. Dataframe bereinigen
df['Name'] = df['Name'].fillna('Unbenannt').astype(str).str.strip()
df = df.fillna({
'Ort': 'Unbekannt',
'Beschreibung': '',
@@ -11970,40 +12017,59 @@ def upload_csv_batch():
'Anschaffungskosten': ''
})
# --- WICHTIG: Gruppierung über einen normalisierten Schlüssel ermöglichen ---
# Erstellt eine unsichtbare Hilfsspalte, die Leerzeichen/Groß-Kleinschreibung ignoriert,
# damit identische Artikel-Typen sauber als Serie erkannt werden.
df['GroupKey'] = df['Name'].str.lower()
created_item_ids = []
grouped_items = df.groupby('GroupKey')
grouped_items = df.groupby('Name')
for name, group in grouped_items:
for group_key, group in grouped_items:
item_count = len(group)
series_group_id = str(uuid.uuid4()) if item_count > 1 else None
parent_item_id = None
# Originalen Namen des ersten Elements der Gruppe übernehmen
actual_group_name = group.iloc[0]['Name']
# Basis-Code für automatisierte Seriencodes ermitteln
first_row_code = clean_db_field(group.iloc[0].get('Code_4', ''))
base_code = first_row_code if first_row_code else None
for position, (index, row) in enumerate(group.iterrows(), start=1):
# Bilder aus der CSV-Zeile extrahieren und über das image_mapping mappen
ort_val = str(row['Ort']).strip()
if ort_val and ort_val not in predefined_locations:
try:
it.add_predefined_location(ort_val)
predefined_locations.append(ort_val)
except Exception as e:
app.logger.warning(f"Ort {ort_val} konnte nicht hinzugefügt werden: {e}")
# Bilder zuordnen und pro Artikel deduplizieren
item_image_filenames = []
if 'Images' in row and pd.notna(row['Images']):
try:
# Aus "['Bild1.JPG', 'Bild2.JPG']" wird eine Liste
img_list = ast.literal_eval(str(row['Images']))
if isinstance(img_list, list):
for img_name in img_list:
base_img_name = os.path.splitext(img_name)[0]
# Falls das Bild hochgeladen wurde, die WebP GridFS ID/Name nehmen
if base_img_name in image_mapping:
item_image_filenames.append(image_mapping[base_img_name])
else:
app.logger.warning(f"Bild {img_name} in CSV definiert, aber nicht hochgeladen.")
except (ValueError, SyntaxError):
pass
# Filter extrahieren (falls vorhanden, erwarte string list wie "['HSU', '', '', '']")
unique_image_filenames = []
for img in item_image_filenames:
if img not in unique_image_filenames:
unique_image_filenames.append(img)
def parse_filter_col(col_data):
try:
res = ast.literal_eval(str(col_data))
return res if isinstance(res, list) else []
except:
except Exception:
return []
filter_upload = parse_filter_col(row.get('Filter', '[]'))
@@ -12012,46 +12078,53 @@ def upload_csv_batch():
reservierbar = bool(row.get('Reservierbar', False))
# DB Insert Funktion aufrufen (orientiert an deiner upload_item)
# Code_4 / Barcode sauber extrahieren und bereinigen
raw_code = row.get('Code_4') or row.get('Barcode') or ''
row_code = clean_db_field(raw_code)
if row_code:
unique_code = row_code
elif item_count > 1:
unique_code = generate_unique_batch_code(base_code, position)
else:
unique_code = None
# DB Insert
item_id = it.add_item(
name=row['Name'],
ort=row['Ort'],
beschreibung=row['Beschreibung'],
image_filenames=item_image_filenames,
filter_upload=filter_upload,
filter_upload2=filter_upload2,
filter_upload3=filter_upload3,
anschaffungs_jahr=str(row['Anschaffungsjahr']) if row['Anschaffungsjahr'] else None,
anschaffungs_kosten=str(row['Anschaffungskosten']) if row['Anschaffungskosten'] else None,
code_4=str(row['Code_4']) if row['Code_4'] else None,
str(actual_group_name), # 1. Name
ort_val, # 2. Ort
str(row['Beschreibung']), # 3. Beschreibung
unique_image_filenames, # 4. Image Filenames (GridFS)
filter_upload, # 5. Filter 1
filter_upload2, # 6. Filter 2
filter_upload3, # 7. Filter 3
clean_db_field(row.get('Anschaffungsjahr')), # 8. Jahr
clean_db_field(row.get('Anschaffungskosten')),# 9. Kosten
unique_code, # 10. Unique Code / Code_4
reservierbar=reservierbar,
series_group_id=series_group_id,
series_count=item_count,
series_position=position,
is_grouped_sub_item=(position > 1),
parent_item_id=parent_item_id,
# Default Werte, falls keine Bibliotheks-CSV
isbn='',
item_type='other',
library_category='',
is_library=False
isbn=str(row.get('ISBN', '')),
item_type=str(row.get('Item_Type', 'other')),
library_category=str(row.get('Library_Category', '')),
is_library=bool(row.get('Is_Library', False))
)
if item_id:
created_item_ids.append(item_id)
# Das erste Item in einer Serie wird der Parent für die restlichen
if position == 1:
parent_item_id = str(item_id)
else:
app.logger.error(f"Fehler beim Erstellen von Item: {row['Name']} (Index {index})")
app.logger.info(
f"Batch Upload abgeschlossen: {len(created_item_ids)} Items erstellt. {processed_count} Bilder verarbeitet.")
app.logger.error(f"Fehler beim Erstellen von Item: {actual_group_name} (Index {index})")
return jsonify({
"success": True,
"message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert und {processed_count} Bilder konvertiert.",
"message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert.",
"created_count": len(created_item_ids),
"images_processed": processed_count,
"images_deduplicated": dedup_count,
"images_failed": error_count
}), 200
+231 -182
View File
@@ -3,211 +3,260 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batch Upload - CSV & Bilder</title>
<title>Batch Upload</title>
<!-- CSRF-Token für JavaScript bereitstellen -->
<meta name="csrf-token" content="{{ session.get('_csrf_token', '') }}">
<style>
:root {
--primary-color: #4a90e2;
--background-color: #f4f7f6;
--text-color: #333;
--border-radius: 8px;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.upload-container {
background: white;
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 100%;
max-width: 500px;
}
h2 {
margin-top: 0;
color: var(--primary-color);
text-align: center;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
input[type="file"] {
display: block;
width: 100%;
padding: 0.5rem;
border: 1px dashed #ccc;
border-radius: var(--border-radius);
background: #fafafa;
cursor: pointer;
}
.btn-submit {
width: 100%;
padding: 0.75rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn-submit:hover {
background-color: #357abd;
}
.btn-submit:disabled {
background-color: #a0c4e8;
cursor: not-allowed;
}
/* Status & Feedback Messages */
#status-message {
margin-top: 1rem;
padding: 1rem;
border-radius: var(--border-radius);
display: none;
text-align: center;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
background-color: #e2e3e5;
color: #383d41;
border: 1px solid #d6d8db;
}
.spinner {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
border: 3px solid rgba(0,0,0,0.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
vertical-align: middle;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
body { font-family: sans-serif; padding: 20px; background: #f4f7f6; }
.upload-container { background: white; padding: 20px; border-radius: 8px; max-width: 500px; margin: 0 auto; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: bold; margin-bottom: 5px; }
input[type="file"] { width: 100%; padding: 8px; box-sizing: border-box; }
.btn-submit { width: 100%; padding: 10px; background: #4a90e2; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; }
.btn-submit:disabled { background: #ccc; }
#uploadProgress { margin-top: 20px; display: none; }
progress { width: 100%; height: 20px; }
#logList { background: #fafafa; border: 1px solid #eee; padding: 10px; max-height: 150px; overflow-y: auto; font-size: 0.85em; list-style: none; }
</style>
</head>
<body>
<div class="upload-container">
<h2>Inventar Batch Upload</h2>
<div class="upload-container">
<h2>Inventar Batch Upload</h2>
<form id="uploadForm">
<div class="form-group">
<label for="csv_file">1. items.csv Datei auswählen</label>
<!-- Akzeptiert nur CSV Dateien -->
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<form id="batchUploadForm">
<div class="form-group">
<label for="csv_file">1. items.csv auswählen</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<div class="form-group">
<label for="images">2. Bilder auswählen</label>
<!-- multiple erlaubt das Auswählen mehrerer Bilder gleichzeitig -->
<input type="file" id="images" name="images" accept="image/*" multiple required>
<small style="color: #666; display: block; margin-top: 5px;">Du kannst mehrere Bilder markieren (Strg/Cmd gedrückt halten).</small>
</div>
<div class="form-group">
<label for="images">2. Bilder auswählen</label>
<input type="file" id="images" name="images" accept="image/*" multiple required>
</div>
<button type="submit" id="submitBtn" class="btn-submit">Daten hochladen</button>
</form>
<button type="submit" id="uploadBtn" class="btn-submit">Daten hochladen</button>
</form>
<div id="status-message"></div>
<div id="uploadProgress">
<div id="progressText">Bereite Upload vor...</div>
<progress id="progressBar" value="0" max="100"></progress>
<ul id="logList"></ul>
</div>
</div>
<script>
document.getElementById('uploadForm').addEventListener('submit', async function(e) {
e.preventDefault(); // Verhindert das Neuladen der Seite
// 1. Robuster CSV-Parser, der Zeilenumbrüche und Kommas in Texten korrekt ignoriert
function parseCSV(csvString) {
const rows = [];
let currentRow = [];
let currentCell = '';
let insideQuotes = false;
const form = e.target;
const submitBtn = document.getElementById('submitBtn');
const statusDiv = document.getElementById('status-message');
for (let i = 0; i < csvString.length; i++) {
const char = csvString[i];
const nextChar = csvString[i + 1];
// UI auf "Laden" setzen
submitBtn.disabled = true;
submitBtn.innerText = 'Wird verarbeitet...';
statusDiv.className = 'loading';
statusDiv.style.display = 'block';
statusDiv.innerHTML = '<div class="spinner"></div> Lade Dateien hoch und verarbeite Bilder... Bitte warten.';
if (char === '"' && insideQuotes && nextChar === '"') {
currentCell += '"';
i++; // Escaped Quotes ("") überspringen
} else if (char === '"') {
insideQuotes = !insideQuotes;
} else if (char === ',' && !insideQuotes) {
currentRow.push(currentCell);
currentCell = '';
} else if ((char === '\n' || char === '\r') && !insideQuotes) {
if (char === '\r' && nextChar === '\n') i++; // Windows Umbrüche überspringen
currentRow.push(currentCell);
if (currentRow.length > 1 || currentRow[0] !== '') {
rows.push(currentRow);
}
currentRow = [];
currentCell = '';
} else {
currentCell += char;
}
}
if (currentCell !== '' || currentRow.length > 0) {
currentRow.push(currentCell);
if (currentRow.length > 1 || currentRow[0] !== '') {
rows.push(currentRow);
}
}
return rows;
}
// FormData sammelt alle Inputs aus dem Formular (csv_file und images)
const formData = new FormData(form);
// 2. Baut das Array wieder sicher zu einer sauberen CSV-Zeile für das Backend zusammen
function rowToCSV(rowArray) {
return rowArray.map(cell => {
if (cell === null || cell === undefined) return '';
let cellStr = String(cell);
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n') || cellStr.includes('\r')) {
return '"' + cellStr.replace(/"/g, '""') + '"';
}
return cellStr;
}).join(',');
}
// 3. Bildnamen extrahieren
function extractImageNames(cellValue) {
if (!cellValue) return [];
let raw = String(cellValue).replace(/^["']|["']$/g, '').trim();
if (raw.startsWith('[') && raw.endsWith(']')) {
try {
const jsonValid = raw.replace(/'/g, '"');
return JSON.parse(jsonValid);
} catch (e) {
const matches = raw.match(/['"]([^'"]+)['"]/g);
if (matches) return matches.map(m => m.replace(/['"]/g, ''));
}
}
return raw ? [raw] : [];
}
document.getElementById('batchUploadForm').addEventListener('submit', async function(e) {
e.preventDefault();
const csvInput = document.getElementById('csv_file');
const imageInput = document.getElementById('images');
const uploadBtn = document.getElementById('uploadBtn');
const progressContainer = document.getElementById('uploadProgress');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const logList = document.getElementById('logList');
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (!csvInput.files.length) {
alert("Bitte wähle eine CSV-Datei aus.");
return;
}
uploadBtn.disabled = true;
progressContainer.style.display = 'block';
logList.innerHTML = '';
const log = (msg) => {
const li = document.createElement('li');
li.textContent = msg;
logList.appendChild(li);
logList.scrollTop = logList.scrollHeight;
};
const csvFile = csvInput.files[0];
const allImages = Array.from(imageInput.files);
const imageMap = new Map();
allImages.forEach(file => {
imageMap.set(file.name.toLowerCase(), file);
});
const BATCH_SIZE = 20;
try {
const csvText = await csvFile.text();
const allRows = parseCSV(csvText);
if (allRows.length <= 1) {
throw new Error("CSV-Datei ist leer oder enthält nur Kopfzeilen.");
}
const headerRow = allRows[0];
const dataRows = allRows.slice(1);
log(`${dataRows.length} Einträge gefunden. Bereite Batches vor...`);
const imagesColIndex = headerRow.findIndex(h => h.toLowerCase().trim() === 'images');
const batches = [];
for (let i = 0; i < dataRows.length; i += BATCH_SIZE) {
batches.push(dataRows.slice(i, i + BATCH_SIZE));
}
progressBar.max = batches.length;
progressBar.value = 0;
// Sequenzieller Upload mit Fehlertoleranz pro Batch
for (let b = 0; b < batches.length; b++) {
const batchRows = batches[b];
progressText.textContent = `Lade Batch ${b + 1} von ${batches.length} hoch...`;
try {
// Sende die Daten an den Flask-Endpoint
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData
const requiredImagesForBatch = new Set();
if (imagesColIndex !== -1) {
batchRows.forEach(rowCols => {
if (rowCols[imagesColIndex]) {
const imgNames = extractImageNames(rowCols[imagesColIndex]);
imgNames.forEach(name => {
const fileMatch = imageMap.get(name.toLowerCase());
if (fileMatch) {
requiredImagesForBatch.add(fileMatch);
}
});
}
});
}
const batchCsvArray = [headerRow, ...batchRows];
const batchCsvText = batchCsvArray.map(rowToCSV).join('\n');
const batchCsvBlob = new Blob([batchCsvText], { type: 'text/csv' });
const formData = new FormData();
formData.append('csv_file', batchCsvBlob, `batch_${b + 1}.csv`);
if (csrfToken) {
formData.append('csrf_token', csrfToken);
}
requiredImagesForBatch.forEach(imgFile => {
formData.append('images', imgFile);
});
log(`Batch ${b + 1}: ${batchRows.length} Items & ${requiredImagesForBatch.size} zugehörige Bilder.`);
const response = await fetch('/upload_csv_batch', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': csrfToken || ''
}
});
const responseText = await response.text();
let result;
try {
// Versuche, die Antwort als JSON zu lesen
result = await response.json();
} catch (jsonError) {
// Wenn der Server kein JSON, sondern HTML (z.B. bei einem Python-Crash) sendet
const errorText = await response.text();
console.error("Server hat kein JSON gesendet. Antwort war:", errorText);
throw new Error("Der Server hat einen HTML-Fehler zurückgegeben (Python-Crash oder falscher Pfad). Siehe Konsole.");
result = JSON.parse(responseText);
} catch (parseErr) {
throw new Error(`Server-Fehler (Status ${response.status}). HTML statt JSON erhalten.`);
}
if (response.ok && result.success) {
// Erfolgreicher Upload
statusDiv.className = 'success';
statusDiv.innerHTML = `
<strong>Erfolg!</strong><br>
${result.message}
`;
form.reset(); // Formular nach Erfolg leeren
} else {
// Fehler vom Server (mit JSON-Fehlermeldung)
statusDiv.className = 'error';
statusDiv.innerHTML = `<strong>Fehler:</strong> ${result.message || 'Ein unbekannter Fehler ist aufgetreten.'}`;
if (!response.ok || !result.success) {
throw new Error(result.message || `Server-Fehler ${response.status}`);
}
} catch (error) {
// Netzwerkfehler oder abgefangener Server-Fehler
statusDiv.className = 'error';
statusDiv.innerHTML = `<strong>Fehler:</strong> ${error.message}`;
console.error('Upload Error:', error);
} finally {
// UI wieder freigeben
submitBtn.disabled = false;
submitBtn.innerText = 'Daten hochladen';
log(`Batch ${b + 1} erfolgreich abgeschlossen.`);
} catch (batchErr) {
log(`⚠️ FEHLER in Batch ${b + 1}: ${batchErr.message}. Überspringe und fahre fort...`);
console.error(`Batch ${b + 1} fehlgeschlagen:`, batchErr);
}
});
</script>
progressBar.value = b + 1;
}
progressText.textContent = "Upload-Prozess beendet!";
uploadBtn.disabled = false;
} catch (err) {
alert("Upload abgebrochen: " + err.message);
log("Fehler: " + err.message);
uploadBtn.disabled = false;
}
});
</script>
</body>
</html>