Seite 1 von 1

Überarbeitet : Kleines Script - Kollektionen anlegen über CSV

Verfasst: 17 Jun 2026, 23:58
von juratextex
Guten Abend

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> &nbsp;|&nbsp; Variante: <strong>$cnr</strong> &nbsp;|&nbsp; 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> &nbsp;|&nbsp; 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&ouml;scht (ID $existing_gid): <strong>$cnr</strong> &nbsp;|&nbsp; Hauptartikel: <strong>$main</strong>"]);
            } else {
                sse(['type' => 'info', 'msg' => "[TESTLAUF] W&uuml;rde Kollektion l&ouml;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) &nbsp;|&nbsp; Hauptartikel: <strong>$main</strong>"]);
            } else {
                sse(['type' => 'info', 'msg' => "[TESTLAUF] W&uuml;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> &nbsp;|&nbsp; Hauptartikel: <strong>$main</strong> &nbsp;|&nbsp; 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 &nbsp;&middot;&nbsp; combi_model &rarr; Variante &rarr; 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> &nbsp;&mdash;&nbsp; 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 &ndash; 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&ouml;nnen sp&auml;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 &uuml;berspringen &ndash; wenn Bilder fehlen</div>
                        <div class="opt-desc">Fehlt mindestens ein Bild, wird f&uuml;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&ouml;schen und neu anlegen</div>
                    <div class="opt-desc">
                        Wird f&uuml;r eine Varianten-Artikelnummer bereits eine Kollektion gefunden, werden
                        <strong>alle</strong> zugeh&ouml;rigen Eintr&auml;ge vollst&auml;ndig gel&ouml;scht
                        (Galerie, Bilder, Bild-Texte, Varianten-Zuordnung) und anschlie&szlig;end als neue
                        Kollektion neu angelegt.<br>
                        <strong style="color:var(--error);">Achtung: Dieser Vorgang kann nicht r&uuml;ckg&auml;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;">
                &#9888;&nbsp; Bestehende Kollektionen werden unwiderruflich gel&ouml;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> &nbsp;&middot;&nbsp; Trennzeichen: Semikolon &nbsp;&middot;&nbsp; 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&uuml;fungen laufen durch &ndash; 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>


Re: Kleines Script - Kollektionen anlegen über CSV

Verfasst: 18 Jun 2026, 13:57
von Kai Stejuhn
Warum machst Du das als "Standalone" Version und nicht als richtiges Modul?

Wenn Du daraus ein Modul machst, dann hast Du keine Probleme mit den Fremdzugriffen, da Du immer über den Shop im Adminbereich bist. Man muss dann auch nicht gleich wieder alles löschen. Vergisst man mal das Löschen, dann hast Du eine echte Sicherheitslücke.

Re: Kleines Script - Kollektionen anlegen über CSV

Verfasst: 18 Jun 2026, 20:50
von juratextex
Kai Stejuhn hat geschrieben: 18 Jun 2026, 13:57 Warum machst Du das als "Standalone" Version und nicht als richtiges Modul?

Wenn Du daraus ein Modul machst, dann hast Du keine Probleme mit den Fremdzugriffen, da Du immer über den Shop im Adminbereich bist. Man muss dann auch nicht gleich wieder alles löschen. Vergisst man mal das Löschen, dann hast Du eine echte Sicherheitslücke.
Guten Abend,
ich habe irgendwie das Gefühl alles was in Gambio läuft instabiler ist, als eine Standalone Script.
Deshalb ziehe ich diese Lösung einfach vor.

Re: Kleines Script - Kollektionen anlegen über CSV

Verfasst: 18 Jun 2026, 22:12
von Kai Stejuhn
Ich habe Standalone extra in Anführungszeichen gesetzt, weil es ja nicht eine echte Standalone Anwendung ist. Du bindest die "application_top.php"von Gambio ein, somit bist Du ja schon mehr oder weniger im Shop. Darum kannst Du ja auch die xtc_db- Funktionen von Gambio nutzen.

Also würde ich sagen Dein Gefühl täuscht Dich. Lass Dich nicht von Gefühlen lenken. Beim Programmieren ist Wissen einfach besser als Gefühl.