ein kleines Script (überarbeitet) um aus einer CSV Datei Kollektionen anzulegen.
Die Zuordnung erfolgt über die Artikelnummer der Kombination der Optionen.
Habe das Script mit Gambio 5.02 getestet und funktioniert - Bei Fehlern bitte melden
Aufbau CSV
combi_model;Bild1;Text1;Bild2;Text2;Bild3;Text3;Bild4;Text4;Bild5;Text5;Bild6;Text6
Das Script kann bis zu 6 Bildern anlegen.
Die Kollektionen haben den Namen der Kombiartikelnummer: combi_model
Das Script ins /admin Verzeichniss kopieren - Passwort lautet : Passwort (Kann in der php geändert werden, ganz oben.)
Im Verzeichniss : /images/product_images/original_images/OBilder
Bitte OBilder anlegen und dort die Optionsbilder alle hochladen, bevor das Script benutzt wird !!!!
Aus Sicherheitsgründen gleich nach dem benutzen, bitte löschen!!
Vorher ein Backup von der Datenbank anlegen !!
Wie immer Benutzung auf eigene Gefahr
Ich wünsche viel Spaß
Code: Alles auswählen
<?php
/**
* Gambio 5 – Varianten Kollektion Importer v0.5 - Beta
* Basis: combi_model → Galerie → Bilder (funktionierender Kern)
*
* Neu:
* – db_base per Formular einstellbar
* – Wahl: fehlende Bilder eintragen ODER Variante überspringen
* – Live-Log per Server-Sent Events (SSE)
* – CSV-Download fehlender Bilder
*/
require_once __DIR__ . '/includes/application_top.php';
// ============================================================
// CONFIG
// ============================================================
define('IMPORT_PASSWORD', 'Passwort'); //Hier Passwort ändern
define('FS_ROOT', DIR_FS_CATALOG . 'images/product_images/original_images/');
// ============================================================
// HELPERS
// ============================================================
function h(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function getMainModel(int $combi_id): string
{
$q = xtc_db_query("
SELECT p.products_model
FROM products_properties_combis ppc
JOIN products p ON p.products_id = ppc.products_id
WHERE ppc.products_properties_combis_id = " . (int)$combi_id . "
LIMIT 1
");
return xtc_db_num_rows($q) ? (string)xtc_db_fetch_array($q)['products_model'] : '–';
}
function sse(array $d): void
{
echo 'data: ' . json_encode($d, JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
}
// ============================================================
// AUTH
// ============================================================
// Session immer früh starten, damit SSE-GET-Requests die Auth aus der Session lesen können
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$pw_post = isset($_POST['pw']) && $_POST['pw'] === IMPORT_PASSWORD;
// Auth in Session persistieren, damit GET-Requests (SSE, CSV-Download) ebenfalls prüfen können
if ($pw_post) {
$_SESSION['auth_ok'] = true;
}
$pw_ok = $pw_post || !empty($_SESSION['auth_ok']);
// ============================================================
// SPRACHEN (wird nur nach Login geladen)
// ============================================================
$languages = [];
if ($pw_ok) {
$lr = xtc_db_query("SELECT languages_id, name FROM languages ORDER BY languages_id");
while ($l = xtc_db_fetch_array($lr)) {
$languages[(int)$l['languages_id']] = $l['name'];
}
}
// ============================================================
// CSV-DOWNLOAD fehlende Bilder
// ============================================================
if (isset($_GET['dl_missing']) && $pw_ok) {
$rows = $_SESSION['missing_imgs'] ?? [];
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="fehlende_bilder_' . date('Ymd_His') . '.csv"');
echo "\xEF\xBB\xBF"; // BOM für Excel
echo "Varianten-Artikelnummer;Hauptartikel;Fehlendes Bild;Erwarteter Pfad\n";
foreach ($rows as $r) {
echo h($r['cnr']) . ';' . h($r['main']) . ';' . h($r['img']) . ';' . h($r['path']) . "\n";
}
exit;
}
// ============================================================
// SSE-STREAM – Import-Logik
// ============================================================
if (isset($_GET['stream']) && $pw_ok) {
$p = $_SESSION['import_params'] ?? null;
if (!$p || !file_exists($p['csv_tmp'])) {
header('Content-Type: text/event-stream');
sse(['type' => 'fatal', 'msg' => 'Session oder CSV nicht gefunden. Bitte neu starten.']);
exit;
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
if (ob_get_level()) ob_end_flush();
$db_base = rtrim($p['db_base'], '/') . '/';
$skip_missing = (bool)$p['skip_missing'];
$delete_existing = (bool)$p['delete_existing'];
$dry_run = (bool)$p['dry_run'];
$sel_langs = $p['languages'];
$csv_tmp = $p['csv_tmp'];
// Zeilen zählen für Fortschritt
$total = 0;
$fh = fopen($csv_tmp, 'r');
while (!feof($fh)) { if (fgetcsv($fh, 0, ';') !== false) $total++; }
fclose($fh);
$total = max(1, $total - 1); // ohne Header
if (!$dry_run) xtc_db_query("START TRANSACTION");
$handle = fopen($csv_tmp, 'r');
$row = 0;
$processed = 0;
$cnt_ok = 0;
$cnt_skip = 0;
$missing_imgs = [];
while (($data = fgetcsv($handle, 0, ';')) !== false) {
$row++;
if ($row === 1) continue; // Header
$cnr = trim($data[0] ?? '');
if (!$cnr) {
sse(['type' => 'warn', 'msg' => "Zeile $row: Leere Artikelnummer – übersprungen."]);
$cnt_skip++;
continue;
}
$processed++;
sse(['type' => 'progress', 'pct' => min(100, (int)round($processed / $total * 100)),
'proc' => $processed, 'total' => $total]);
// ---- VARIANTE ----
$q = xtc_db_query("
SELECT ppc.products_properties_combis_id,
p.products_model AS main_model
FROM products_properties_combis ppc
LEFT JOIN products p ON p.products_id = ppc.products_id
WHERE ppc.combi_model = '" . xtc_db_input($cnr) . "'
LIMIT 1
");
if (!xtc_db_num_rows($q)) {
sse(['type' => 'error', 'msg' => "Variante nicht gefunden: <strong>$cnr</strong>"]);
$cnt_skip++;
continue;
}
$row_data = xtc_db_fetch_array($q);
$combi_id = (int)$row_data['products_properties_combis_id'];
$main = $row_data['main_model'] ?? '–';
// ---- BILDER VORAB PRÜFEN ----
$imgs = [];
$has_missing = false;
for ($i = 1; $i < count($data); $i += 2) {
$img = trim($data[$i] ?? '');
$text = trim($data[$i + 1] ?? '');
if (!$img) continue;
$path_sub = FS_ROOT . $db_base . $img;
$path_bare = FS_ROOT . $img;
if (file_exists($path_sub)) {
$db_path = $db_base . $img;
$ok = true;
} elseif (file_exists($path_bare)) {
$db_path = $img;
$ok = true;
} else {
$db_path = $db_base . $img;
$ok = false;
$has_missing = true;
$missing_imgs[] = ['cnr' => $cnr, 'main' => $main, 'img' => $img, 'path' => $path_sub];
sse(['type' => 'missing',
'msg' => "Bild fehlt: <strong>$img</strong> | Variante: <strong>$cnr</strong> | Hauptartikel: <strong>$main</strong>"]);
}
$imgs[] = compact('img', 'text', 'db_path', 'ok');
}
// Überspringen wenn gewünscht und Bilder fehlen
if ($skip_missing && $has_missing) {
sse(['type' => 'warn',
'msg' => "Übersprungen (fehlende Bilder) – Variante: <strong>$cnr</strong> | Hauptartikel: <strong>$main</strong>"]);
$cnt_skip++;
continue;
}
// ---- GALERIE: bestehende prüfen ----
$g = xtc_db_query("
SELECT product_image_list_id
FROM product_image_list
WHERE product_image_list_name = '" . xtc_db_input($cnr) . "'
LIMIT 1
");
$existing_gid = xtc_db_num_rows($g) ? (int)xtc_db_fetch_array($g)['product_image_list_id'] : 0;
// Bestehende Kollektion vollständig löschen wenn Option aktiv
if ($existing_gid > 0 && $delete_existing) {
if (!$dry_run) {
// 1. Bild-Texte löschen (referenzieren product_image_list_image)
$imgs_q = xtc_db_query("
SELECT product_image_list_image_id
FROM product_image_list_image
WHERE product_image_list_id = $existing_gid
");
while ($ir = xtc_db_fetch_array($imgs_q)) {
$iid_del = (int)$ir['product_image_list_image_id'];
xtc_db_query("DELETE FROM product_image_list_image_text WHERE product_image_list_image_id = $iid_del");
}
// 2. Bilder löschen
xtc_db_query("DELETE FROM product_image_list_image WHERE product_image_list_id = $existing_gid");
// 3. Combi-Zuordnung löschen
xtc_db_query("DELETE FROM product_image_list_combi WHERE product_image_list_id = $existing_gid");
// 4. Galerie selbst löschen
xtc_db_query("DELETE FROM product_image_list WHERE product_image_list_id = $existing_gid");
sse(['type' => 'warn', 'msg' => "Bestehende Kollektion gelöscht (ID $existing_gid): <strong>$cnr</strong> | Hauptartikel: <strong>$main</strong>"]);
} else {
sse(['type' => 'info', 'msg' => "[TESTLAUF] Würde Kollektion löschen (ID $existing_gid): <strong>$cnr</strong>"]);
}
$existing_gid = 0; // als neu behandeln
} elseif ($existing_gid > 0 && !$delete_existing) {
$gid = $existing_gid;
sse(['type' => 'info', 'msg' => "Bestehende Galerie wird beibehalten: <strong>$cnr</strong> (ID $gid)"]);
}
// Neue Galerie anlegen (entweder gar nicht vorhanden oder gerade gelöscht)
if ($existing_gid === 0) {
if (!$dry_run) {
xtc_db_perform('product_image_list', ['product_image_list_name' => $cnr]);
$gid = (int)xtc_db_insert_id();
if ($gid <= 0) {
sse(['type' => 'error', 'msg' => "Galerie-INSERT fehlgeschlagen: <strong>$cnr</strong>"]);
$cnt_skip++;
continue;
}
sse(['type' => 'info', 'msg' => "Neue Galerie angelegt: <strong>$cnr</strong> (ID $gid) | Hauptartikel: <strong>$main</strong>"]);
} else {
sse(['type' => 'info', 'msg' => "[TESTLAUF] Würde Galerie anlegen: <strong>$cnr</strong>"]);
$gid = 0;
}
}
// ---- ALT-LINK LÖSCHEN + COMBI-LINK ----
if (!$dry_run && $gid > 0) {
xtc_db_query("DELETE FROM product_image_list_combi
WHERE products_properties_combis_id = $combi_id");
xtc_db_perform('product_image_list_combi', [
'products_properties_combis_id' => $combi_id,
'product_image_list_id' => $gid,
]);
sse(['type' => 'info', 'msg' => "Variante <strong>$cnr</strong> mit Galerie $gid verknüpft"]);
}
// ---- BILDER ----
$sort = 1;
$imgs_ok = 0;
foreach ($imgs as $entry) {
if (!$dry_run && $gid > 0) {
xtc_db_perform('product_image_list_image', [
'product_image_list_id' => $gid,
'product_image_list_image_local_path' => $entry['db_path'],
'product_image_list_image_sort_order' => $sort,
]);
$iid = (int)xtc_db_insert_id();
$base_text = $entry['text'] ?: pathinfo($entry['img'], PATHINFO_FILENAME);
foreach ($sel_langs as $lid) {
foreach (['title', 'alt_title'] as $ttype) {
xtc_db_perform('product_image_list_image_text', [
'product_image_list_image_id' => $iid,
'language_id' => (int)$lid,
'product_image_list_image_text_type' => $ttype,
'product_image_list_image_text_value'=> $base_text,
]);
}
}
}
if ($entry['ok']) $imgs_ok++;
$sort++;
}
$imgs_miss = count(array_filter($imgs, fn($e) => !$e['ok']));
$suffix = $imgs_miss ? ", <span style='color:#f59e0b'>$imgs_miss fehlend</span>" : '';
sse(['type' => 'success',
'msg' => "OK: <strong>$cnr</strong> | Hauptartikel: <strong>$main</strong> | Bilder: $imgs_ok eingetragen$suffix"]);
$cnt_ok++;
}
fclose($handle);
if (!$dry_run) xtc_db_query("COMMIT");
$_SESSION['missing_imgs'] = $missing_imgs;
sse([
'type' => 'done',
'processed' => $processed,
'ok' => $cnt_ok,
'skipped' => $cnt_skip,
'missing' => count($missing_imgs),
'dry_run' => $dry_run,
]);
exit;
}
// ============================================================
// FORM-POST – CSV in Session speichern, dann SSE starten
// ============================================================
$stream_ready = false;
if ($pw_ok && !empty($_FILES['csv_file']['tmp_name'])) {
$tmp = tempnam(sys_get_temp_dir(), 'g5imp_');
move_uploaded_file($_FILES['csv_file']['tmp_name'], $tmp);
$db_base_raw = trim($_POST['db_base'] ?? 'OBilder');
if ($db_base_raw === '') $db_base_raw = 'OBilder';
$_SESSION['import_params'] = [
'db_base' => $db_base_raw,
'skip_missing' => ($_POST['missing_mode'] ?? '0') === '1',
'delete_existing' => isset($_POST['delete_existing']),
'dry_run' => isset($_POST['dry_run']),
'languages' => array_map('intval', $_POST['languages'] ?? array_keys($languages)),
'csv_tmp' => $tmp,
];
$stream_ready = true;
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gambio Kollektion Importer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f1f5f9;
--surface: #ffffff;
--border: #e2e8f0;
--brand: #2563eb;
--brand-dk: #1d4ed8;
--brand-lt: #eff6ff;
--success: #16a34a;
--error: #dc2626;
--warn: #d97706;
--info: #0891b2;
--text: #1e293b;
--muted: #64748b;
--radius: 10px;
--shadow: 0 2px 8px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.05);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
padding: 32px 16px 64px;
min-height: 100vh;
font-size: 14px;
line-height: 1.6;
}
.wrap { max-width: 820px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; }
/* Header */
.hd {
background: var(--brand);
color: #fff;
border-radius: var(--radius);
padding: 22px 26px;
box-shadow: 0 4px 16px rgba(37,99,235,.3);
}
.hd h1 { font-size: 19px; font-weight: 700; letter-spacing: -.2px; }
.hd p { font-size: 12px; opacity: .75; margin-top: 3px; }
/* Card */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 22px 24px;
box-shadow: var(--shadow);
}
.card-title {
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .07em;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 7px;
}
.card-title svg { width: 14px; height: 14px; stroke: var(--brand); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
/* Form */
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field > span { font-size: 12px; font-weight: 600; color: var(--text); }
.field small { font-size: 11px; color: var(--muted); }
input[type="text"],
input[type="password"] {
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 7px;
font-size: 13px;
background: #f8fafc;
color: var(--text);
transition: border .15s, box-shadow .15s;
outline: none;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(37,99,235,.12);
background: #fff;
}
/* File drop */
.file-drop {
position: relative;
border: 2px dashed var(--border);
border-radius: 8px;
padding: 20px;
text-align: center;
background: #f8fafc;
transition: border-color .15s, background .15s;
cursor: pointer;
}
.file-drop:hover, .file-drop.over { border-color: var(--brand); background: var(--brand-lt); }
.file-drop input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
.file-drop svg { width: 28px; height: 28px; stroke: var(--muted); fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; margin-bottom: 6px; }
.file-drop p { font-size: 13px; color: var(--muted); }
.file-drop .chosen { font-size: 12px; font-weight: 600; color: var(--brand); margin-top: 4px; }
/* Radio option cards */
.opt-group { display: flex; flex-direction: column; gap: 9px; }
.opt-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 13px 14px;
border: 2px solid var(--border);
border-radius: 8px;
cursor: pointer;
background: #f8fafc;
transition: border-color .15s, background .15s;
}
.opt-item:has(input:checked) { border-color: var(--brand); background: var(--brand-lt); }
.opt-item input[type="radio"] { margin-top: 3px; accent-color: var(--brand); flex-shrink: 0; }
.opt-title { font-size: 13px; font-weight: 600; line-height: 1.3; }
.opt-desc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.5; }
/* Lang grid */
.lang-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.lang-chip {
display: flex; align-items: center; gap: 8px;
padding: 9px 11px;
background: #f8fafc; border: 1px solid var(--border); border-radius: 7px;
font-size: 13px; cursor: pointer; transition: all .15s;
}
.lang-chip:has(input:checked) { background: var(--brand-lt); border-color: var(--brand); color: var(--brand-dk); font-weight: 600; }
.lang-chip input { accent-color: var(--brand); }
/* Dry-run row */
.dryrun-row {
display: flex; align-items: center; gap: 10px;
padding: 11px 14px;
background: #fff8f0; border: 1px solid #fed7aa; border-radius: 8px;
font-size: 13px; cursor: pointer;
}
.dryrun-row input { accent-color: var(--warn); width: 15px; height: 15px; }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 20px; border-radius: 8px;
font-size: 13px; font-weight: 600; cursor: pointer; border: none;
transition: background .15s, transform .1s;
text-decoration: none;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-dk); }
.btn-primary:disabled { background: #94a3b8; cursor: not-allowed; transform: none; }
.btn-outline { background: transparent; color: var(--brand); border: 2px solid var(--brand); }
.btn-outline:hover { background: var(--brand-lt); }
.btn-success { background: var(--success); color: #fff; }
.btn-success:hover { background: #15803d; }
/* Spinner */
.spin { display: none; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.35); border-top-color: #fff; border-radius: 50%; animation: rot .6s linear infinite; }
@keyframes rot { to { transform: rotate(360deg); } }
/* Progress */
.prog-wrap { display: flex; flex-direction: column; gap: 8px; }
.prog-bg { background: #e2e8f0; border-radius: 99px; height: 8px; overflow: hidden; }
.prog-fill { height: 100%; width: 0%; background: var(--brand); border-radius: 99px; transition: width .25s ease; }
.prog-stats { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: var(--muted); }
.prog-stats b { color: var(--text); }
/* Live log */
.log-ctrl { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
.lf-btn {
padding: 3px 11px; border-radius: 99px; font-size: 11px; font-weight: 700;
border: 1px solid var(--border); background: #f8fafc; cursor: pointer; transition: all .12s;
}
.lf-btn.on { background: var(--brand); color: #fff; border-color: var(--brand); }
.log-count { margin-left: auto; font-size: 11px; color: var(--muted); }
#log-box {
background: #0f172a; border-radius: 8px; padding: 12px 14px;
height: 320px; overflow-y: auto;
font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.7;
}
.ll { display: flex; gap: 7px; align-items: baseline; }
.ll.hide { display: none; }
.lb {
flex-shrink: 0; font-size: 9px; font-weight: 800;
padding: 1px 5px; border-radius: 3px; letter-spacing: .4px;
}
.lb.ERROR { background: #7f1d1d; color: #fca5a5; }
.lb.WARN { background: #78350f; color: #fcd34d; }
.lb.SUCCESS { background: #14532d; color: #86efac; }
.lb.INFO { background: #164e63; color: #67e8f9; }
.lb.MISSING { background: #4c1d95; color: #c4b5fd; }
.lm { color: #94a3b8; }
.lm strong { color: #f1f5f9; }
.lm span { }
/* Stat grid */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; margin-bottom: 14px; }
.stat {
background: #f8fafc; border: 1px solid var(--border); border-radius: 8px;
padding: 14px; text-align: center;
}
.stat .val { font-size: 30px; font-weight: 800; line-height: 1; }
.stat .lbl { font-size: 11px; color: var(--muted); margin-top: 3px; }
.stat.ok .val { color: var(--success); }
.stat.err .val { color: var(--error); }
.stat.warn .val { color: var(--warn); }
.stat.blue .val { color: var(--brand); }
/* Result banner */
.banner {
padding: 11px 14px; border-radius: 8px; font-size: 13px; font-weight: 600;
display: flex; align-items: center; gap: 8px;
}
.banner.ok { background: #dcfce7; color: #166534; }
.banner.err { background: #fee2e2; color: #991b1b; }
.banner.dry { background: #dbeafe; color: #1e40af; }
/* Auth ok pill */
.auth-ok { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--success); }
.auth-ok svg { width: 14px; height: 14px; stroke: var(--success); fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
</style>
</head>
<body>
<div class="wrap">
<!-- Header -->
<div class="hd">
<h1>Gambio Kollektion Importer</h1>
<p>Gambio 5 · combi_model → Variante → Bildergalerie</p>
</div>
<?php if (!$pw_ok): ?>
<!-- LOGIN -->
<div class="card" style="max-width:380px;">
<div class="card-title">
<svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Anmeldung
</div>
<form method="post" enctype="multipart/form-data">
<div class="field">
<span>Passwort</span>
<input type="password" name="pw" placeholder="Passwort eingeben" required autofocus>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:6px;">Anmelden</button>
</form>
</div>
<?php elseif ($stream_ready): ?>
<!-- IMPORT LÄUFT – Live-View -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Import-Fortschritt
</div>
<!-- Progressbar -->
<div class="prog-wrap">
<div class="prog-bg"><div class="prog-fill" id="pbar"></div></div>
<div class="prog-stats">
<span><b id="p-pct">0</b>%</span>
<span>Zeilen: <b id="p-proc">0</b> / <b id="p-tot">–</b></span>
<span style="color:var(--success)">OK: <b id="p-ok">0</b></span>
<span style="color:var(--warn)">Übersprungen: <b id="p-skip">0</b></span>
<span style="color:#7c3aed">Bilder fehlen: <b id="p-miss">0</b></span>
</div>
</div>
<!-- Log -->
<div style="margin-top:16px;">
<div class="log-ctrl">
<button class="lf-btn on" onclick="fLog('all',this)">Alle</button>
<button class="lf-btn" onclick="fLog('ERROR',this)">Fehler</button>
<button class="lf-btn" onclick="fLog('WARN',this)">Warnungen</button>
<button class="lf-btn" onclick="fLog('SUCCESS',this)">Erfolg</button>
<button class="lf-btn" onclick="fLog('MISSING',this)">Bilder fehlen</button>
<button class="lf-btn" onclick="fLog('INFO',this)">Info</button>
<span class="log-count">Einträge: <b id="lc">0</b></span>
</div>
<div id="log-box"></div>
</div>
</div>
<!-- Ergebnis (eingeblendet nach done) -->
<div class="card" id="result" style="display:none;">
<div class="card-title">
<svg viewBox="0 0 24 24"><path d="M9 12l2 2 4-4M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
Ergebnis
</div>
<div class="stat-grid">
<div class="stat blue"><div class="val" id="r-proc">–</div><div class="lbl">Verarbeitet</div></div>
<div class="stat ok"> <div class="val" id="r-ok">–</div> <div class="lbl">Erfolgreich</div></div>
<div class="stat warn"><div class="val" id="r-skip">–</div><div class="lbl">Übersprungen</div></div>
<div class="stat err"> <div class="val" id="r-miss">–</div><div class="lbl">Bilder fehlen</div></div>
</div>
<div id="banner" class="banner"></div>
<div id="dl-wrap" style="display:none; margin-top:12px;">
<a href="?dl_missing=1" class="btn btn-outline">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Fehlende Bilder als CSV herunterladen
</a>
</div>
<div style="margin-top:14px;">
<a href="?" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.57"/></svg>
Neuer Import
</a>
</div>
</div>
<script>
(function(){
var filter = 'all', lc = 0, cOk = 0, cSkip = 0, cMiss = 0, autoScroll = true;
var box = document.getElementById('log-box');
box.addEventListener('scroll', function(){
autoScroll = box.scrollHeight - box.scrollTop - box.clientHeight < 50;
});
function addLine(type, html){
lc++;
document.getElementById('lc').textContent = lc;
var d = document.createElement('div');
d.className = 'll' + (filter !== 'all' && filter !== type ? ' hide' : '');
d.dataset.t = type;
d.innerHTML = '<span class="lb '+type+'">'+type+'</span><span class="lm">'+html+'</span>';
box.appendChild(d);
if(autoScroll) box.scrollTop = box.scrollHeight;
}
window.fLog = function(f, btn){
filter = f;
document.querySelectorAll('.lf-btn').forEach(function(b){ b.classList.remove('on'); });
btn.classList.add('on');
document.querySelectorAll('.ll').forEach(function(l){
l.classList.toggle('hide', f !== 'all' && l.dataset.t !== f);
});
};
var es = new EventSource('?stream=1');
es.onmessage = function(e){
var d = JSON.parse(e.data);
if(d.type === 'progress'){
document.getElementById('pbar').style.width = d.pct + '%';
document.getElementById('p-pct').textContent = d.pct;
document.getElementById('p-proc').textContent= d.proc;
document.getElementById('p-tot').textContent = d.total;
return;
}
if(d.type === 'success'){ cOk++; document.getElementById('p-ok').textContent = cOk; addLine('SUCCESS', d.msg); return; }
if(d.type === 'error') { document.getElementById('p-skip').textContent = ++cSkip; addLine('ERROR', d.msg); return; }
if(d.type === 'warn') { document.getElementById('p-skip').textContent = ++cSkip; addLine('WARN', d.msg); return; }
if(d.type === 'missing'){ cMiss++; document.getElementById('p-miss').textContent = cMiss; addLine('MISSING', d.msg); return; }
if(d.type === 'info') { addLine('INFO', d.msg); return; }
if(d.type === 'fatal') { addLine('ERROR', d.msg); es.close(); return; }
if(d.type === 'done'){
es.close();
document.getElementById('pbar').style.width = '100%';
document.getElementById('p-pct').textContent = 100;
document.getElementById('r-proc').textContent = d.processed;
document.getElementById('r-ok').textContent = d.ok;
document.getElementById('r-skip').textContent = d.skipped;
document.getElementById('r-miss').textContent = d.missing;
var b = document.getElementById('banner');
if(d.dry_run){
b.className = 'banner dry'; b.textContent = 'Testlauf abgeschlossen – keine Daten gespeichert.';
} else if(cSkip === 0 && d.missing === 0){
b.className = 'banner ok'; b.textContent = 'Import erfolgreich abgeschlossen.';
} else {
b.className = 'banner err'; b.textContent = 'Import abgeschlossen – bitte Log prüfen.';
}
document.getElementById('result').style.display = 'block';
if(d.missing > 0) document.getElementById('dl-wrap').style.display = 'block';
}
};
es.onerror = function(){ addLine('ERROR','Verbindung getrennt.'); es.close(); };
})();
</script>
<?php else: ?>
<!-- FORMULAR -->
<form id="frm" method="post" enctype="multipart/form-data">
<input type="hidden" name="pw" value="<?= h($_POST['pw'] ?? '') ?>">
<!-- Auth OK -->
<div class="card" style="padding:14px 20px; border-left:3px solid var(--success);">
<div class="auth-ok">
<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
Angemeldet
</div>
</div>
<!-- Galerie-Verzeichnis -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
Galerie-Verzeichnis (Bildquelle)
</div>
<div class="field">
<span>Unterverzeichnis</span>
<input type="text" name="db_base" value="OBilder" placeholder="z.B. OBilder oder Marke/Saison">
<small>Relativ zu <code>images/product_images/original_images/</code> — Bilder werden dort gesucht und mit diesem Präfix in der Datenbank gespeichert.</small>
</div>
</div>
<!-- Verhalten bei fehlenden Bildern -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
Verhalten bei fehlenden Bildern
</div>
<div class="opt-group">
<label class="opt-item">
<input type="radio" name="missing_mode" value="0" checked>
<div>
<div class="opt-title">Eintragen – auch wenn Bilder fehlen</div>
<div class="opt-desc">Kollektion und Bildreferenzen werden angelegt, auch wenn das Bild im Verzeichnis noch nicht vorhanden ist. Bilder können später manuell hochgeladen werden. Fehlende Bilder werden im Log und als CSV-Download aufgelistet.</div>
</div>
</label>
<label class="opt-item">
<input type="radio" name="missing_mode" value="1">
<div>
<div class="opt-title">Variante überspringen – wenn Bilder fehlen</div>
<div class="opt-desc">Fehlt mindestens ein Bild, wird für diese Variante <strong>kein</strong> Datenbankeintrag angelegt. Fehlende Bilder werden im Log aufgelistet und als CSV angeboten.</div>
</div>
</label>
</div>
</div>
<!-- Bestehende Kollektionen löschen -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
Bestehende Kollektionen
</div>
<label class="opt-item" style="border-color: var(--error); background: #fff5f5;" id="del-opt">
<input type="checkbox" name="delete_existing" id="del-chk" style="accent-color:var(--error); margin-top:3px; flex-shrink:0;">
<div>
<div class="opt-title" style="color:var(--error)">Bestehende Kollektion löschen und neu anlegen</div>
<div class="opt-desc">
Wird für eine Varianten-Artikelnummer bereits eine Kollektion gefunden, werden
<strong>alle</strong> zugehörigen Einträge vollständig gelöscht
(Galerie, Bilder, Bild-Texte, Varianten-Zuordnung) und anschließend als neue
Kollektion neu angelegt.<br>
<strong style="color:var(--error);">Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden.</strong>
</div>
</div>
</label>
<div id="del-warn" style="display:none; margin-top:10px; padding:10px 14px; background:#fee2e2; border-radius:7px; font-size:12px; color:#991b1b; font-weight:600;">
⚠ Bestehende Kollektionen werden unwiderruflich gelöscht! Bitte vorher eine Datenbanksicherung erstellen.
</div>
</div>
<!-- Sprachen -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><path d="M5 8l6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6"/></svg>
Sprachen (Title / Alt-Text)
</div>
<div class="lang-grid">
<?php foreach ($languages as $id => $name): ?>
<label class="lang-chip">
<input type="checkbox" name="languages[]" value="<?= $id ?>" checked>
<?= h($name) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<!-- CSV Upload -->
<div class="card">
<div class="card-title">
<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
CSV-Datei
</div>
<div class="file-drop" id="fd">
<input type="file" name="csv_file" accept=".csv,.txt" required id="csv-input">
<svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<p>CSV-Datei hierher ziehen oder klicken</p>
<div class="chosen" id="fn"></div>
</div>
<small style="display:block;margin-top:7px;">Format: <code>Varianten-Artikelnummer;Bild1.jpg;Text1;Bild2.jpg;Text2;...</code> · Trennzeichen: Semikolon · Erste Zeile = Header</small>
</div>
<!-- Dry-run -->
<div class="card" style="padding:14px 18px;">
<label class="dryrun-row">
<input type="checkbox" name="dry_run">
<div>
<div style="font-weight:600;">Testlauf (Dry Run)</div>
<div style="font-size:11px;color:var(--muted);margin-top:1px;">Alle Prüfungen laufen durch – es wird <strong>nichts</strong> in die Datenbank geschrieben.</div>
</div>
</label>
</div>
<!-- Submit -->
<div style="display:flex; justify-content:flex-end;">
<button type="submit" class="btn btn-primary" id="sbtn">
<span class="spin" id="sp"></span>
<svg id="sic" viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Import starten
</button>
</div>
</form>
<script>
// File-drop label
document.getElementById('csv-input').addEventListener('change', function(){
document.getElementById('fn').textContent = this.files[0] ? this.files[0].name : '';
});
var fd = document.getElementById('fd');
fd.addEventListener('dragover', function(e){ e.preventDefault(); fd.classList.add('over'); });
fd.addEventListener('dragleave', function(){ fd.classList.remove('over'); });
fd.addEventListener('drop', function(){ fd.classList.remove('over'); });
// Löschen-Checkbox Warnbox
var delChk = document.getElementById('del-chk');
var delWarn = document.getElementById('del-warn');
var delOpt = document.getElementById('del-opt');
delChk.addEventListener('change', function(){
delWarn.style.display = this.checked ? 'block' : 'none';
delOpt.style.borderColor = this.checked ? 'var(--error)' : 'var(--border)';
delOpt.style.background = this.checked ? '#fee2e2' : '#f8fafc';
});
// Submit spinner
document.getElementById('frm').addEventListener('submit', function(){
var btn = document.getElementById('sbtn');
btn.disabled = true;
document.getElementById('sp').style.display = 'inline-block';
document.getElementById('sic').style.display = 'none';
});
</script>
<?php endif; ?>
</div><!-- /wrap -->
</body>
</html>