diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json
index a773bf70..f187889d 100644
--- a/.vscode/ftp-kr.sync.cache.json
+++ b/.vscode/ftp-kr.sync.cache.json
@@ -98,13 +98,13 @@
},
"google-merchant_id-1.xml": {
"type": "-",
- "size": 18781656,
+ "size": 18784618,
"lmtime": 0,
"modified": true
},
".htaccess": {
"type": "-",
- "size": 87138,
+ "size": 87360,
"lmtime": 0,
"modified": true
},
diff --git a/export-csv.php b/export-csv.php
new file mode 100644
index 00000000..ed6ff1e8
--- /dev/null
+++ b/export-csv.php
@@ -0,0 +1,398 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, // <— włącz buffer
+]);
+
+// Otwórz CSV
+$fh = fopen($outFile, "w");
+if (!$fh) die("Nie mogę utworzyć pliku CSV: {$outFile}\n");
+
+// (Opcjonalnie) BOM dla Excela na Windows
+fwrite($fh, "\xEF\xBB\xBF");
+
+// Nagłówek
+$header = [
+ 'id_product',
+ 'id_product_attribute',
+ 'product_name',
+ 'manufacturer',
+ 'default_category',
+ 'active',
+ 'price_tax_excl',
+ 'quantity',
+ 'reference',
+ 'ean13',
+ 'weight',
+ 'comb_reference',
+ 'comb_ean13',
+ 'comb_weight',
+ 'comb_quantity',
+ 'attributes', // "Grupa: Wartość | Grupa2: Wartość2"
+ 'features' // "Cecha: Wartość | Cecha2: Wartość2"
+];
+fputcsv($fh, $header, $delimiter);
+
+// ===== POMOCNICZE FUNKCJE =====
+function getProductFeatures(PDO $pdo, $prefix, $idLang, $idProduct): array
+{
+ // Fallback: weź tłumaczenie w :id_lang, a jeśli go nie ma – w minimalnym (dowolnym) id_lang
+ $sql = "
+ SELECT
+ COALESCE(fl.name, fl_any.name, '') AS feature_name,
+ COALESCE(fvl.value, fvl_any.value, '') AS feature_value
+ FROM {$prefix}feature_product fp
+ /* nazwa cechy w żądanym języku */
+ LEFT JOIN {$prefix}feature_lang fl
+ ON fl.id_feature = fp.id_feature
+ AND fl.id_lang = :id_lang
+ /* wybór jakiegokolwiek dostępnego języka dla cechy */
+ LEFT JOIN (
+ SELECT id_feature, MIN(id_lang) AS any_lang
+ FROM {$prefix}feature_lang
+ GROUP BY id_feature
+ ) fl_pick
+ ON fl_pick.id_feature = fp.id_feature
+ LEFT JOIN {$prefix}feature_lang fl_any
+ ON fl_any.id_feature = fp.id_feature
+ AND fl_any.id_lang = fl_pick.any_lang
+
+ /* wartość cechy */
+ JOIN {$prefix}feature_value fv
+ ON fv.id_feature_value = fp.id_feature_value
+
+ /* wartość w żądanym języku */
+ LEFT JOIN {$prefix}feature_value_lang fvl
+ ON fvl.id_feature_value = fv.id_feature_value
+ AND fvl.id_lang = :id_lang
+ /* fallback: jakikolwiek język dla wartości */
+ LEFT JOIN (
+ SELECT id_feature_value, MIN(id_lang) AS any_lang
+ FROM {$prefix}feature_value_lang
+ GROUP BY id_feature_value
+ ) fvl_pick
+ ON fvl_pick.id_feature_value = fv.id_feature_value
+ LEFT JOIN {$prefix}feature_value_lang fvl_any
+ ON fvl_any.id_feature_value = fv.id_feature_value
+ AND fvl_any.id_lang = fvl_pick.any_lang
+
+ WHERE fp.id_product = :id_product
+ ORDER BY feature_name ASC
+ ";
+
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([
+ ':id_lang' => (int)$idLang,
+ ':id_product' => (int)$idProduct,
+ ]);
+
+ $out = [];
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
+ {
+ $fname = trim((string)($row['feature_name'] ?? ''));
+ $fval = trim((string)($row['feature_value'] ?? ''));
+ if ($fname === '' && $fval === '') continue;
+ if ($fname === '') $fname = 'Cecha';
+ $out[$fname] = $fval;
+ }
+ return $out;
+}
+
+
+function getProductCombinations(PDO $pdo, $prefix, $idLang, $idShop, $idProduct): array
+{
+ $sqlComb = "
+ SELECT pa.id_product_attribute,
+ pa.reference AS comb_reference,
+ pa.ean13 AS comb_ean13,
+ pa.weight AS comb_weight,
+ COALESCE(sa.quantity, 0) AS comb_quantity
+ FROM {$prefix}product_attribute pa
+ LEFT JOIN {$prefix}stock_available sa
+ ON sa.id_product = pa.id_product
+ AND sa.id_product_attribute = pa.id_product_attribute
+ AND (sa.id_shop = :id_shop OR sa.id_shop IS NULL)
+ WHERE pa.id_product = :id_product
+ ORDER BY pa.id_product_attribute ASC
+ ";
+ $st = $pdo->prepare($sqlComb);
+ $st->execute([':id_product' => (int)$idProduct, ':id_shop' => (int)$idShop]);
+ $combinations = $st->fetchAll(PDO::FETCH_ASSOC);
+ if (!$combinations) return [];
+
+ $ids = array_column($combinations, 'id_product_attribute');
+ $in = implode(',', array_map('intval', $ids));
+
+ $sqlAttrs = "
+ SELECT pac.id_product_attribute,
+ agl.name AS group_name,
+ al.name AS attr_name
+ FROM {$prefix}product_attribute_combination pac
+ JOIN {$prefix}attribute a
+ ON a.id_attribute = pac.id_attribute
+ JOIN {$prefix}attribute_lang al
+ ON al.id_attribute = a.id_attribute AND al.id_lang = :id_lang
+ JOIN {$prefix}attribute_group_lang agl
+ ON agl.id_attribute_group = a.id_attribute_group AND agl.id_lang = :id_lang
+ WHERE pac.id_product_attribute IN ($in)
+ ORDER BY agl.name, al.name
+ ";
+ $st2 = $pdo->prepare($sqlAttrs);
+ $st2->execute([':id_lang' => (int)$idLang]);
+
+ $attrMap = [];
+ while ($r = $st2->fetch(PDO::FETCH_ASSOC))
+ {
+ $ipa = (int)$r['id_product_attribute'];
+ $pair = trim($r['group_name']) . ": " . trim($r['attr_name']);
+ $attrMap[$ipa][] = $pair;
+ }
+
+ foreach ($combinations as &$c)
+ {
+ $ipa = (int)$c['id_product_attribute'];
+ $pairs = $attrMap[$ipa] ?? [];
+ $c['attributes_str'] = implode(' | ', $pairs);
+ }
+ return $combinations;
+}
+
+// ===== FUNKCJA PISZĄCA WIERSZE DO CSV =====
+function writeProductRowsToCsv($fh, $prod, $featuresStr, $combinations, $delimiter)
+{
+ if ($combinations)
+ {
+ foreach ($combinations as $c)
+ {
+ $line = [
+ $prod['id_product'],
+ $c['id_product_attribute'],
+
+ $prod['product_name'],
+ $prod['manufacturer'],
+ $prod['default_category'],
+ (int)$prod['active'],
+ $prod['price_tax_excl'],
+ $prod['quantity'],
+ $prod['reference'],
+ $prod['ean13'],
+ $prod['weight'],
+
+ $c['comb_reference'],
+ $c['comb_ean13'],
+ $c['comb_weight'],
+ $c['comb_quantity'],
+ $c['attributes_str'],
+ $featuresStr,
+ ];
+ fputcsv($fh, $line, $delimiter);
+ }
+ }
+ else
+ {
+ $line = [
+ $prod['id_product'],
+ 0,
+
+ $prod['product_name'],
+ $prod['manufacturer'],
+ $prod['default_category'],
+ (int)$prod['active'],
+ $prod['price_tax_excl'],
+ $prod['quantity'],
+ $prod['reference'],
+ $prod['ean13'],
+ $prod['weight'],
+
+ '',
+ '',
+ '',
+ '',
+ '', // attributes
+ $featuresStr,
+ ];
+ fputcsv($fh, $line, $delimiter);
+ }
+}
+
+// ===== ZAPYTANIE BAZOWE (wspólne) =====
+$baseSelect = "
+ SELECT
+ p.id_product,
+ COALESCE(pl.name, CONCAT('Produkt #', p.id_product)) AS product_name,
+ p.reference,
+ p.ean13,
+ p.weight,
+ ps.price AS price_tax_excl,
+ ps.active,
+ COALESCE(sa.quantity, 0) AS quantity,
+ m.name AS manufacturer,
+ cl.name AS default_category
+ FROM {$prefix}product p
+ /* KLUCZOWA ZMIANA: LEFT JOIN + warunek po id_shop */
+ LEFT JOIN {$prefix}product_lang pl
+ ON pl.id_product = p.id_product
+ AND pl.id_lang = :id_lang
+ AND pl.id_shop = :id_shop
+ JOIN {$prefix}product_shop ps
+ ON ps.id_product = p.id_product
+ AND ps.id_shop = :id_shop
+ LEFT JOIN {$prefix}stock_available sa
+ ON sa.id_product = p.id_product
+ AND sa.id_product_attribute = 0
+ AND (sa.id_shop = :id_shop OR sa.id_shop IS NULL)
+ LEFT JOIN {$prefix}manufacturer m
+ ON m.id_manufacturer = p.id_manufacturer
+ LEFT JOIN {$prefix}category_lang cl
+ ON cl.id_category = p.id_category_default
+ AND cl.id_lang = :id_lang
+";
+
+// ===== TRYB TESTOWY: tylko jeden produkt po id_product lub EAN =====
+if ($testProductId || $testEan !== "")
+{
+ if ($testEan !== "" && !$testProductId)
+ {
+ $stmtId = $pdo->prepare("SELECT id_product FROM {$prefix}product WHERE ean13 = :ean LIMIT 1");
+ $stmtId->execute([':ean' => $testEan]);
+ $foundId = (int)$stmtId->fetchColumn();
+ if ($foundId) $testProductId = $foundId;
+ }
+
+ if (!$testProductId)
+ {
+ fclose($fh);
+ die("Nie znaleziono produktu dla podanego EAN.\n");
+ }
+
+ echo "Tryb testowy – eksport tylko produktu id_product={$testProductId}" . ($testEan ? " (EAN={$testEan})" : "") . "\n";
+
+ $sqlOne = $baseSelect . " WHERE p.id_product = :pid LIMIT 1";
+ $stmt = $pdo->prepare($sqlOne);
+ $stmt->bindValue(':id_lang', (int)$idLang, PDO::PARAM_INT);
+ $stmt->bindValue(':id_shop', (int)$idShop, PDO::PARAM_INT);
+ $stmt->bindValue(':pid', (int)$testProductId, PDO::PARAM_INT);
+ $stmt->execute();
+
+ $prod = $stmt->fetch(PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
+ if ($prod)
+ {
+ $featuresMap = getProductFeatures($pdo, $prefix, $idLang, (int)$prod['id_product']);
+ $featuresStr = '';
+ if ($featuresMap)
+ {
+ $pairs = [];
+ foreach ($featuresMap as $k => $v) $pairs[] = trim($k) . ': ' . trim($v);
+ $featuresStr = implode(' | ', $pairs);
+ }
+ $combinations = getProductCombinations($pdo, $prefix, $idLang, $idShop, (int)$prod['id_product']);
+ writeProductRowsToCsv($fh, $prod, $featuresStr, $combinations, $delimiter);
+ echo "✅ Zapisano rekord(y) produktu testowego do CSV.\n";
+ }
+ else
+ {
+ echo "Brak produktu o id_product={$testProductId}.\n";
+ }
+
+ fclose($fh);
+ echo "✅ Zapisano CSV: {$outFile}\n";
+ exit;
+}
+
+// ===== PEŁNY EKSPORT (paginacja) =====
+$sqlCount = "SELECT COUNT(*) FROM {$prefix}product";
+$total = (int)$pdo->query($sqlCount)->fetchColumn();
+echo "Znaleziono produktów: {$total}\n";
+
+$limit = 1000;
+$offset = 0;
+
+while (true)
+{
+ $sqlProducts = $baseSelect . " ORDER BY p.id_product ASC LIMIT :limit OFFSET :offset";
+ $stmt = $pdo->prepare($sqlProducts);
+ $stmt->bindValue(':id_lang', (int)$idLang, PDO::PARAM_INT);
+ $stmt->bindValue(':id_shop', (int)$idShop, PDO::PARAM_INT);
+ $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT);
+ $stmt->bindValue(':offset', (int)$offset, PDO::PARAM_INT);
+ $stmt->execute();
+
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
+ if (!$rows) break;
+
+ foreach ($rows as $prod)
+ {
+ $idProduct = (int)$prod['id_product'];
+
+ $featuresMap = getProductFeatures($pdo, $prefix, $idLang, $idProduct);
+ $featuresStr = '';
+ if ($featuresMap)
+ {
+ $pairs = [];
+ foreach ($featuresMap as $k => $v) $pairs[] = trim($k) . ': ' . trim($v);
+ $featuresStr = implode(' | ', $pairs);
+ }
+
+ $combinations = getProductCombinations($pdo, $prefix, $idLang, $idShop, $idProduct);
+
+ writeProductRowsToCsv($fh, $prod, $featuresStr, $combinations, $delimiter);
+ }
+
+ $offset += $limit;
+ echo "Przetworzono: {$offset} / {$total}\n";
+}
+
+fclose($fh);
+echo "✅ Zapisano CSV: {$outFile}\n";
diff --git a/export.php b/export.php
new file mode 100644
index 00000000..ac700387
--- /dev/null
+++ b/export.php
@@ -0,0 +1,78 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+// Pobierz produkty
+if ($testEan !== "") {
+ $sql = "SELECT p.id_product, p.ean13
+ FROM {$prefix}product p
+ WHERE p.ean13 = :ean";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute(['ean' => $testEan]);
+ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ echo "Tryb testowy – eksportuję tylko produkt o EAN: {$testEan}\n
";
+} else {
+ $sql = "SELECT p.id_product, p.ean13
+ FROM {$prefix}product p
+ WHERE p.ean13 IS NOT NULL AND p.ean13 <> ''";
+ $products = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
+ echo "Eksportuję wszystkie produkty\n
";
+}
+
+foreach ($products as $prod) {
+ $ean = $prod['ean13'];
+ $id_product = (int)$prod['id_product'];
+
+ // Pobierz zdjęcia produktu
+ $sqlImg = "SELECT id_image
+ FROM {$prefix}image
+ WHERE id_product = :id_product
+ ORDER BY position ASC";
+ $stmt = $pdo->prepare($sqlImg);
+ $stmt->execute(['id_product' => $id_product]);
+ $images = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (!$images) {
+ echo "Brak zdjęć dla EAN: {$ean}\n
";
+ continue;
+ }
+
+ $i = 1;
+ foreach ($images as $img) {
+ $id_image = (int)$img['id_image'];
+
+ // Ścieżka do pliku (PrestaShop – struktura katalogów np. 123 -> /1/2/3/123.jpg)
+ $path = implode('/', str_split((string)$id_image)) . "/" . $id_image . ".jpg";
+ $src = __DIR__ . "/img/p/" . $path;
+
+ if (file_exists($src)) {
+ $dest = $exportDir . "/" . $ean . "_" . $i . ".jpg";
+ copy($src, $dest);
+ echo "Zapisano: $dest\n
";
+ } else {
+ echo "Brak pliku: $src\n
";
+ }
+ $i++;
+ }
+}
+
+echo "✅ Eksport zakończony.\n
";
+?>
\ No newline at end of file