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>, 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 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 ]; } /** * Aktualizuje ceny kombinacji produktu uwzględniając wpływ na cenę (impact_on_the_price). * * @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'] ] ); } } }