- Add 8 frontend methods to ArticleRepository (with Redis cache) - Create front\Views\Articles (rendering + utility methods) - Rewire front\view\Site::show() and front\controls\Site::route() to repo + Views - Update 5 article templates to use \front\Views\Articles:: - Convert front\factory\Articles and front\view\Articles to facades - Remove class.Article (entity + static methods migrated to repo + Views) - Remove front\factory\Settings facade (already migrated) - Fix: eliminate global $lang from articleNoindex(), inline page sort query - Tests: 450 OK, 1431 assertions (+13 new) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1852 lines
73 KiB
PHP
1852 lines
73 KiB
PHP
<?php
|
||
namespace Domain\Product;
|
||
|
||
/**
|
||
* Repository odpowiedzialny za dostęp do danych produktów
|
||
*
|
||
* Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
|
||
* logikę dostępu do bazy danych dla produktów.
|
||
*/
|
||
class ProductRepository
|
||
{
|
||
private const MAX_PER_PAGE = 100;
|
||
|
||
/**
|
||
* @var \medoo Instancja Medoo ORM
|
||
*/
|
||
private $db;
|
||
|
||
/**
|
||
* Konstruktor - przyjmuje instancję bazy danych
|
||
*
|
||
* @param \medoo $db Instancja Medoo ORM
|
||
*/
|
||
public function __construct($db)
|
||
{
|
||
$this->db = $db;
|
||
}
|
||
|
||
/**
|
||
* Pobiera stan magazynowy produktu
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return int|null Ilość produktu lub null jeśli nie znaleziono
|
||
*/
|
||
public function getQuantity(int $productId): ?int
|
||
{
|
||
$quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
|
||
|
||
// Medoo zwraca false jeśli nie znaleziono
|
||
return $quantity !== false ? (int)$quantity : null;
|
||
}
|
||
|
||
/**
|
||
* Pobiera produkt po ID
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return array|null Dane produktu lub null
|
||
*/
|
||
public function find(int $productId): ?array
|
||
{
|
||
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
|
||
return $product ?: null;
|
||
}
|
||
|
||
/**
|
||
* Zwraca liste produktow z archiwum do panelu admin.
|
||
*
|
||
* @return array{items: array<int, array<string, mixed>>, total: int}
|
||
*/
|
||
public function listArchivedForAdmin(
|
||
array $filters,
|
||
string $sortColumn = 'id',
|
||
string $sortDir = 'DESC',
|
||
int $page = 1,
|
||
int $perPage = 10
|
||
): array {
|
||
$allowedSortColumns = [
|
||
'id' => 'psp.id',
|
||
'name' => 'name',
|
||
'price_brutto' => 'psp.price_brutto',
|
||
'price_brutto_promo' => 'psp.price_brutto_promo',
|
||
'quantity' => 'psp.quantity',
|
||
'combinations' => 'combinations',
|
||
];
|
||
|
||
$sortSql = $allowedSortColumns[$sortColumn] ?? 'psp.id';
|
||
$sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC';
|
||
$page = max(1, $page);
|
||
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||
$offset = ($page - 1) * $perPage;
|
||
|
||
$where = ['psp.archive = 1', 'psp.parent_id IS NULL'];
|
||
$params = [];
|
||
|
||
$phrase = trim((string)($filters['phrase'] ?? ''));
|
||
if (strlen($phrase) > 255) {
|
||
$phrase = substr($phrase, 0, 255);
|
||
}
|
||
|
||
if ($phrase !== '') {
|
||
$where[] = '(
|
||
psp.ean LIKE :phrase
|
||
OR psp.sku LIKE :phrase
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM pp_shop_products_langs AS pspl2
|
||
WHERE pspl2.product_id = psp.id
|
||
AND pspl2.name LIKE :phrase
|
||
)
|
||
)';
|
||
$params[':phrase'] = '%' . $phrase . '%';
|
||
}
|
||
|
||
$whereSql = implode(' AND ', $where);
|
||
|
||
$sqlCount = "
|
||
SELECT COUNT(0)
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
";
|
||
|
||
$stmtCount = $this->db->query($sqlCount, $params);
|
||
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||
|
||
$sql = "
|
||
SELECT
|
||
psp.id,
|
||
psp.price_brutto,
|
||
psp.price_brutto_promo,
|
||
psp.quantity,
|
||
psp.sku,
|
||
psp.ean,
|
||
(
|
||
SELECT pspl.name
|
||
FROM pp_shop_products_langs AS pspl
|
||
INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id
|
||
WHERE pspl.product_id = psp.id
|
||
AND pspl.name <> ''
|
||
ORDER BY pl.o ASC
|
||
LIMIT 1
|
||
) AS name,
|
||
(
|
||
SELECT pspi.src
|
||
FROM pp_shop_products_images AS pspi
|
||
WHERE pspi.product_id = psp.id
|
||
ORDER BY pspi.o ASC, pspi.id ASC
|
||
LIMIT 1
|
||
) AS image_src,
|
||
(
|
||
SELECT pspi.alt
|
||
FROM pp_shop_products_images AS pspi
|
||
WHERE pspi.product_id = psp.id
|
||
ORDER BY pspi.o ASC, pspi.id ASC
|
||
LIMIT 1
|
||
) AS image_alt,
|
||
(
|
||
SELECT COUNT(0)
|
||
FROM pp_shop_products AS pspc
|
||
WHERE pspc.parent_id = psp.id
|
||
) AS combinations
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir}
|
||
LIMIT {$perPage} OFFSET {$offset}
|
||
";
|
||
|
||
$stmt = $this->db->query($sql, $params);
|
||
$items = $stmt ? $stmt->fetchAll() : [];
|
||
|
||
return [
|
||
'items' => is_array($items) ? $items : [],
|
||
'total' => $total,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Pobiera cenę produktu (promocyjną jeśli jest niższa, w przeciwnym razie regularną)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return float|null Cena brutto lub null jeśli nie znaleziono
|
||
*/
|
||
public function getPrice(int $productId): ?float
|
||
{
|
||
$prices = $this->db->get('pp_shop_products', ['price_brutto', 'price_brutto_promo'], ['id' => $productId]);
|
||
|
||
if (!$prices) {
|
||
return null;
|
||
}
|
||
|
||
if ($prices['price_brutto_promo'] != '' && $prices['price_brutto_promo'] < $prices['price_brutto']) {
|
||
return (float)$prices['price_brutto_promo'];
|
||
}
|
||
|
||
return (float)$prices['price_brutto'];
|
||
}
|
||
|
||
/**
|
||
* Pobiera nazwę produktu w danym języku
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param string $langId ID języka
|
||
* @return string|null Nazwa produktu lub null jeśli nie znaleziono
|
||
*/
|
||
public function getName(int $productId, string $langId): ?string
|
||
{
|
||
$name = $this->db->get('pp_shop_products_langs', 'name', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]);
|
||
|
||
return $name ?: null;
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje ilość produktu
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param int $quantity Nowa ilość
|
||
* @return bool Czy aktualizacja się powiodła
|
||
*/
|
||
public function updateQuantity(int $productId, int $quantity): bool
|
||
{
|
||
$result = $this->db->update(
|
||
'pp_shop_products',
|
||
['quantity' => $quantity],
|
||
['id' => $productId]
|
||
);
|
||
|
||
return $result !== false;
|
||
}
|
||
|
||
/**
|
||
* Przywraca produkt z archiwum (wraz z kombinacjami)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool Czy operacja się powiodła
|
||
*/
|
||
public function unarchive(int $productId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'id' => $productId ] );
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'parent_id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Przenosi produkt do archiwum (wraz z kombinacjami)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool Czy operacja się powiodła
|
||
*/
|
||
public function archive(int $productId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'id' => $productId ] );
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'parent_id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Pobiera listę wszystkich produktów głównych (id => name) do masowej edycji.
|
||
* Zwraca tylko produkty bez parent_id (bez kombinacji).
|
||
*
|
||
* @return array<int, string> Mapa id => nazwa produktu
|
||
*/
|
||
public function allProductsForMassEdit(): array
|
||
{
|
||
$defaultLang = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
|
||
if ( !$defaultLang ) {
|
||
$defaultLang = 'pl';
|
||
}
|
||
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
|
||
$products = [];
|
||
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $id ) {
|
||
$name = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $id, 'lang_id' => $defaultLang ]
|
||
] );
|
||
$products[ (int) $id ] = $name ?: '';
|
||
}
|
||
}
|
||
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Pobiera listę ID produktów przypisanych do danej kategorii.
|
||
*
|
||
* @param int $categoryId ID kategorii
|
||
* @return int[] Lista ID produktów
|
||
*/
|
||
public function getProductsByCategory(int $categoryId): array
|
||
{
|
||
$results = $this->db->select(
|
||
'pp_shop_products_categories',
|
||
'product_id',
|
||
[ 'category_id' => $categoryId ]
|
||
);
|
||
|
||
return is_array( $results ) ? $results : [];
|
||
}
|
||
|
||
/**
|
||
* Aplikuje rabat procentowy na produkt (cena promocyjna = cena - X%).
|
||
* Aktualizuje również ceny kombinacji produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param float $discountPercent Procent rabatu
|
||
* @return array|null Tablica z price_brutto i price_brutto_promo lub null przy błędzie
|
||
*/
|
||
public function applyDiscountPercent(int $productId, float $discountPercent): ?array
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', [
|
||
'vat', 'price_brutto', 'price_netto'
|
||
], [ 'id' => $productId ] );
|
||
|
||
if ( !$product ) {
|
||
return null;
|
||
}
|
||
|
||
$vat = $product['vat'];
|
||
$priceBrutto = (float) $product['price_brutto'];
|
||
$priceNetto = (float) $product['price_netto'];
|
||
|
||
$priceBruttoPromo = $priceBrutto - ( $priceBrutto * ( $discountPercent / 100 ) );
|
||
$priceNettoPromo = $priceNetto - ( $priceNetto * ( $discountPercent / 100 ) );
|
||
|
||
if ( $priceBrutto == $priceBruttoPromo ) {
|
||
$priceBruttoPromo = null;
|
||
}
|
||
if ( $priceNetto == $priceNettoPromo ) {
|
||
$priceNettoPromo = null;
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto_promo' => $priceBruttoPromo,
|
||
'price_netto_promo' => $priceNettoPromo
|
||
], [ 'id' => $productId ] );
|
||
|
||
$this->updateCombinationPrices( $productId, $priceNetto, $vat, $priceNettoPromo );
|
||
|
||
return [
|
||
'price_brutto' => $priceBrutto,
|
||
'price_brutto_promo' => $priceBruttoPromo
|
||
];
|
||
}
|
||
|
||
// ─── Krok 1: metody CRUD admin ───────────────────────────────────
|
||
|
||
/**
|
||
* Liczba produktów (nie-archiwum, parent_id IS NULL).
|
||
*/
|
||
public function countProducts(?array $where = null): int
|
||
{
|
||
if ( $where ) {
|
||
return (int) $this->db->count( 'pp_shop_products', $where );
|
||
}
|
||
|
||
return (int) $this->db->count( 'pp_shop_products', [ 'archive' => 0 ] );
|
||
}
|
||
|
||
/**
|
||
* Lista produktów do panelu admin (paginacja, sortowanie, filtrowanie).
|
||
*
|
||
* @return array{items: array, total: int}
|
||
*/
|
||
public function listForAdmin(
|
||
array $filters = [],
|
||
string $sortColumn = 'id',
|
||
string $sortDir = 'DESC',
|
||
int $page = 1,
|
||
int $perPage = 15
|
||
): array {
|
||
$page = max( 1, $page );
|
||
$perPage = min( max( 1, $perPage ), self::MAX_PER_PAGE );
|
||
$offset = ( $page - 1 ) * $perPage;
|
||
|
||
$allowedSort = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ];
|
||
if ( !in_array( $sortColumn, $allowedSort, true ) ) {
|
||
$sortColumn = 'id';
|
||
}
|
||
$sortDir = strtoupper( $sortDir ) === 'ASC' ? 'ASC' : 'DESC';
|
||
|
||
$conditions = [ 'psp.archive = 0', 'psp.parent_id IS NULL' ];
|
||
$params = [];
|
||
|
||
$search = trim( (string) ( $filters['search'] ?? '' ) );
|
||
if ( $search !== '' ) {
|
||
$conditions[] = '( pspl.name LIKE :search OR psp.ean LIKE :search_ean OR psp.sku LIKE :search_sku )';
|
||
$params[':search'] = '%' . $search . '%';
|
||
$params[':search_ean'] = '%' . $search . '%';
|
||
$params[':search_sku'] = '%' . $search . '%';
|
||
}
|
||
|
||
$statusFilter = (string) ( $filters['status'] ?? '' );
|
||
if ( $statusFilter === '1' || $statusFilter === '0' ) {
|
||
$conditions[] = 'psp.status = :status';
|
||
$params[':status'] = (int) $statusFilter;
|
||
}
|
||
|
||
$promotedFilter = (string) ( $filters['promoted'] ?? '' );
|
||
if ( $promotedFilter === '1' || $promotedFilter === '0' ) {
|
||
$conditions[] = 'psp.promoted = :promoted';
|
||
$params[':promoted'] = (int) $promotedFilter;
|
||
}
|
||
|
||
$whereSql = implode( ' AND ', $conditions );
|
||
$needsJoin = $search !== '' || $sortColumn === 'name';
|
||
|
||
$joinSql = $needsJoin
|
||
? 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id AND pspl.lang_id = ' . $this->db->quote( $this->defaultLangId() )
|
||
: '';
|
||
|
||
$orderColumn = $sortColumn === 'name' ? 'pspl.name' : 'psp.' . $sortColumn;
|
||
|
||
$countStmt = $this->db->query(
|
||
'SELECT COUNT( DISTINCT psp.id ) AS total '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. $joinSql . ' '
|
||
. 'WHERE ' . $whereSql,
|
||
$params
|
||
);
|
||
$countRow = $countStmt ? $countStmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
$total = isset( $countRow[0]['total'] ) ? (int) $countRow[0]['total'] : 0;
|
||
|
||
$stmt = $this->db->query(
|
||
'SELECT DISTINCT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. $joinSql . ' '
|
||
. 'WHERE ' . $whereSql
|
||
. ' ORDER BY ' . $orderColumn . ' ' . $sortDir . ', psp.id DESC'
|
||
. ' LIMIT ' . $offset . ', ' . $perPage,
|
||
$params
|
||
);
|
||
|
||
$rows = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
$items = [];
|
||
|
||
foreach ( $rows as $row ) {
|
||
$id = (int) ( $row['id'] ?? 0 );
|
||
if ( $id > 0 ) {
|
||
$product = $this->findForAdmin( $id );
|
||
if ( $product ) {
|
||
$items[] = $product;
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'items' => $items,
|
||
'total' => $total,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Szczegóły produktu (admin) — zastępuje factory product_details().
|
||
*/
|
||
public function findForAdmin(int $productId): ?array
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||
if ( !$product ) {
|
||
return null;
|
||
}
|
||
|
||
$results = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$product['languages'][ $row['lang_id'] ] = $row;
|
||
}
|
||
}
|
||
|
||
$product['images'] = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||
$product['files'] = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => $productId ] );
|
||
$product['categories'] = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
|
||
$product['attributes'] = $this->db->select( 'pp_shop_products_attributes', [ 'attribute_id', 'value_id' ], [ 'product_id' => $productId ] );
|
||
$product['products_related'] = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
|
||
$product['custom_fields'] = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||
|
||
return $product;
|
||
}
|
||
|
||
/**
|
||
* Prosta lista produktów (id => name) — do selectów w formularzu.
|
||
*/
|
||
public function allProductsList(): array
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
|
||
$products = [];
|
||
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $id ) {
|
||
$products[ $id ] = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $id, 'lang_id' => 'pl' ]
|
||
] );
|
||
}
|
||
}
|
||
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Tekst z nazwami kategorii produktu (oddzielone " / ").
|
||
*/
|
||
public function productCategoriesText(int $productId): string
|
||
{
|
||
$results = $this->db->query(
|
||
'SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid',
|
||
[ ':pid' => $productId ]
|
||
);
|
||
$rows = $results ? $results->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
|
||
if ( !is_array( $rows ) || empty( $rows ) ) {
|
||
return '';
|
||
}
|
||
|
||
$out = ' - ';
|
||
foreach ( $rows as $i => $row ) {
|
||
$title = $this->db->get( 'pp_shop_categories_langs', 'title', [
|
||
'AND' => [
|
||
'category_id' => (int) $row['category_id'],
|
||
'lang_id' => $this->defaultLangId(),
|
||
]
|
||
] );
|
||
$out .= $title ?: '';
|
||
if ( $i < count( $rows ) - 1 ) {
|
||
$out .= ' / ';
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* ID produktu nadrzędnego (parent_id).
|
||
*/
|
||
public function getParentId(int $productId): ?int
|
||
{
|
||
$parentId = $this->db->get( 'pp_shop_products', 'parent_id', [ 'id' => $productId ] );
|
||
return $parentId ? (int) $parentId : null;
|
||
}
|
||
|
||
/**
|
||
* Domyślna nazwa produktu (w domyślnym języku).
|
||
*/
|
||
public function productDefaultName(int $productId): ?string
|
||
{
|
||
$name = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $this->defaultLangId() ]
|
||
] );
|
||
return $name ?: null;
|
||
}
|
||
|
||
/**
|
||
* ID domyślnego języka.
|
||
*/
|
||
private function defaultLangId(): string
|
||
{
|
||
$langId = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
|
||
return $langId ?: 'pl';
|
||
}
|
||
|
||
// ─── Krok 2: zapis produktu ──────────────────────────────────────
|
||
|
||
/**
|
||
* Zapis produktu (insert lub update). Zastępuje factory save().
|
||
*
|
||
* @param array $d Dane produktu (z formularza)
|
||
* @param int|null $userId ID aktualnego użytkownika admin
|
||
* @return int|null ID produktu lub null przy błędzie
|
||
*/
|
||
public function saveProduct(array $d, ?int $userId = null): ?int
|
||
{
|
||
$productId = (int) ( $d['id'] ?? 0 );
|
||
$isNew = !$productId;
|
||
|
||
$productData = [
|
||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||
'modify_by' => $userId,
|
||
'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
|
||
'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
|
||
'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),
|
||
'vat' => $d['vat'] ?? 0,
|
||
'promoted' => ( $d['promoted'] ?? '' ) === 'on' ? 1 : 0,
|
||
'layout_id' => $this->nullIfEmpty( $d['layout_id'] ?? null ),
|
||
'price_netto_promo' => $this->nullIfEmpty( $d['price_netto_promo'] ?? null ),
|
||
'price_brutto_promo' => $this->nullIfEmpty( $d['price_brutto_promo'] ?? null ),
|
||
'new_to_date' => $this->nullIfEmpty( $d['new_to_date'] ?? null ),
|
||
'stock_0_buy' => ( $d['stock_0_buy'] ?? '' ) === 'on' ? 1 : 0,
|
||
'wp' => $this->nullIfEmpty( $d['wp'] ?? null ),
|
||
'sku' => $this->nullIfEmpty( $d['sku'] ?? null ),
|
||
'ean' => $this->nullIfEmpty( $d['ean'] ?? null ),
|
||
'custom_label_0' => $this->nullIfEmpty( $d['custom_label_0'] ?? null ),
|
||
'custom_label_1' => $this->nullIfEmpty( $d['custom_label_1'] ?? null ),
|
||
'custom_label_2' => $this->nullIfEmpty( $d['custom_label_2'] ?? null ),
|
||
'custom_label_3' => $this->nullIfEmpty( $d['custom_label_3'] ?? null ),
|
||
'custom_label_4' => $this->nullIfEmpty( $d['custom_label_4'] ?? null ),
|
||
'additional_message' => ( $d['additional_message'] ?? '' ) == 'on' ? 1 : 0,
|
||
'set_id' => !empty( $d['set'] ) ? (int) $d['set'] : null,
|
||
'quantity' => (int) ( $d['quantity'] ?? 0 ),
|
||
'additional_message_text' => $this->nullIfEmpty( $d['additional_message_text'] ?? null ),
|
||
'additional_message_required' => ( $d['additional_message_required'] ?? '' ) == 'on' ? 1 : 0,
|
||
'producer_id' => !empty( $d['producer_id'] ) ? $d['producer_id'] : null,
|
||
'product_unit_id' => !empty( $d['product_unit'] ) ? $d['product_unit'] : null,
|
||
'weight' => !empty( $d['weight'] ) ? $d['weight'] : null,
|
||
];
|
||
|
||
if ( $isNew ) {
|
||
$productData['date_add'] = date( 'Y-m-d H:i:s' );
|
||
$this->db->insert( 'pp_shop_products', $productData );
|
||
$productId = (int) $this->db->id();
|
||
|
||
if ( !$productId ) {
|
||
return null;
|
||
}
|
||
} else {
|
||
$this->db->update( 'pp_shop_products', $productData, [ 'id' => $productId ] );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'additional_message' => $productData['additional_message'],
|
||
], [ 'parent_id' => $productId ] );
|
||
|
||
$this->updateCombinationPricesFromBase(
|
||
$productId,
|
||
$d['price_brutto'] ?? 0,
|
||
$d['vat'] ?? 0,
|
||
$d['price_brutto_promo'] ?? null
|
||
);
|
||
}
|
||
|
||
$this->saveLanguages( $productId, $d, $isNew );
|
||
$this->saveCategories( $productId, $d['categories'] ?? null );
|
||
$this->saveRelatedProducts( $productId, $d['products_related'] ?? null );
|
||
$this->moveTemporaryFiles( $productId );
|
||
$this->moveTemporaryImages( $productId );
|
||
|
||
if ( !empty( $d['gallery_order'] ) ) {
|
||
$this->saveImagesOrder( $productId, $d['gallery_order'] );
|
||
}
|
||
|
||
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
|
||
|
||
if ( !$isNew ) {
|
||
$this->cleanupDeletedFiles( $productId );
|
||
$this->cleanupDeletedImages( $productId );
|
||
}
|
||
|
||
\S::htacces();
|
||
\S::delete_dir( '../temp/' );
|
||
\S::delete_dir( '../thumbs/' );
|
||
|
||
if ( !$isNew ) {
|
||
$redis = \RedisConnection::getInstance()->getConnection();
|
||
if ( $redis ) {
|
||
$redis->flushAll();
|
||
}
|
||
}
|
||
|
||
return $productId;
|
||
}
|
||
|
||
private function saveLanguages(int $productId, array $d, bool $isNew): void
|
||
{
|
||
$langs = ( new \Domain\Languages\LanguagesRepository( $this->db ) )->languagesList( true );
|
||
|
||
foreach ( $langs as $lg ) {
|
||
$lid = $lg['id'];
|
||
$langData = [
|
||
'name' => $this->nullIfEmpty( $d['name'][$lid] ?? null ),
|
||
'short_description' => $this->nullIfEmpty( $d['short_description'][$lid] ?? null ),
|
||
'description' => $this->nullIfEmpty( $d['description'][$lid] ?? null ),
|
||
'meta_description' => $this->nullIfEmpty( $d['meta_description'][$lid] ?? null ),
|
||
'meta_keywords' => $this->nullIfEmpty( $d['meta_keywords'][$lid] ?? null ),
|
||
'seo_link' => !empty( $d['seo_link'][$lid] ) ? \S::seo( $d['seo_link'][$lid] ) : null,
|
||
'copy_from' => $this->nullIfEmpty( $d['copy_from'][$lid] ?? null ),
|
||
'warehouse_message_zero' => $this->nullIfEmpty( $d['warehouse_message_zero'][$lid] ?? null ),
|
||
'warehouse_message_nonzero' => $this->nullIfEmpty( $d['warehouse_message_nonzero'][$lid] ?? null ),
|
||
'tab_name_1' => $this->nullIfEmpty( $d['tab_name_1'][$lid] ?? null ),
|
||
'tab_description_1' => $this->nullIfEmpty( $d['tab_description_1'][$lid] ?? null ),
|
||
'tab_name_2' => $this->nullIfEmpty( $d['tab_name_2'][$lid] ?? null ),
|
||
'tab_description_2' => $this->nullIfEmpty( $d['tab_description_2'][$lid] ?? null ),
|
||
'canonical' => $this->nullIfEmpty( $d['canonical'][$lid] ?? null ),
|
||
'meta_title' => $this->nullIfEmpty( $d['meta_title'][$lid] ?? null ),
|
||
'security_information' => $this->nullIfEmpty( $d['security_information'][$lid] ?? null ),
|
||
];
|
||
|
||
if ( $isNew ) {
|
||
$langData['product_id'] = $productId;
|
||
$langData['lang_id'] = $lid;
|
||
$this->db->insert( 'pp_shop_products_langs', $langData );
|
||
} else {
|
||
$translationId = $this->db->get( 'pp_shop_products_langs', 'id', [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $lid ]
|
||
] );
|
||
|
||
if ( $translationId ) {
|
||
$currentSeoLink = $this->db->get( 'pp_shop_products_langs', 'seo_link', [ 'id' => $translationId ] );
|
||
$newSeoLink = $langData['seo_link'] ?: \S::seo( 'p-' . $productId . '-' . ( $d['name'][$lid] ?? '' ) );
|
||
|
||
if ( $newSeoLink !== $currentSeoLink && $currentSeoLink != '' ) {
|
||
$this->handleSeoRedirects( $productId, $lid, $newSeoLink, $currentSeoLink );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_langs', $langData, [ 'id' => $translationId ] );
|
||
} else {
|
||
$langData['product_id'] = $productId;
|
||
$langData['lang_id'] = $lid;
|
||
$this->db->insert( 'pp_shop_products_langs', $langData );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private function handleSeoRedirects(int $productId, string $langId, string $newSeoLink, string $currentSeoLink): void
|
||
{
|
||
if ( $this->db->count( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
|
||
$this->db->delete( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
|
||
}
|
||
|
||
$this->db->delete( 'pp_redirects', [
|
||
'AND' => [
|
||
'product_id' => $productId,
|
||
'lang_id' => $langId,
|
||
'from' => $currentSeoLink,
|
||
'to[!]' => $newSeoLink,
|
||
],
|
||
] );
|
||
|
||
$seoUsed = (bool) $this->db->count( 'pp_shop_products_langs', [
|
||
'AND' => [
|
||
'lang_id' => $langId,
|
||
'seo_link' => $currentSeoLink,
|
||
'product_id[!]' => $productId,
|
||
],
|
||
] );
|
||
|
||
if ( !$seoUsed ) {
|
||
$this->db->delete( 'pp_redirects', [
|
||
'AND' => [
|
||
'from' => $currentSeoLink,
|
||
'lang_id' => $langId,
|
||
'product_id[!]' => $productId,
|
||
],
|
||
] );
|
||
|
||
if ( !$this->db->count( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
|
||
if ( \S::canAddRedirect( $currentSeoLink, $newSeoLink, $langId ) ) {
|
||
$this->db->insert( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
|
||
}
|
||
}
|
||
} else {
|
||
$this->db->delete( 'pp_redirects', [ 'AND' => [ 'product_id' => $productId, 'lang_id' => $langId, 'from' => $currentSeoLink ] ] );
|
||
}
|
||
}
|
||
|
||
private function saveCategories(int $productId, $categories): void
|
||
{
|
||
if ( !is_array( $categories ) ) {
|
||
$categories = $categories ? [ $categories ] : [];
|
||
}
|
||
|
||
$notIn = array_merge( [0], $categories );
|
||
$this->db->delete( 'pp_shop_products_categories', [ 'AND' => [ 'product_id' => $productId, 'category_id[!]' => $notIn ] ] );
|
||
|
||
$existing = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
|
||
$existing = is_array( $existing ) ? $existing : [];
|
||
$toAdd = array_diff( $categories, $existing );
|
||
|
||
foreach ( $toAdd as $categoryId ) {
|
||
if ( $productId && $categoryId ) {
|
||
$order = (int) $this->db->max( 'pp_shop_products_categories', 'o' ) + 1;
|
||
$this->db->insert( 'pp_shop_products_categories', [
|
||
'product_id' => $productId,
|
||
'category_id' => (int) $categoryId,
|
||
'o' => $order,
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function saveRelatedProducts(int $productId, $products): void
|
||
{
|
||
if ( !is_array( $products ) ) {
|
||
$products = $products ? [ $products ] : [];
|
||
}
|
||
|
||
$notIn = array_merge( [0], $products );
|
||
$this->db->delete( 'pp_shop_products_related', [ 'AND' => [ 'product_id' => $productId, 'product_related_id[!]' => $notIn ] ] );
|
||
|
||
$existing = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
|
||
$existing = is_array( $existing ) ? $existing : [];
|
||
$toAdd = array_diff( $products, $existing );
|
||
|
||
foreach ( $toAdd as $relatedId ) {
|
||
if ( $productId && $relatedId ) {
|
||
$this->db->insert( 'pp_shop_products_related', [
|
||
'product_id' => $productId,
|
||
'product_related_id' => (int) $relatedId,
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function moveTemporaryFiles(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
|
||
if ( !is_array( $results ) ) return;
|
||
|
||
$created = false;
|
||
foreach ( $results as $row ) {
|
||
$dir = '/upload/product_files/product_' . $productId;
|
||
$newName = str_replace( '/upload/product_files/tmp', $dir, $row['src'] );
|
||
|
||
if ( file_exists( '..' . $row['src'] ) ) {
|
||
if ( !is_dir( '../' . $dir ) && !$created ) {
|
||
if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
|
||
}
|
||
rename( '..' . $row['src'], '..' . $newName );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_files', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
private function moveTemporaryImages(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
|
||
if ( !is_array( $results ) ) return;
|
||
|
||
$created = false;
|
||
foreach ( $results as $row ) {
|
||
$dir = '/upload/product_images/product_' . $productId;
|
||
$newName = str_replace( '/upload/product_images/tmp', $dir, $row['src'] );
|
||
|
||
if ( file_exists( '..' . $newName ) ) {
|
||
$ext = strrpos( $newName, '.' );
|
||
$base = substr( $newName, 0, $ext );
|
||
$extension = substr( $newName, $ext );
|
||
$count = 1;
|
||
while ( file_exists( '..' . $base . '_' . $count . $extension ) ) {
|
||
++$count;
|
||
}
|
||
$newName = $base . '_' . $count . $extension;
|
||
}
|
||
|
||
if ( file_exists( '..' . $row['src'] ) ) {
|
||
if ( !is_dir( '../' . $dir ) && !$created ) {
|
||
if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
|
||
}
|
||
rename( '..' . $row['src'], '..' . $newName );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_images', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
private function saveCustomFields(int $productId, array $names, array $types, array $required): void
|
||
{
|
||
$existingIds = [];
|
||
foreach ( $names as $i => $name ) {
|
||
if ( !empty( $name ) ) {
|
||
$id = (int) $this->db->get( 'pp_shop_products_custom_fields', 'id_additional_field', [
|
||
'AND' => [ 'id_product' => $productId, 'name' => $name ]
|
||
] );
|
||
if ( $id ) $existingIds[] = $id;
|
||
}
|
||
}
|
||
|
||
if ( !empty( $existingIds ) ) {
|
||
$this->db->delete( 'pp_shop_products_custom_fields', [
|
||
'AND' => [ 'id_product' => $productId, 'id_additional_field[!]' => $existingIds ]
|
||
] );
|
||
} else {
|
||
$this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
|
||
}
|
||
|
||
foreach ( $names as $i => $name ) {
|
||
if ( empty( $name ) ) continue;
|
||
|
||
$typeVal = $types[$i] ?? 'text';
|
||
$isRequired = !empty( $required[$i] ) ? 1 : 0;
|
||
|
||
if ( !$this->db->count( 'pp_shop_products_custom_fields', [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] ) ) {
|
||
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||
'id_product' => $productId,
|
||
'name' => $name,
|
||
'type' => $typeVal,
|
||
'is_required' => $isRequired,
|
||
] );
|
||
} else {
|
||
$this->db->update( 'pp_shop_products_custom_fields', [
|
||
'type' => $typeVal,
|
||
'is_required' => $isRequired,
|
||
], [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function cleanupDeletedFiles(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
if ( file_exists( '../' . $row['src'] ) ) {
|
||
unlink( '../' . $row['src'] );
|
||
}
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_files', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
}
|
||
|
||
private function cleanupDeletedImages(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
if ( file_exists( '../' . $row['src'] ) ) {
|
||
unlink( '../' . $row['src'] );
|
||
}
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_images', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
}
|
||
|
||
private function nullIfEmpty($value)
|
||
{
|
||
return ( $value !== null && $value !== '' && $value !== 0.00 ) ? $value : null;
|
||
}
|
||
|
||
// ─── Koniec kroku 2 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 3: Operacje na produktach ──────────────────────────────
|
||
|
||
/**
|
||
* Usuwa produkt i wszystkie powiązane dane.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool
|
||
*/
|
||
public function delete(int $productId): bool
|
||
{
|
||
// Usuń kombinacje (produkty z parent_id = productId)
|
||
$combinations = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
|
||
if ( $combinations ) {
|
||
foreach ( $combinations as $combId ) {
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combId ] );
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $combId ] );
|
||
}
|
||
}
|
||
|
||
$this->db->delete( 'pp_shop_products_categories', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_routes', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_redirects', [ 'product_id' => $productId ] );
|
||
|
||
\S::delete_dir( '../upload/product_images/product_' . $productId . '/' );
|
||
\S::delete_dir( '../upload/product_files/product_' . $productId . '/' );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Duplikuje produkt z opcjonalnym kopiowaniem kombinacji.
|
||
*
|
||
* @param int $productId ID produktu do zduplikowania
|
||
* @param bool $withCombinations Czy kopiować kombinacje
|
||
* @return bool
|
||
*/
|
||
public function duplicate(int $productId, bool $withCombinations = false): bool
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||
if ( !$product ) {
|
||
return false;
|
||
}
|
||
|
||
$this->db->insert( 'pp_shop_products', [
|
||
'price_netto' => $product['price_netto'],
|
||
'price_brutto' => $product['price_brutto'],
|
||
'price_netto_promo' => $product['price_netto_promo'],
|
||
'price_brutto_promo' => $product['price_brutto_promo'],
|
||
'vat' => $product['vat'],
|
||
'promoted' => $product['promoted'],
|
||
'layout_id' => $product['layout_id'],
|
||
'new_to_date' => $product['new_to_date'],
|
||
'stock_0_buy' => $product['stock_0_buy'],
|
||
'wp' => $product['wp'],
|
||
'custom_label_0' => $product['custom_label_0'],
|
||
'custom_label_1' => $product['custom_label_1'],
|
||
'custom_label_2' => $product['custom_label_2'],
|
||
'custom_label_3' => $product['custom_label_3'],
|
||
'custom_label_4' => $product['custom_label_4'],
|
||
'additional_message' => $product['additional_message'],
|
||
] );
|
||
|
||
$newProductId = $this->db->id();
|
||
if ( !$newProductId ) {
|
||
return false;
|
||
}
|
||
|
||
// Atrybuty
|
||
$attributes = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $productId ] );
|
||
if ( \S::is_array_fix( $attributes ) ) {
|
||
foreach ( $attributes as $row ) {
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $newProductId,
|
||
'attribute_id' => $row['attribute_id'],
|
||
'value_id' => $row['value_id'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Kategorie
|
||
$categories = $this->db->select( 'pp_shop_products_categories', '*', [ 'product_id' => $productId ] );
|
||
if ( \S::is_array_fix( $categories ) ) {
|
||
foreach ( $categories as $row ) {
|
||
$this->db->insert( 'pp_shop_products_categories', [
|
||
'product_id' => $newProductId,
|
||
'category_id' => $row['category_id'],
|
||
'o' => $row['o'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Języki
|
||
$langs = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
|
||
if ( \S::is_array_fix( $langs ) ) {
|
||
foreach ( $langs as $row ) {
|
||
$this->db->insert( 'pp_shop_products_langs', [
|
||
'product_id' => $newProductId,
|
||
'lang_id' => $row['lang_id'],
|
||
'name' => $row['name'] . ' - kopia',
|
||
'short_description' => $row['short_description'],
|
||
'description' => $row['description'],
|
||
'tab_name_1' => $row['tab_name_1'],
|
||
'tab_description_1' => $row['tab_description_1'],
|
||
'tab_name_2' => $row['tab_name_2'],
|
||
'tab_description_2' => $row['tab_description_2'],
|
||
'meta_description' => $row['meta_description'],
|
||
'meta_keywords' => $row['meta_keywords'],
|
||
'copy_from' => $row['copy_from'],
|
||
'warehouse_message_zero' => $row['warehouse_message_zero'],
|
||
'warehouse_message_nonzero' => $row['warehouse_message_nonzero'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Custom fields
|
||
$customFields = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||
if ( \S::is_array_fix( $customFields ) ) {
|
||
foreach ( $customFields as $row ) {
|
||
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||
'id_product' => $newProductId,
|
||
'name' => $row['name'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Duplikowanie kombinacji
|
||
if ( $withCombinations ) {
|
||
$productCombinations = $this->db->select( 'pp_shop_products', '*', [ 'parent_id' => $productId ] );
|
||
if ( \S::is_array_fix( $productCombinations ) ) {
|
||
foreach ( $productCombinations as $comb ) {
|
||
$this->db->insert( 'pp_shop_products', [
|
||
'parent_id' => $newProductId,
|
||
'permutation_hash' => $comb['permutation_hash'],
|
||
'price_netto' => $comb['price_netto'],
|
||
'price_brutto' => $comb['price_brutto'],
|
||
'price_netto_promo' => $comb['price_netto_promo'],
|
||
'price_brutto_promo' => $comb['price_brutto_promo'],
|
||
'vat' => $comb['vat'],
|
||
'stock_0_buy' => $comb['stock_0_buy'],
|
||
'quantity' => $comb['quantity'],
|
||
'wp' => $comb['wp'],
|
||
'additional_message' => $comb['additional_message'],
|
||
'additional_message_text' => $comb['additional_message_text'],
|
||
'additional_message_required' => $comb['additional_message_required'],
|
||
] );
|
||
|
||
$combNewId = $this->db->id();
|
||
if ( $combNewId ) {
|
||
$combAttrs = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $comb['id'] ] );
|
||
if ( \S::is_array_fix( $combAttrs ) ) {
|
||
foreach ( $combAttrs as $attr ) {
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $combNewId,
|
||
'attribute_id' => $attr['attribute_id'],
|
||
'value_id' => $attr['value_id'],
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Przełącza status produktu (aktywny/nieaktywny).
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool
|
||
*/
|
||
public function toggleStatus(int $productId): bool
|
||
{
|
||
$status = $this->db->get( 'pp_shop_products', 'status', [ 'id' => $productId ] );
|
||
$newStatus = $status == 1 ? 0 : 1;
|
||
$this->db->update( 'pp_shop_products', [ 'status' => $newStatus ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana ceny brutto produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param mixed $price Nowa cena brutto
|
||
* @return bool
|
||
*/
|
||
public function updatePriceBrutto(int $productId, $price): bool
|
||
{
|
||
$vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceNetto = \S::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto' => $price != 0.00 ? $price : null,
|
||
'price_netto' => $priceNetto != 0.00 ? $priceNetto : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana ceny promocyjnej brutto produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param mixed $price Nowa cena promo brutto
|
||
* @return bool
|
||
*/
|
||
public function updatePriceBruttoPromo(int $productId, $price): bool
|
||
{
|
||
$vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceNetto = \S::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto_promo' => $price != 0.00 ? $price : null,
|
||
'price_netto_promo' => $priceNetto != 0.00 ? $priceNetto : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana custom label produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param string $label Numer labela (0-4)
|
||
* @param mixed $value Wartość
|
||
* @return bool
|
||
*/
|
||
public function updateCustomLabel(int $productId, string $label, $value): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [
|
||
'custom_label_' . $label => $value ? $value : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
// ─── Koniec kroku 3 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 4: Kombinacje ──────────────────────────────────────────
|
||
|
||
/**
|
||
* Pobiera kombinacje produktu — lekka wersja do tabeli admin.
|
||
* Jedno zapytanie SQL zamiast N×7 (findForAdmin per wiersz).
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return array<int, array{id: int, permutation_hash: string, sku: ?string, quantity: ?int, price_netto: ?float, stock_0_buy: int, parent_id: int}>
|
||
*/
|
||
public function getCombinationsForTable(int $productId): array
|
||
{
|
||
$stmt = $this->db->query(
|
||
'SELECT id, permutation_hash, sku, quantity, price_netto, stock_0_buy, parent_id
|
||
FROM pp_shop_products
|
||
WHERE parent_id = :pid
|
||
ORDER BY id ASC',
|
||
[ ':pid' => $productId ]
|
||
);
|
||
|
||
return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
}
|
||
|
||
/**
|
||
* Pobiera permutacje (kombinacje) produktu z pełnymi danymi.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return array
|
||
*/
|
||
public function getPermutations(int $productId): array
|
||
{
|
||
$products = [];
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
|
||
if ( \S::is_array_fix( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$detail = $this->findForAdmin( (int) $row );
|
||
if ( $detail ) {
|
||
$products[] = $detail;
|
||
}
|
||
}
|
||
}
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Generuje kombinacje produktu na podstawie atrybutów.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param array $attributes Tablica atrybutów [attribute_id => [value_id, ...], ...]
|
||
* @return bool
|
||
*/
|
||
public function generateCombinations(int $productId, array $attributes): bool
|
||
{
|
||
$vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$attributeRepository = new \Domain\Attribute\AttributeRepository( $this->db );
|
||
|
||
$permutations = \shop\Product::array_cartesian( $attributes );
|
||
if ( !\S::is_array_fix( $permutations ) ) {
|
||
return true;
|
||
}
|
||
|
||
foreach ( $permutations as $permutation ) {
|
||
$product = null;
|
||
ksort( $permutation );
|
||
|
||
$permutationHash = '';
|
||
|
||
if ( \S::is_array_fix( $permutation ) ) {
|
||
foreach ( $permutation as $key => $val ) {
|
||
if ( $permutationHash ) {
|
||
$permutationHash .= '|';
|
||
}
|
||
$permutationHash .= $key . '-' . $val;
|
||
|
||
$valueDetails = $attributeRepository->valueDetails( (int) $val );
|
||
$impactOnPrice = $valueDetails['impact_on_the_price'];
|
||
|
||
if ( $impactOnPrice > 0 ) {
|
||
if ( !$product ) {
|
||
$product = $this->findForAdmin( $productId );
|
||
}
|
||
|
||
$productPriceBrutto = $product['price_brutto'] + $impactOnPrice;
|
||
$productPriceNetto = $productPriceBrutto / ( 1 + ( $product['vat'] / 100 ) );
|
||
|
||
if ( $product['price_brutto_promo'] ) {
|
||
$productPriceBruttoPromo = $product['price_brutto_promo'] + $impactOnPrice;
|
||
$productPriceNettoPromo = $productPriceBruttoPromo / ( 1 + ( $product['vat'] / 100 ) );
|
||
} else {
|
||
$productPriceBruttoPromo = null;
|
||
$productPriceNettoPromo = null;
|
||
}
|
||
}
|
||
|
||
if ( $permutationHash && !$this->db->count( 'pp_shop_products', [ 'AND' => [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash ] ] ) ) {
|
||
if ( $this->db->insert( 'pp_shop_products', [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash, 'vat' => $vat ] ) ) {
|
||
$combinationId = $this->db->id();
|
||
if ( $product ) {
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $productPriceNetto,
|
||
'vat' => $product['vat'],
|
||
'price_brutto' => $productPriceBrutto,
|
||
'price_netto_promo' => $productPriceNettoPromo ?? null,
|
||
'price_brutto_promo' => $productPriceBruttoPromo ?? null,
|
||
], [ 'id' => $combinationId ] );
|
||
}
|
||
|
||
$hashRows = explode( '|', $permutationHash );
|
||
foreach ( $hashRows as $hashRow ) {
|
||
$attrRev = explode( '-', $hashRow );
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $combinationId,
|
||
'attribute_id' => $attrRev[0],
|
||
'value_id' => $attrRev[1],
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa kombinację produktu.
|
||
*
|
||
* @param int $combinationId ID kombinacji
|
||
* @return bool
|
||
*/
|
||
public function deleteCombination(int $combinationId): bool
|
||
{
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $combinationId ] );
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combinationId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Liczy kombinacje produktu.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return int
|
||
*/
|
||
public function countCombinations(int $productId): int
|
||
{
|
||
return (int) $this->db->count( 'pp_shop_products', [ 'parent_id' => $productId ] );
|
||
}
|
||
|
||
/**
|
||
* Zapisuje stock_0_buy kombinacji.
|
||
*/
|
||
public function saveCombinationStock0Buy(int $productId, $value): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'stock_0_buy' => $value == 'true' ? 1 : 0 ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje SKU kombinacji.
|
||
*/
|
||
public function saveCombinationSku(int $productId, $sku): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'sku' => $sku ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje ilość kombinacji.
|
||
*/
|
||
public function saveCombinationQuantity(int $productId, $quantity): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [
|
||
'quantity' => $quantity == '' ? null : (int) $quantity,
|
||
], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje cenę kombinacji (z przeliczeniem netto -> brutto).
|
||
*/
|
||
public function saveCombinationPrice(int $productId, $priceNetto): bool
|
||
{
|
||
$vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceBrutto = (float) $priceNetto * ( 1 + ( (float) $vat / 100 ) );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $priceNetto == '' ? null : (float) $priceNetto,
|
||
'price_brutto' => $priceBrutto == '' ? null : (float) $priceBrutto,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
// ─── Koniec kroku 4 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 5: Zdjęcia / Pliki / XML ──────────────────────────────
|
||
|
||
/**
|
||
* Oznacza zdjęcie do usunięcia.
|
||
*/
|
||
public function deleteImage(int $imageId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_images', [ 'to_delete' => 1 ], [ 'id' => $imageId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zmienia atrybut alt zdjęcia.
|
||
*/
|
||
public function updateImageAlt(int $imageId, string $alt): bool
|
||
{
|
||
$result = $this->db->update( 'pp_shop_products_images', [ 'alt' => $alt ], [ 'id' => $imageId ] );
|
||
\S::delete_cache();
|
||
return (bool) $result;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje kolejność zdjęć.
|
||
*/
|
||
public function saveImagesOrder(int $productId, string $order): bool
|
||
{
|
||
$orderArr = explode( ';', $order );
|
||
if ( is_array( $orderArr ) && !empty( $orderArr ) ) {
|
||
$i = 0;
|
||
foreach ( $orderArr as $imageId ) {
|
||
$this->db->update( 'pp_shop_products_images', [
|
||
'o' => $i++,
|
||
], [
|
||
'AND' => [
|
||
'product_id' => $productId,
|
||
'id' => $imageId,
|
||
],
|
||
] );
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa nieprzypisane zdjęcia (product_id = null).
|
||
*/
|
||
public function deleteNonassignedImages(): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
if ( file_exists( '../' . $row['src'] ) ) {
|
||
unlink( '../' . $row['src'] );
|
||
}
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => null ] );
|
||
}
|
||
|
||
/**
|
||
* Oznacza plik do usunięcia.
|
||
*/
|
||
public function deleteFile(int $fileId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_files', [ 'to_delete' => 1 ], [ 'id' => $fileId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zmienia nazwę pliku.
|
||
*/
|
||
public function updateFileName(int $fileId, string $name): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_files', [ 'name' => $name ], [ 'id' => $fileId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa nieprzypisane pliki (product_id = null).
|
||
*/
|
||
public function deleteNonassignedFiles(): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
if ( file_exists( '../' . $row['src'] ) ) {
|
||
unlink( '../' . $row['src'] );
|
||
}
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => null ] );
|
||
}
|
||
|
||
/**
|
||
* Pobiera zdjęcia produktu.
|
||
*/
|
||
public function getProductImages(int $productId): array
|
||
{
|
||
return $this->db->select( 'pp_shop_products_images', 'src', [
|
||
'product_id' => $productId,
|
||
'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ],
|
||
] ) ?: [];
|
||
}
|
||
|
||
/**
|
||
* Zapisuje nazwę XML produktu.
|
||
*/
|
||
public function saveXmlName(int $productId, string $xmlName, string $langId): bool
|
||
{
|
||
return (bool) $this->db->update( 'pp_shop_products_langs', [
|
||
'xml_name' => $xmlName,
|
||
], [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $langId ],
|
||
] );
|
||
}
|
||
|
||
/**
|
||
* Pobiera sugestie custom label.
|
||
*/
|
||
public function customLabelSuggestions(string $customLabel, string $labelType): array
|
||
{
|
||
$output = [];
|
||
$results = $this->db->query(
|
||
'SELECT DISTINCT ' . $labelType . ' AS label FROM pp_shop_products WHERE ' . $labelType . ' LIKE :custom_label LIMIT 10',
|
||
[ ':custom_label' => '%' . $customLabel . '%' ]
|
||
);
|
||
if ( $results ) {
|
||
foreach ( $results->fetchAll( \PDO::FETCH_ASSOC ) as $row ) {
|
||
$output[] = $row;
|
||
}
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje custom label produktu.
|
||
*/
|
||
public function saveCustomLabel(int $productId, string $customLabel, string $labelType): bool
|
||
{
|
||
return (bool) $this->db->update( 'pp_shop_products', [ $labelType => $customLabel ], [ 'id' => $productId ] );
|
||
}
|
||
|
||
/**
|
||
* Generuje kod EAN.
|
||
*/
|
||
public function generateEAN(string $number): string
|
||
{
|
||
$code = '200' . str_pad( $number, 9, '0' );
|
||
$weightflag = true;
|
||
$sum = 0;
|
||
|
||
for ( $i = strlen( $code ) - 1; $i >= 0; $i-- ) {
|
||
$sum += (int) $code[$i] * ( $weightflag ? 3 : 1 );
|
||
$weightflag = !$weightflag;
|
||
}
|
||
|
||
$code .= ( 10 - ( $sum % 10 ) ) % 10;
|
||
return $code;
|
||
}
|
||
|
||
/**
|
||
* Generuje Google Feed XML.
|
||
*/
|
||
public function generateGoogleFeedXml(): void
|
||
{
|
||
global $lang_id;
|
||
|
||
$settings = ( new \Domain\Settings\SettingsRepository( $this->db ) )->allSettings( true );
|
||
|
||
$domainPrefix = 'https';
|
||
$url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
|
||
|
||
$doc = new \DOMDocument( '1.0', 'UTF-8' );
|
||
$xmlRoot = $doc->createElement( 'rss' );
|
||
$doc->appendChild( $xmlRoot );
|
||
$xmlRoot->setAttribute( 'version', '2.0' );
|
||
$xmlRoot->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:g', 'http://base.google.com/ns/1.0' );
|
||
$channelNode = $xmlRoot->appendChild( $doc->createElement( 'channel' ) );
|
||
$channelNode->appendChild( $doc->createElement( 'title', $settings['firm_name'] ) );
|
||
$channelNode->appendChild( $doc->createElement( 'link', $domainPrefix . '://' . $url ) );
|
||
|
||
$rows = $this->db->select( 'pp_shop_products', 'id', [
|
||
'AND' => [ 'status' => '1', 'archive' => 0, 'parent_id' => null ],
|
||
] );
|
||
|
||
if ( \S::is_array_fix( $rows ) ) {
|
||
foreach ( $rows as $productId ) {
|
||
$product = \shop\Product::getFromCache( $productId, $lang_id );
|
||
|
||
if ( is_array( $product->product_combinations ) && count( $product->product_combinations ) ) {
|
||
foreach ( $product->product_combinations as $productCombination ) {
|
||
if ( $productCombination->quantity !== null || $productCombination->stock_0_buy ) {
|
||
$this->appendCombinationToXml( $doc, $channelNode, $product, $productCombination, $domainPrefix, $url );
|
||
}
|
||
}
|
||
} else {
|
||
$this->appendProductToXml( $doc, $channelNode, $product, $domainPrefix, $url );
|
||
}
|
||
}
|
||
}
|
||
|
||
file_put_contents( '../google-feed.xml', $doc->saveXML() );
|
||
}
|
||
|
||
/**
|
||
* Dodaje kombinację produktu do XML feed.
|
||
*/
|
||
private function appendCombinationToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, $combination, string $domainPrefix, string $url): void
|
||
{
|
||
$itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:id', $combination->id ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product->id ) );
|
||
|
||
for ( $l = 0; $l <= 4; $l++ ) {
|
||
$label = 'custom_label_' . $l;
|
||
if ( $product->$label ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:' . $label, $product->$label ) );
|
||
}
|
||
}
|
||
|
||
$title = $product->language['xml_name'] ?: $product->language['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&', $title ) . ' - ' . $product->generateSubtitleFromAttributes( $combination->permutation_hash ) ) );
|
||
|
||
$gtin = $product->ean ?: $this->generateEAN( $product->id );
|
||
$itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );
|
||
|
||
$desc = $product->language['short_description'] ?: $product->language['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
|
||
|
||
if ( $product->language['seo_link'] ) {
|
||
$link = $domainPrefix . '://' . $url . '/' . \S::seo( $product->language['seo_link'] ) . '/' . str_replace( '|', '/', $combination->permutation_hash );
|
||
} else {
|
||
$link = $domainPrefix . '://' . $url . '/p-' . $product->id . '-' . \S::seo( $product->language['name'] ) . '/' . str_replace( '|', '/', $combination->permutation_hash );
|
||
}
|
||
$itemNode->appendChild( $doc->createElement( 'link', $link ) );
|
||
|
||
$this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );
|
||
|
||
if ( $combination->quantity !== null ) {
|
||
if ( $combination->quantity > 0 ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $combination->quantity ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', $combination->stock_0_buy ? 'in stock' : 'out of stock' ) );
|
||
}
|
||
} else {
|
||
if ( $product->quantity > 0 ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product->quantity ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', $product->stock_0_buy ? 'in stock' : 'out of stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product->stock_0_buy ? 999 : 0 ) );
|
||
}
|
||
}
|
||
|
||
if ( $combination->price_brutto ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $combination->price_brutto . ' PLN' ) );
|
||
if ( $combination->price_brutto_promo ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $combination->price_brutto_promo . ' PLN' ) );
|
||
}
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $product->price_brutto . ' PLN' ) );
|
||
if ( $product->price_brutto_promo ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $product->price_brutto_promo . ' PLN' ) );
|
||
}
|
||
}
|
||
|
||
$this->appendShippingToXml( $doc, $itemNode, $product );
|
||
}
|
||
|
||
/**
|
||
* Dodaje produkt (bez kombinacji) do XML feed.
|
||
*/
|
||
private function appendProductToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, string $domainPrefix, string $url): void
|
||
{
|
||
$itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:id', $product->id ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product->id ) );
|
||
|
||
if ( $product->google_xml_label ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:custom_label_0', $product->google_xml_label ) );
|
||
}
|
||
|
||
$title = $product->language['xml_name'] ?: $product->language['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&', $title ) ) );
|
||
|
||
$gtin = $product->ean ?: $this->generateEAN( $product->id );
|
||
$itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );
|
||
|
||
$desc = $product->language['short_description'] ?: $product->language['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
|
||
|
||
if ( $product->language['seo_link'] ) {
|
||
$link = $domainPrefix . '://' . $url . '/' . \S::seo( $product->language['seo_link'] );
|
||
} else {
|
||
$link = $domainPrefix . '://' . $url . '/p-' . $product->id . '-' . \S::seo( $product->language['name'] );
|
||
}
|
||
$itemNode->appendChild( $doc->createElement( 'link', $link ) );
|
||
|
||
for ( $l = 0; $l <= 4; $l++ ) {
|
||
$label = 'custom_label_' . $l;
|
||
if ( $product->$label ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:' . $label, $product->$label ) );
|
||
}
|
||
}
|
||
|
||
$this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );
|
||
|
||
if ( $product->quantity ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product->quantity ) );
|
||
} else {
|
||
if ( $product->stock_0_buy ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', 999 ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'out of stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', 0 ) );
|
||
}
|
||
}
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $product->price_brutto . ' PLN' ) );
|
||
if ( $product->price_brutto_promo ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $product->price_brutto_promo . ' PLN' ) );
|
||
}
|
||
|
||
$this->appendShippingToXml( $doc, $itemNode, $product );
|
||
}
|
||
|
||
/**
|
||
* Dodaje zdjęcia do node XML.
|
||
*/
|
||
private function appendImagesToXml(\DOMDocument $doc, \DOMElement $itemNode, $product, string $domainPrefix, string $url): void
|
||
{
|
||
if ( isset( $product->images[0] ) ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:image_link', $domainPrefix . '://' . $url . $product->images[0]['src'] ) );
|
||
}
|
||
if ( count( $product->images ) > 1 ) {
|
||
for ( $i = 1; $i < count( $product->images ); ++$i ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:additional_image_link', $domainPrefix . '://' . $url . $product->images[$i]['src'] ) );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Dodaje sekcję shipping do node XML.
|
||
*/
|
||
private function appendShippingToXml(\DOMDocument $doc, \DOMElement $itemNode, $product): void
|
||
{
|
||
$shippingNode = $itemNode->appendChild( $doc->createElement( 'g:shipping' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:country', 'PL' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:service', '1 dzień roboczy' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:price',
|
||
( new \Domain\Transport\TransportRepository( $this->db ) )->lowestTransportPrice( (int) $product->wp ) . ' PLN'
|
||
) );
|
||
}
|
||
|
||
// ─── Koniec kroku 5 ──────────────────────────────────────────────
|
||
|
||
/**
|
||
* Aktualizuje ceny kombinacji produktu na podstawie cen brutto bazowych.
|
||
* Wywoływane z saveProduct() i z cron.php.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param float $priceBrutto Cena brutto bazowa
|
||
* @param float $vat Stawka VAT
|
||
* @param float|null $priceBruttoPromo Cena promo brutto bazowa (null = brak)
|
||
*/
|
||
public function updateCombinationPricesFromBase(int $productId, $priceBrutto, $vat, $priceBruttoPromo): void
|
||
{
|
||
$priceBrutto = (float) $priceBrutto;
|
||
$vat = (float) $vat;
|
||
$priceBruttoPromo = $priceBruttoPromo ? (float) $priceBruttoPromo : null;
|
||
|
||
$combinations = $this->db->query(
|
||
'SELECT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
|
||
. 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
|
||
. 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
|
||
[ ':product_id' => $productId ]
|
||
);
|
||
|
||
if ( !$combinations ) {
|
||
return;
|
||
}
|
||
|
||
$rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
|
||
foreach ( $rows as $row ) {
|
||
$combBrutto = $priceBrutto;
|
||
$combBruttoPromo = $priceBruttoPromo;
|
||
|
||
$values = $this->db->query(
|
||
'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
|
||
. 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
|
||
[ ':product_id' => $row['id'] ]
|
||
);
|
||
|
||
if ( $values ) {
|
||
foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
|
||
$combBrutto += $value['impact_on_the_price'];
|
||
if ( $combBruttoPromo !== null ) {
|
||
$combBruttoPromo += $value['impact_on_the_price'];
|
||
} else {
|
||
$combBruttoPromo = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
$combNetto = \S::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
|
||
$combNettoPromo = $combBruttoPromo !== null
|
||
? \S::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
|
||
: null;
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $combNetto,
|
||
'price_brutto' => $combBrutto,
|
||
'price_netto_promo' => $combNettoPromo,
|
||
'price_brutto_promo' => $combBruttoPromo
|
||
], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje ceny kombinacji produktu uwzględniając wpływ na cenę (impact_on_the_price).
|
||
* Wersja prywatna — przyjmuje ceny netto (dla mass_edit / applyDiscountPercent).
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param float $priceNetto Cena netto bazowa
|
||
* @param float $vat Stawka VAT
|
||
* @param float|null $priceNettoPromo Cena promo netto bazowa (null = brak)
|
||
*/
|
||
private function updateCombinationPrices(int $productId, float $priceNetto, float $vat, ?float $priceNettoPromo): void
|
||
{
|
||
$priceBrutto = \S::normalize_decimal( $priceNetto * ( 100 + $vat ) / 100, 2 );
|
||
$priceBruttoPromo = $priceNettoPromo !== null
|
||
? \S::normalize_decimal( $priceNettoPromo * ( 100 + $vat ) / 100, 2 )
|
||
: null;
|
||
|
||
$combinations = $this->db->query(
|
||
'SELECT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
|
||
. 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
|
||
. 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
|
||
[ ':product_id' => $productId ]
|
||
);
|
||
|
||
if ( !$combinations ) {
|
||
return;
|
||
}
|
||
|
||
$rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
|
||
foreach ( $rows as $row ) {
|
||
$combBrutto = $priceBrutto;
|
||
$combBruttoPromo = $priceBruttoPromo;
|
||
|
||
$values = $this->db->query(
|
||
'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
|
||
. 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
|
||
[ ':product_id' => $row['id'] ]
|
||
);
|
||
|
||
if ( $values ) {
|
||
foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
|
||
$combBrutto += $value['impact_on_the_price'];
|
||
if ( $combBruttoPromo !== null ) {
|
||
$combBruttoPromo += $value['impact_on_the_price'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$combNetto = \S::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
|
||
$combNettoPromo = $combBruttoPromo !== null
|
||
? \S::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
|
||
: null;
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $combNetto,
|
||
'price_brutto' => $combBrutto,
|
||
'price_netto_promo' => $combNettoPromo,
|
||
'price_brutto_promo' => $combBruttoPromo
|
||
], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
}
|