Add view classes for articles, banners, languages, menu, newsletter, containers, shop categories, clients, payment methods, products, and search

- Created `Articles` class for rendering article views including full articles, miniature lists, and news sections.
- Added `Banners` class for handling banner displays.
- Introduced `Languages` class for rendering language options.
- Implemented `Menu` class for rendering page and menu structures.
- Developed `Newsletter` class for newsletter rendering.
- Created `Scontainers` class for rendering specific containers.
- Added `ShopCategory` class for managing shop category views and pagination.
- Implemented `ShopClient` class for client-related views including address management and login forms.
- Created `ShopPaymentMethod` class for displaying payment methods in the basket.
- Added `ShopProduct` class for generating product URLs.
- Introduced `ShopSearch` class for rendering a simple search form.
- Added `.htaccess` file in the plugins directory to enhance security by restricting access to sensitive files and directories.
This commit is contained in:
2026-02-21 23:00:54 +01:00
parent a605e0f4ad
commit fc45bbf20e
322 changed files with 35722 additions and 21849 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,399 @@
<?php
namespace Domain\Banner;
/**
* Repository odpowiedzialny za dostęp do danych banerów
*/
class BannerRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Pobiera baner po ID wraz z tłumaczeniami
*
* @param int $bannerId ID banera
* @return array|null Dane banera lub null
*/
public function find(int $bannerId): ?array
{
$banner = $this->db->get('pp_banners', '*', ['id' => $bannerId]);
if (!$banner) {
return null;
}
$results = $this->db->select('pp_banners_langs', '*', [
'id_banner' => $bannerId,
'ORDER' => ['id' => 'ASC'],
]);
if (is_array($results)) {
foreach ($results as $row) {
$banner['languages'][$row['id_lang']] = $row;
}
}
return $banner;
}
/**
* Usuwa baner
*
* @param int $bannerId ID banera
* @return bool Czy usunięto
*/
public function delete(int $bannerId): bool
{
$result = $this->db->delete('pp_banners', ['id' => $bannerId]);
return $result !== false;
}
/**
* Zapisuje baner (insert lub update)
*
* @param array $data Dane banera (obsługuje format z FormRequestHandler lub stary format)
* @return int|false ID banera lub false
*/
public function save(array $data)
{
$bannerId = $data['id'] ?? null;
// Obsługa obu formatów: nowy (int) i stary ('on'/string)
$status = $data['status'] ?? 0;
if ($status === 'on') {
$status = 1;
}
$homePage = $data['home_page'] ?? 0;
if ($homePage === 'on') {
$homePage = 1;
}
$bannerData = [
'name' => $data['name'],
'status' => (int)$status,
'date_start' => !empty($data['date_start']) ? $data['date_start'] : null,
'date_end' => !empty($data['date_end']) ? $data['date_end'] : null,
'home_page' => (int)$homePage,
];
if (!$bannerId) {
$this->db->insert('pp_banners', $bannerData);
$bannerId = $this->db->id();
if (!$bannerId) {
return false;
}
} else {
$this->db->update('pp_banners', $bannerData, ['id' => (int)$bannerId]);
}
// Obsługa danych językowych - nowy format (translations) lub stary (src/url/html/text)
if (isset($data['translations']) && is_array($data['translations'])) {
// Nowy format z FormRequestHandler
$this->saveTranslationsFromArray($bannerId, $data['translations']);
} elseif (isset($data['src']) && is_array($data['src'])) {
// Stary format (backward compatibility)
$this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return (int)$bannerId;
}
/**
* Zwraca liste banerow do panelu admin z filtrowaniem, sortowaniem i paginacja.
*
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$sortColumn = trim($sortColumn);
$sortDir = strtoupper(trim($sortDir));
$allowedSortColumns = [
'name' => 'b.name',
'status' => 'b.status',
'home_page' => 'b.home_page',
'date_start' => 'b.date_start',
'date_end' => 'b.date_end',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'b.name';
$sortDir = $sortDir === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1=1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
if ($name !== '') {
$where[] = 'b.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
if (($filters['status'] ?? '') !== '' && ($filters['status'] === '0' || $filters['status'] === '1')) {
$where[] = 'b.status = :status';
$params[':status'] = (int)$filters['status'];
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_banners AS b
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
b.id,
b.name,
b.status,
b.home_page,
b.date_start,
b.date_end
FROM pp_banners AS b
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, b.id {$sortDir}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
$items = is_array($items) ? $items : [];
if (!empty($items)) {
$bannerIds = array_map('intval', array_column($items, 'id'));
$thumbByBannerId = $this->fetchThumbnailsByBannerIds($bannerIds);
foreach ($items as &$item) {
$item['thumbnail_src'] = $thumbByBannerId[(int)($item['id'] ?? 0)] ?? '';
}
unset($item);
}
return [
'items' => $items,
'total' => $total,
];
}
/**
* Pobiera pierwsza dostepna sciezke obrazka (src) dla kazdego banera.
*
* @param array<int, int> $bannerIds
* @return array<int, string> [id_banner => src]
*/
private function fetchThumbnailsByBannerIds(array $bannerIds): array
{
$bannerIds = array_values(array_unique(array_filter($bannerIds, static function ($id): bool {
return (int)$id > 0;
})));
if (empty($bannerIds)) {
return [];
}
$in = [];
$params = [];
foreach ($bannerIds as $index => $bannerId) {
$placeholder = ':id' . $index;
$in[] = $placeholder;
$params[$placeholder] = (int)$bannerId;
}
$sql = '
SELECT id_banner, src
FROM pp_banners_langs
WHERE id_banner IN (' . implode(', ', $in) . ')
AND src IS NOT NULL
AND src <> \'\'
ORDER BY id_lang ASC, id ASC
';
$stmt = $this->db->query($sql, $params);
$rows = $stmt ? $stmt->fetchAll() : [];
if (!is_array($rows)) {
return [];
}
$thumbByBannerId = [];
foreach ($rows as $row) {
$bannerId = (int)($row['id_banner'] ?? 0);
if ($bannerId <= 0 || isset($thumbByBannerId[$bannerId])) {
continue;
}
$src = trim((string)($row['src'] ?? ''));
if ($src !== '') {
$thumbByBannerId[$bannerId] = $src;
}
}
return $thumbByBannerId;
}
/**
* Zapisuje tłumaczenia banera (stary format - zachowano dla kompatybilności)
*/
private function saveTranslations(int $bannerId, array $src, array $url, array $html, array $text): void
{
foreach ($src as $langId => $val) {
$this->upsertTranslation($bannerId, $langId, [
'src' => $src[$langId] ?? '',
'url' => $url[$langId] ?? '',
'html' => $html[$langId] ?? '',
'text' => $text[$langId] ?? '',
]);
}
}
/**
* Zapisuje tłumaczenia banera z nowego formatu (z FormRequestHandler)
* Format: [lang_id => [field => value]]
*/
private function saveTranslationsFromArray(int $bannerId, array $translations): void
{
foreach ($translations as $langId => $fields) {
$this->upsertTranslation($bannerId, $langId, [
'src' => $fields['src'] ?? '',
'url' => $fields['url'] ?? '',
'html' => $fields['html'] ?? '',
'text' => $fields['text'] ?? '',
]);
}
}
/**
* Upsert tlumaczenia banera.
* Aktualizuje wszystkie rekordy dla pary id_banner + id_lang,
* co usuwa problem z historycznymi duplikatami.
*/
private function upsertTranslation(int $bannerId, $langId, array $fields): void
{
$where = ['AND' => ['id_banner' => $bannerId, 'id_lang' => $langId]];
$translationData = [
'id_banner' => $bannerId,
'id_lang' => $langId,
'src' => $fields['src'] ?? '',
'url' => $fields['url'] ?? '',
'html' => $fields['html'] ?? '',
'text' => $fields['text'] ?? '',
];
$hasExisting = (int)$this->db->count('pp_banners_langs', $where) > 0;
if ($hasExisting) {
$this->db->update('pp_banners_langs', $translationData, $where);
return;
}
$this->db->insert('pp_banners_langs', $translationData);
}
// ─── Frontend methods ───────────────────────────────────────────
/**
* Pobiera aktywne banery (home_page = 0) z filtrowaniem dat, z Redis cache.
* Zwraca dane w formacie zgodnym z szablonami: $banner['languages'] = płaski wiersz.
*/
public function banners(string $langId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "BannerRepository::banners:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$today = date('Y-m-d');
$stmt = $this->db->query(
"SELECT id, name FROM pp_banners "
. "WHERE status = 1 "
. "AND (date_start <= :today1 OR date_start IS NULL) "
. "AND (date_end >= :today2 OR date_end IS NULL) "
. "AND home_page = 0",
[':today1' => $today, ':today2' => $today]
);
$results = $stmt ? $stmt->fetchAll() : [];
$banners = null;
if (is_array($results) && !empty($results)) {
foreach ($results as $row) {
$row['languages'] = $this->db->get('pp_banners_langs', '*', [
'AND' => ['id_banner' => (int)$row['id'], 'id_lang' => $langId]
]);
$banners[] = $row;
}
}
$cacheHandler->set($cacheKey, $banners);
return $banners;
}
/**
* Pobiera glowny baner (home_page = 1) z filtrowaniem dat, z Redis cache.
* Zwraca dane w formacie zgodnym z szablonami: $banner['languages'] = plaski wiersz.
*/
public function mainBanner(string $langId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "BannerRepository::mainBanner:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$today = date('Y-m-d');
$stmt = $this->db->query(
"SELECT * FROM pp_banners "
. "WHERE status = 1 "
. "AND (date_start <= :today1 OR date_start IS NULL) "
. "AND (date_end >= :today2 OR date_end IS NULL) "
. "AND home_page = 1 "
. "ORDER BY date_end ASC "
. "LIMIT 1",
[':today1' => $today, ':today2' => $today]
);
$results = $stmt ? $stmt->fetchAll() : [];
$banner = null;
if (is_array($results) && !empty($results)) {
$banner = $results[0];
$banner['languages'] = $this->db->get('pp_banners_langs', '*', [
'AND' => ['id_banner' => (int)$banner['id'], 'id_lang' => $langId]
]);
}
$cacheHandler->set($cacheKey, $banner);
return $banner;
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Domain\Basket;
class BasketCalculator
{
public static function summaryWp($basket)
{
$wp = 0;
if (is_array($basket)) {
foreach ($basket as $product) {
$wp += $product['wp'] * $product['quantity'];
}
}
return $wp;
}
public static function countProductsText($count)
{
$count = (int)$count;
if ($count === 1) {
return $count . ' produkt';
}
if ($count >= 2 && $count <= 4) {
return $count . ' produkty';
}
return $count . ' produktów';
}
/**
* @param string|null $langId Language ID (falls back to global $lang_id if null)
* @param \Domain\Product\ProductRepository|null $productRepo (falls back to $GLOBALS['mdb'] if null)
*/
public static function summaryPrice($basket, $coupon = null, $langId = null, $productRepo = null)
{
if ($langId === null) {
global $lang_id;
$langId = $lang_id;
}
if ($productRepo === null) {
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
}
$summary = 0;
if (is_array($basket)) {
foreach ($basket as $position) {
$product = $productRepo->findCached((int)$position['product-id'], $langId);
$product_price_tmp = self::calculateBasketProductPrice(
(float)($product['price_brutto_promo'] ?? 0),
(float)($product['price_brutto'] ?? 0),
$coupon,
$position,
$productRepo
);
$summary += $product_price_tmp['price_new'] * $position['quantity'];
}
}
return \Shared\Helpers\Helpers::normalize_decimal($summary);
}
public static function countProducts($basket)
{
$count = 0;
if (is_array($basket)) {
foreach ($basket as $product) {
$count += $product['quantity'];
}
}
return $count;
}
public static function validateBasket($basket)
{
if ( !is_array( $basket ) )
return array();
return $basket;
}
public static function checkProductQuantityInStock($basket, bool $message = false)
{
if ( !is_array( $basket ) || empty( $basket ) )
return false;
$result = false;
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
foreach ( $basket as $key => $val )
{
$permutation = null;
if ( isset( $val['parent_id'] ) and (int)$val['parent_id'] and isset( $val['product-id'] ) )
$permutation = $productRepo->getProductPermutationHash( (int)$val['product-id'] );
if ( !$permutation and isset( $val['attributes'] ) and is_array( $val['attributes'] ) and count( $val['attributes'] ) )
$permutation = implode( '|', $val['attributes'] );
$quantity_options = $productRepo->getProductPermutationQuantityOptions(
$val['parent_id'] ? $val['parent_id'] : $val['product-id'],
$permutation
);
if (
(int)$basket[ $key ][ 'quantity' ] < 1
and ( (int)$quantity_options['quantity'] > 0 or (int)$quantity_options['stock_0_buy'] === 1 )
)
{
$basket[ $key ][ 'quantity' ] = 1;
$result = true;
}
if ( ( $val[ 'quantity' ] > $quantity_options['quantity'] ) and !$quantity_options['stock_0_buy'] )
{
$basket[ $key ][ 'quantity' ] = $quantity_options['quantity'];
if ( $message )
\Shared\Helpers\Helpers::error( 'Ilość jednego lub więcej produktów została zmniejszona z powodu niestarczających stanów magazynowych. Sprawdź proszę koszyk.' );
$result = true;
}
}
\Shared\Helpers\Helpers::set_session( 'basket', $basket );
return $result;
}
/**
* Calculate product price in basket (with coupon + promotion discounts).
* Migrated from \shop\Product::calculate_basket_product_price()
*/
/**
* @param \Domain\Product\ProductRepository|null $productRepo (falls back to $GLOBALS['mdb'] if null)
*/
public static function calculateBasketProductPrice( float $price_brutto_promo, float $price_brutto, $coupon, $basket_position, $productRepo = null )
{
if ($productRepo === null) {
$productRepo = new \Domain\Product\ProductRepository($GLOBALS['mdb']);
}
// Produkty przecenione
if ( $price_brutto_promo )
{
$price['price'] = $price_brutto;
$price['price_new'] = $price_brutto_promo;
$coupon_type = is_array($coupon) ? ($coupon['type'] ?? null) : (is_object($coupon) ? $coupon->type : null);
$coupon_include_discounted = is_array($coupon) ? ($coupon['include_discounted_product'] ?? null) : (is_object($coupon) ? $coupon->include_discounted_product : null);
$coupon_categories = is_array($coupon) ? ($coupon['categories'] ?? null) : (is_object($coupon) ? $coupon->categories : null);
$coupon_amount = is_array($coupon) ? ($coupon['amount'] ?? 0) : (is_object($coupon) ? $coupon->amount : 0);
if ( $coupon_type && $coupon_include_discounted )
{
if ( $coupon_categories != null )
{
$cats = is_string($coupon_categories) ? json_decode($coupon_categories) : $coupon_categories;
$product_categories = $productRepo->productCategories( (int)$basket_position['parent_id'] ? (int)$basket_position['parent_id'] : (int)$basket_position['product-id'] );
if ( is_array( $cats ) ) foreach ( $cats as $category_tmp )
{
if ( in_array( $category_tmp, $product_categories ) )
{
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $price['price_new'] - $price['price_new'] * $coupon_amount / 100 );
break;
}
}
}
else
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $price['price_new'] - $price['price_new'] * $coupon_amount / 100 );
if ( $basket_position['discount_amount'] && $basket_position['discount_include_coupon'] && $basket_position['include_product_promo'] )
{
if ( $basket_position['discount_type'] == 3 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $basket_position['discount_amount'] );
if ( $basket_position['discount_type'] == 1 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal($price['price_new'] - $price['price_new'] * $basket_position['discount_amount'] / 100 );
}
}
else
{
if ( $basket_position['discount_amount'] && $basket_position['include_product_promo'] )
{
if ( $basket_position['discount_type'] == 3 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $basket_position['discount_amount'] );
if ( $basket_position['discount_type'] == 1 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal($price['price_new'] - $price['price_new'] * $basket_position['discount_amount'] / 100 );
}
}
}
// Produkt nieprzeceniony
else
{
$price['price'] = $price_brutto;
$price['price_new'] = $price_brutto;
$coupon_type = is_array($coupon) ? ($coupon['type'] ?? null) : (is_object($coupon) ? $coupon->type : null);
$coupon_categories = is_array($coupon) ? ($coupon['categories'] ?? null) : (is_object($coupon) ? $coupon->categories : null);
$coupon_amount = is_array($coupon) ? ($coupon['amount'] ?? 0) : (is_object($coupon) ? $coupon->amount : 0);
if ( $coupon_type )
{
if ( $coupon_categories != null )
{
$cats = is_string($coupon_categories) ? json_decode($coupon_categories) : $coupon_categories;
$product_categories = $productRepo->productCategories( $basket_position['parent_id'] ? $basket_position['parent_id'] : $basket_position['product-id'] );
if ( is_array( $cats ) ) foreach ( $cats as $category_tmp )
{
if ( in_array( $category_tmp, $product_categories ) )
{
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $price['price_new'] - $price['price_new'] * $coupon_amount / 100 );
break;
}
}
}
else
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal($price['price'] - $price['price'] * $coupon_amount / 100 );
if ( $basket_position['discount_amount'] && $basket_position['discount_include_coupon'] && $basket_position['include_product_promo'] )
{
if ( $basket_position['discount_type'] == 3 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $basket_position['discount_amount'] );
if ( $basket_position['discount_type'] == 1 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal($price['price'] - $price['price'] * $basket_position['discount_amount'] / 100 );
}
}
else
{
if ( $basket_position['discount_amount'] )
{
if ( $basket_position['discount_type'] == 3 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal( $basket_position['discount_amount'] );
if ( $basket_position['discount_type'] == 1 )
$price['price_new'] = \Shared\Helpers\Helpers::normalize_decimal($price['price'] - $price['price'] * $basket_position['discount_amount'] / 100 );
}
}
}
return $price;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Domain\Cache;
/**
* Repozytorium zarządzania cache (katalogi + Redis)
*
* Wyodrębniona logika czyszczenia cache z wstrzykiwanymi zależnościami.
* Obecnie nie używane przez SettingsController (cache czyszczony bezpośrednio).
* Przygotowane na przyszłe użycie w innych kontrolerach.
*/
class CacheRepository
{
private $redisConnection;
private $basePath;
/**
* @param \Shared\Cache\RedisConnection $redisConnection Połączenie z Redis (nullable)
* @param string $basePath Ścieżka bazowa do katalogów cache
*/
public function __construct(
?\Shared\Cache\RedisConnection $redisConnection = null,
string $basePath = '../'
) {
$this->redisConnection = $redisConnection;
$this->basePath = $basePath;
}
/**
* Czyszczenie całego cache (katalogi + Redis)
*
* @return array ['success' => bool, 'message' => string]
*/
public function clearCache(): array
{
\Shared\Helpers\Helpers::delete_dir( $this->basePath . 'temp/' );
\Shared\Helpers\Helpers::delete_dir( $this->basePath . 'thumbs/' );
$redisCleared = false;
if ( $this->redisConnection ) {
$redis = $this->redisConnection->getConnection();
if ( $redis ) {
$redis->flushAll();
$redisCleared = true;
}
}
return [
'success' => true,
'message' => 'Cache został wyczyszczony.',
'redisCleared' => $redisCleared
];
}
}

View File

@@ -0,0 +1,772 @@
<?php
namespace Domain\Category;
class CategoryRepository
{
private $db;
private const SORT_TYPES = [
0 => 'data dodania - najstarsze na początku',
1 => 'data dodania - najnowsze na początku',
2 => 'data modyfikacji - rosnąco',
3 => 'data mofyfikacji - malejąco',
4 => 'ręczne',
5 => 'alfabetycznie - A - Z',
6 => 'alfabetycznie - Z - A',
];
private const SORT_ORDER_SQL = [
0 => 'q1.date_add ASC',
1 => 'q1.date_add DESC',
2 => 'q1.date_modify ASC',
3 => 'q1.date_modify DESC',
4 => 'q1.o ASC',
5 => 'q1.name ASC',
6 => 'q1.name DESC',
];
private const PRODUCTS_PER_PAGE = 12;
private const LANGUAGE_FALLBACK_NAME_SQL = '(CASE '
. 'WHEN copy_from IS NULL THEN name '
. 'WHEN copy_from IS NOT NULL THEN ('
. 'SELECT name FROM pp_shop_products_langs '
. 'WHERE lang_id = pspl.copy_from AND product_id = psp.id'
. ') '
. 'END) AS name';
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array<int, string>
*/
public function sortTypes(): array
{
return self::SORT_TYPES;
}
/**
* @return array<int, array<string, mixed>>
*/
public function subcategories($parentId = null): array
{
$rows = $this->db->select('pp_shop_categories', ['id'], [
'parent_id' => $this->toNullableInt($parentId),
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$categories = [];
foreach ($rows as $row) {
$categoryId = (int)($row['id'] ?? 0);
if ($categoryId <= 0) {
continue;
}
$details = $this->categoryDetails($categoryId);
if (!empty($details)) {
$categories[] = $details;
}
}
return $categories;
}
/**
* @return array<string, mixed>
*/
public function categoryDetails($categoryId = ''): array
{
$id = (int)$categoryId;
if ($id <= 0) {
return $this->defaultCategory();
}
$category = $this->db->get('pp_shop_categories', '*', ['id' => $id]);
if (!is_array($category)) {
return $this->defaultCategory();
}
$category['id'] = (int)($category['id'] ?? 0);
$category['status'] = $this->toSwitchValue($category['status'] ?? 0);
$category['view_subcategories'] = $this->toSwitchValue($category['view_subcategories'] ?? 0);
$category['sort_type'] = (int)($category['sort_type'] ?? 0);
$category['parent_id'] = $this->toNullableInt($category['parent_id'] ?? null);
$translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $id]);
$category['languages'] = [];
if (is_array($translations)) {
foreach ($translations as $translation) {
$langId = (string)($translation['lang_id'] ?? '');
if ($langId === '') {
continue;
}
$category['languages'][$langId] = $translation;
}
}
return $category;
}
/**
* @return array<int, array<string, mixed>>
*/
public function categoryProducts(int $categoryId): array
{
if ($categoryId <= 0) {
return [];
}
$rows = $this->db->query(
'SELECT '
. 'pspc.product_id, pspc.o, psp.status '
. 'FROM '
. 'pp_shop_products_categories AS pspc '
. 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id '
. 'WHERE '
. 'pspc.category_id = :category_id '
. 'ORDER BY '
. 'pspc.o ASC',
[
':category_id' => $categoryId,
]
);
if (!$rows) {
return [];
}
$products = [];
foreach ($rows->fetchAll() as $row) {
$productId = (int)($row['product_id'] ?? 0);
if ($productId <= 0) {
continue;
}
$products[] = [
'product_id' => $productId,
'o' => (int)($row['o'] ?? 0),
'status' => $this->toSwitchValue($row['status'] ?? 0),
'name' => $this->productName($productId),
];
}
return $products;
}
public function categoryDelete($categoryId): bool
{
$id = (int)$categoryId;
if ($id <= 0) {
return false;
}
if ((int)$this->db->count('pp_shop_categories', ['parent_id' => $id]) > 0) {
return false;
}
$deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]);
if ($deleted) {
$this->refreshCategoryArtifacts();
}
return $deleted;
}
public function saveCategoriesOrder($categories): bool
{
if (!is_array($categories)) {
return false;
}
$this->db->update('pp_shop_categories', ['o' => 0]);
$position = 0;
foreach ($categories as $item) {
$itemId = (int)($item['item_id'] ?? 0);
if ($itemId <= 0) {
continue;
}
$position++;
$parentId = $this->toNullableInt($item['parent_id'] ?? null);
$this->db->update('pp_shop_categories', [
'o' => $position,
'parent_id' => $parentId,
], [
'id' => $itemId,
]);
}
$this->refreshCategoryArtifacts();
return true;
}
public function saveProductOrder($categoryId, $products): bool
{
$id = (int)$categoryId;
if ($id <= 0 || !is_array($products)) {
return false;
}
$this->db->update('pp_shop_products_categories', ['o' => 0], ['category_id' => $id]);
$position = 0;
foreach ($products as $item) {
$productId = (int)($item['item_id'] ?? 0);
if ($productId <= 0) {
continue;
}
$position++;
$this->db->update('pp_shop_products_categories', ['o' => $position], [
'AND' => [
'category_id' => $id,
'product_id' => $productId,
],
]);
}
return true;
}
/**
* @param array<string, mixed> $data
*/
public function save(array $data): ?int
{
$categoryId = (int)($data['id'] ?? 0);
$parentId = $this->toNullableInt($data['parent_id'] ?? null);
$row = [
'status' => $this->toSwitchValue($data['status'] ?? 0),
'parent_id' => $parentId,
'sort_type' => (int)($data['sort_type'] ?? 0),
'view_subcategories' => $this->toSwitchValue($data['view_subcategories'] ?? 0),
];
if ($categoryId <= 0) {
$row['o'] = $this->maxOrder() + 1;
$this->db->insert('pp_shop_categories', $row);
$categoryId = (int)$this->db->id();
if ($categoryId <= 0) {
return null;
}
} else {
$this->db->update('pp_shop_categories', $row, ['id' => $categoryId]);
}
$title = is_array($data['title'] ?? null) ? $data['title'] : [];
$text = is_array($data['text'] ?? null) ? $data['text'] : [];
$textHidden = is_array($data['text_hidden'] ?? null) ? $data['text_hidden'] : [];
$seoLink = is_array($data['seo_link'] ?? null) ? $data['seo_link'] : [];
$metaTitle = is_array($data['meta_title'] ?? null) ? $data['meta_title'] : [];
$metaDescription = is_array($data['meta_description'] ?? null) ? $data['meta_description'] : [];
$metaKeywords = is_array($data['meta_keywords'] ?? null) ? $data['meta_keywords'] : [];
$noindex = is_array($data['noindex'] ?? null) ? $data['noindex'] : [];
$categoryTitle = is_array($data['category_title'] ?? null) ? $data['category_title'] : [];
$additionalText = is_array($data['additional_text'] ?? null) ? $data['additional_text'] : [];
foreach ($title as $langId => $langTitle) {
$translationData = [
'lang_id' => (string)$langId,
'title' => $this->toNullableString($langTitle),
'text' => $this->toNullableString($text[$langId] ?? null),
'text_hidden' => $this->toNullableString($textHidden[$langId] ?? null),
'meta_description' => $this->toNullableString($metaDescription[$langId] ?? null),
'meta_keywords' => $this->toNullableString($metaKeywords[$langId] ?? null),
'meta_title' => $this->toNullableString($metaTitle[$langId] ?? null),
'seo_link' => $this->normalizeSeoLink($seoLink[$langId] ?? null),
'noindex' => (int)($noindex[$langId] ?? 0),
'category_title' => $this->toNullableString($categoryTitle[$langId] ?? null),
'additional_text' => $this->toNullableString($additionalText[$langId] ?? null),
];
$translationId = $this->db->get('pp_shop_categories_langs', 'id', [
'AND' => [
'category_id' => $categoryId,
'lang_id' => (string)$langId,
],
]);
if ($translationId) {
$this->db->update('pp_shop_categories_langs', $translationData, ['id' => $translationId]);
continue;
}
$this->db->insert('pp_shop_categories_langs', array_merge($translationData, [
'category_id' => $categoryId,
]));
}
$this->refreshCategoryArtifacts();
return $categoryId;
}
public function categoryTitle(int $categoryId): string
{
if ($categoryId <= 0) {
return '';
}
$title = $this->db->select('pp_shop_categories_langs', [
'[><]pp_langs' => ['lang_id' => 'id'],
], 'title', [
'AND' => [
'category_id' => $categoryId,
'title[!]' => '',
],
'ORDER' => ['o' => 'ASC'],
'LIMIT' => 1,
]);
if (!is_array($title) || !isset($title[0])) {
return '';
}
return (string)$title[0];
}
// ===== Frontend methods =====
public function getCategorySort(int $categoryId): int
{
if ($categoryId <= 0) {
return 0;
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "get_category_sort:$categoryId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return (int)unserialize($objectData);
}
$sortType = (int)$this->db->get('pp_shop_categories', 'sort_type', ['id' => $categoryId]);
$cacheHandler->set($cacheKey, $sortType);
return $sortType;
}
public function categoryName(int $categoryId, string $langId): string
{
if ($categoryId <= 0 || $langId === '') {
return '';
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "category_name:{$langId}:{$categoryId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return (string)unserialize($objectData);
}
$name = $this->db->get('pp_shop_categories_langs', 'title', [
'AND' => [
'category_id' => $categoryId,
'lang_id' => $langId,
],
]);
$cacheHandler->set($cacheKey, $name);
return (string)$name;
}
public function categoryUrl(int $categoryId, string $langId): string
{
if ($categoryId <= 0) {
return '#';
}
$category = $this->frontCategoryDetails($categoryId, $langId);
if (empty($category)) {
return '#';
}
$url = !empty($category['language']['seo_link'])
? '/' . $category['language']['seo_link']
: '/k-' . $category['id'] . '-' . \Shared\Helpers\Helpers::seo($category['language']['title'] ?? '');
$currentLang = \Shared\Helpers\Helpers::get_session('current-lang');
$defaultLang = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage();
if ($currentLang != $defaultLang && $url !== '#') {
$url = '/' . $currentLang . $url;
}
return $url;
}
/**
* @return array<string, mixed>
*/
public function frontCategoryDetails(int $categoryId, string $langId): array
{
if ($categoryId <= 0) {
return [];
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "front_category_details:{$categoryId}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]);
if (!is_array($category)) {
return [];
}
$category['language'] = $this->db->get('pp_shop_categories_langs', '*', [
'AND' => [
'category_id' => $categoryId,
'lang_id' => $langId,
],
]);
$cacheHandler->set($cacheKey, $category);
return $category;
}
/**
* @return array<int, array<string, mixed>>
*/
public function categoriesTree(string $langId, ?int $parentId = null): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "categories_tree:{$langId}:{$parentId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$categories = [];
$results = $this->db->select('pp_shop_categories', 'id', [
'parent_id' => $parentId,
'ORDER' => ['o' => 'ASC'],
]);
if (is_array($results)) {
foreach ($results as $row) {
$category = $this->frontCategoryDetails((int)$row, $langId);
$category['categories'] = $this->categoriesTree($langId, (int)$row);
$categories[] = $category;
}
}
$cacheHandler->set($cacheKey, $categories);
return $categories;
}
/**
* @return array<int, mixed>
*/
public function blogCategoryProducts(int $categoryId, string $langId, int $limit): array
{
if ($categoryId <= 0 || $langId === '' || $limit <= 0) {
return [];
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "blog_category_products:{$categoryId}:{$langId}:{$limit}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$rows = $this->db->query(
'SELECT * FROM ('
. 'SELECT '
. 'psp.id, date_modify, date_add, o, '
. self::LANGUAGE_FALLBACK_NAME_SQL . ' '
. 'FROM '
. 'pp_shop_products_categories AS pspc '
. 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id '
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id '
. 'WHERE '
. 'status = 1 AND category_id = :category_id AND lang_id = :lang_id'
. ') AS q1 '
. 'WHERE '
. 'q1.name IS NOT NULL '
. 'ORDER BY '
. 'RAND() '
. 'LIMIT ' . (int)$limit,
[
':category_id' => $categoryId,
':lang_id' => $langId,
]
);
$output = [];
if ($rows) {
foreach ($rows->fetchAll() as $row) {
$output[] = $row['id'];
}
}
$cacheHandler->set($cacheKey, $output);
return $output;
}
public function categoryProductsCount(int $categoryId, string $langId): int
{
if ($categoryId <= 0 || $langId === '') {
return 0;
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "category_products_count:{$categoryId}:{$langId}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return (int)unserialize($objectData);
}
$rows = $this->db->query(
'SELECT COUNT(0) FROM ('
. 'SELECT '
. 'psp.id, '
. self::LANGUAGE_FALLBACK_NAME_SQL . ' '
. 'FROM '
. 'pp_shop_products_categories AS pspc '
. 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id '
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id '
. 'WHERE '
. 'status = 1 AND category_id = :category_id AND lang_id = :lang_id'
. ') AS q1 '
. 'WHERE '
. 'q1.name IS NOT NULL',
[
':category_id' => $categoryId,
':lang_id' => $langId,
]
);
$productsCount = 0;
if ($rows) {
$result = $rows->fetchAll();
$productsCount = (int)($result[0][0] ?? 0);
}
$cacheHandler->set($cacheKey, $productsCount);
return $productsCount;
}
/**
* @return array<int, mixed>
*/
public function productsId(int $categoryId, int $sortType, string $langId, int $productsLimit, int $from): array
{
if ($categoryId <= 0 || $langId === '') {
return [];
}
$order = self::SORT_ORDER_SQL[$sortType] ?? self::SORT_ORDER_SQL[0];
$today = date('Y-m-d');
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "products_id:{$categoryId}:{$sortType}:{$langId}:{$productsLimit}:{$from}";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$rows = $this->db->query(
'SELECT * FROM ('
. 'SELECT '
. 'psp.id, date_modify, date_add, o, '
. self::LANGUAGE_FALLBACK_NAME_SQL . ', '
. '(CASE '
. 'WHEN new_to_date >= :today THEN new_to_date '
. 'WHEN new_to_date < :today2 THEN null '
. 'END) AS new_to_date, '
. '(CASE WHEN (quantity + (SELECT IFNULL(SUM(quantity),0) FROM pp_shop_products WHERE parent_id = psp.id)) > 0 THEN 1 ELSE 0 END) AS total_quantity '
. 'FROM '
. 'pp_shop_products_categories AS pspc '
. 'INNER JOIN pp_shop_products AS psp ON psp.id = pspc.product_id '
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = pspc.product_id '
. 'WHERE '
. 'status = 1 AND category_id = :category_id AND lang_id = :lang_id'
. ') AS q1 '
. 'WHERE '
. 'q1.name IS NOT NULL '
. 'ORDER BY '
. $order . ' '
. 'LIMIT ' . (int)$from . ',' . (int)$productsLimit,
[
':category_id' => $categoryId,
':lang_id' => $langId,
':today' => $today,
':today2' => $today,
]
);
$output = [];
if ($rows) {
foreach ($rows->fetchAll() as $row) {
$output[] = $row['id'];
}
}
$cacheHandler->set($cacheKey, $output);
return $output;
}
/**
* @return array{products: array, ls: int}
*/
public function paginatedCategoryProducts(int $categoryId, int $sortType, string $langId, int $page): array
{
$count = $this->categoryProductsCount($categoryId, $langId);
if ($count <= 0) {
return ['products' => [], 'ls' => 0];
}
$totalPages = (int)ceil($count / self::PRODUCTS_PER_PAGE);
if ($page < 1) {
$page = 1;
} elseif ($page > $totalPages) {
$page = $totalPages;
}
$from = self::PRODUCTS_PER_PAGE * ($page - 1);
return [
'products' => $this->productsId($categoryId, $sortType, $langId, self::PRODUCTS_PER_PAGE, $from),
'ls' => $totalPages,
];
}
private function maxOrder(): int
{
return (int)$this->db->max('pp_shop_categories', 'o');
}
private function refreshCategoryArtifacts(): void
{
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
}
private function normalizeSeoLink($value): ?string
{
$seo = \Shared\Helpers\Helpers::seo((string)$value);
$seo = trim((string)$seo);
return $seo !== '' ? $seo : null;
}
private function toNullableString($value): ?string
{
$text = trim((string)$value);
return $text === '' ? null : $text;
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
private function toNullableInt($value): ?int
{
if ($value === null || $value === '' || (int)$value <= 0) {
return null;
}
return (int)$value;
}
private function defaultCategory(): array
{
return [
'id' => 0,
'status' => 1,
'parent_id' => null,
'sort_type' => 0,
'view_subcategories' => 0,
'languages' => [],
];
}
private function productName(int $productId): string
{
if ($productId <= 0) {
return '';
}
$defaultLang = $this->db->get('pp_langs', 'id', ['start' => 1]);
if (!$defaultLang) {
$defaultLang = 'pl';
}
$name = $this->db->get('pp_shop_products_langs', 'name', [
'AND' => [
'product_id' => $productId,
'lang_id' => (string)$defaultLang,
],
]);
if ($name !== false && $name !== null && trim((string)$name) !== '') {
return (string)$name;
}
$fallback = $this->db->get('pp_shop_products_langs', 'name', [
'AND' => [
'product_id' => $productId,
'name[!]' => '',
],
'LIMIT' => 1,
]);
return $fallback ? (string)$fallback : '';
}
public function getCategoryProductIds(int $categoryId): array
{
if ($categoryId <= 0) {
return [];
}
$result = $this->db->select('pp_shop_products_categories', 'product_id', ['category_id' => $categoryId]);
return is_array($result) ? $result : [];
}
}

View File

@@ -0,0 +1,532 @@
<?php
namespace Domain\Client;
class ClientRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'client_surname',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'client_name' => 'c.client_name',
'client_surname' => 'c.client_surname',
'client_email' => 'c.client_email',
'client_phone' => 'c.client_phone',
'client_city' => 'c.client_city',
'total_orders' => 'c.total_orders',
'total_spent' => 'c.total_spent',
'client_type' => 'c.is_registered',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'c.client_surname';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = [
'o.client_name IS NOT NULL',
'o.client_surname IS NOT NULL',
'o.client_email IS NOT NULL',
];
$params = [];
$name = $this->normalizeTextFilter($filters['name'] ?? '');
if ($name !== '') {
$where[] = 'o.client_name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$surname = $this->normalizeTextFilter($filters['surname'] ?? '');
if ($surname !== '') {
$where[] = 'o.client_surname LIKE :surname';
$params[':surname'] = '%' . $surname . '%';
}
$email = $this->normalizeTextFilter($filters['email'] ?? '');
if ($email !== '') {
$where[] = 'o.client_email LIKE :email';
$params[':email'] = '%' . $email . '%';
}
$clientType = trim((string)($filters['client_type'] ?? ''));
if ($clientType === 'registered') {
$where[] = 'o.client_id IS NOT NULL';
} elseif ($clientType === 'guest') {
$where[] = 'o.client_id IS NULL';
}
$whereSql = implode(' AND ', $where);
$aggregatedSql = "
SELECT
MAX(o.client_id) AS client_id,
o.client_name,
o.client_surname,
o.client_email,
MAX(o.client_phone) AS client_phone,
MAX(o.client_city) AS client_city,
COUNT(*) AS total_orders,
COALESCE(SUM(o.summary), 0) AS total_spent,
CASE
WHEN MAX(o.client_id) IS NOT NULL THEN 1
ELSE 0
END AS is_registered
FROM pp_shop_orders AS o
WHERE {$whereSql}
GROUP BY o.client_name, o.client_surname, o.client_email
";
$sqlCount = "
SELECT COUNT(0)
FROM ({$aggregatedSql}) AS c
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
c.client_id,
c.client_name,
c.client_surname,
c.client_email,
c.client_phone,
c.client_city,
c.total_orders,
c.total_spent,
c.is_registered
FROM ({$aggregatedSql}) AS c
ORDER BY {$sortSql} {$sortDir}, c.client_surname ASC, c.client_name ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item['client_id'] = !isset($item['client_id']) ? null : (int)$item['client_id'];
$item['client_name'] = (string)($item['client_name'] ?? '');
$item['client_surname'] = (string)($item['client_surname'] ?? '');
$item['client_email'] = (string)($item['client_email'] ?? '');
$item['client_phone'] = (string)($item['client_phone'] ?? '');
$item['client_city'] = (string)($item['client_city'] ?? '');
$item['total_orders'] = (int)($item['total_orders'] ?? 0);
$item['total_spent'] = (float)($item['total_spent'] ?? 0);
$item['is_registered'] = ((int)($item['is_registered'] ?? 0)) === 1 ? 1 : 0;
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function ordersForClient(string $name, string $surname, string $email): array
{
$name = trim($name);
$surname = trim($surname);
$email = trim($email);
if ($name === '' || $surname === '' || $email === '') {
return [];
}
$sql = "
SELECT
o.id,
o.date_order,
o.summary,
o.payment_method,
o.transport,
o.message
FROM pp_shop_orders AS o
WHERE o.client_name = :name
AND o.client_surname = :surname
AND o.client_email = :email
ORDER BY o.date_order DESC, o.id DESC
";
$stmt = $this->db->query($sql, [
':name' => $name,
':surname' => $surname,
':email' => $email,
]);
$rows = $stmt ? $stmt->fetchAll() : [];
if (!is_array($rows)) {
return [];
}
foreach ($rows as &$row) {
$row['id'] = (int)($row['id'] ?? 0);
$row['date_order'] = (string)($row['date_order'] ?? '');
$row['summary'] = (float)($row['summary'] ?? 0);
$row['payment_method'] = (string)($row['payment_method'] ?? '');
$row['transport'] = (string)($row['transport'] ?? '');
$row['message'] = (string)($row['message'] ?? '');
}
unset($row);
return $rows;
}
/**
* @return array{total_orders: int, total_spent: float}
*/
public function totalsForClient(string $name, string $surname, string $email): array
{
$name = trim($name);
$surname = trim($surname);
$email = trim($email);
if ($name === '' || $surname === '' || $email === '') {
return [
'total_orders' => 0,
'total_spent' => 0.0,
];
}
$sql = "
SELECT
COUNT(*) AS total_orders,
COALESCE(SUM(o.summary), 0) AS total_spent
FROM pp_shop_orders AS o
WHERE o.client_name = :name
AND o.client_surname = :surname
AND o.client_email = :email
";
$stmt = $this->db->query($sql, [
':name' => $name,
':surname' => $surname,
':email' => $email,
]);
$rows = $stmt ? $stmt->fetchAll() : [];
return [
'total_orders' => isset($rows[0]['total_orders']) ? (int)$rows[0]['total_orders'] : 0,
'total_spent' => isset($rows[0]['total_spent']) ? (float)$rows[0]['total_spent'] : 0.0,
];
}
// ===== Frontend methods =====
/**
* @return array<string, mixed>|null
*/
public function clientDetails(int $clientId): ?array
{
if ($clientId <= 0) {
return null;
}
return $this->db->get('pp_shop_clients', '*', ['id' => $clientId]) ?: null;
}
public function clientEmail(int $clientId): ?string
{
if ($clientId <= 0) {
return null;
}
$email = $this->db->get('pp_shop_clients', 'email', ['id' => $clientId]);
return $email ? (string)$email : null;
}
/**
* @return array<int, array<string, mixed>>
*/
public function clientAddresses(int $clientId): array
{
if ($clientId <= 0) {
return [];
}
$rows = $this->db->select('pp_shop_clients_addresses', '*', ['client_id' => $clientId]);
return is_array($rows) ? $rows : [];
}
/**
* @return array<string, mixed>|null
*/
public function addressDetails(int $addressId): ?array
{
if ($addressId <= 0) {
return null;
}
return $this->db->get('pp_shop_clients_addresses', '*', ['id' => $addressId]) ?: null;
}
public function addressDelete(int $addressId): bool
{
if ($addressId <= 0) {
return false;
}
return (bool)$this->db->delete('pp_shop_clients_addresses', ['id' => $addressId]);
}
/**
* @param array<string, string> $data Keys: name, surname, street, postal_code, city, phone
*/
public function addressSave(int $clientId, ?int $addressId, array $data): bool
{
if ($clientId <= 0) {
return false;
}
$row = [
'name' => (string)($data['name'] ?? ''),
'surname' => (string)($data['surname'] ?? ''),
'street' => (string)($data['street'] ?? ''),
'postal_code' => (string)($data['postal_code'] ?? ''),
'city' => (string)($data['city'] ?? ''),
'phone' => (string)($data['phone'] ?? ''),
];
if (!$addressId || $addressId <= 0) {
$row['client_id'] = $clientId;
return (bool)$this->db->insert('pp_shop_clients_addresses', $row);
}
return (bool)$this->db->update('pp_shop_clients_addresses', $row, [
'AND' => [
'client_id' => $clientId,
'id' => $addressId,
],
]);
}
public function markAddressAsCurrent(int $clientId, int $addressId): bool
{
if ($clientId <= 0 || $addressId <= 0) {
return false;
}
$this->db->update('pp_shop_clients_addresses', ['current' => 0], ['client_id' => $clientId]);
$this->db->update('pp_shop_clients_addresses', ['current' => 1], [
'AND' => [
'client_id' => $clientId,
'id' => $addressId,
],
]);
return true;
}
/**
* @return array<int, array<string, mixed>>
*/
public function clientOrders(int $clientId): array
{
if ($clientId <= 0) {
return [];
}
$rows = $this->db->select('pp_shop_orders', 'id', [
'client_id' => $clientId,
'ORDER' => ['date_order' => 'DESC'],
]);
$orders = [];
if (is_array($rows)) {
$orderRepo = new \Domain\Order\OrderRepository($this->db);
foreach ($rows as $row) {
$orders[] = $orderRepo->orderDetailsFrontend($row);
}
}
return $orders;
}
/**
* @return array{status: string, client?: array, hash?: string, code?: string}
*/
public function authenticate(string $email, string $password): array
{
$email = trim($email);
$password = trim($password);
if ($email === '' || $password === '') {
return ['status' => 'error', 'code' => 'logowanie-nieudane'];
}
$client = $this->db->get('pp_shop_clients', [
'id', 'password', 'register_date', 'hash', 'status',
], ['email' => $email]);
if (!$client) {
return ['status' => 'error', 'code' => 'logowanie-nieudane'];
}
if (!(int)$client['status']) {
return ['status' => 'inactive', 'hash' => $client['hash']];
}
if ($client['password'] !== md5($client['register_date'] . $password)) {
return ['status' => 'error', 'code' => 'logowanie-blad-nieprawidlowe-haslo'];
}
$fullClient = $this->clientDetails((int)$client['id']);
return ['status' => 'ok', 'client' => $fullClient];
}
/**
* @return array{id: int, hash: string}|null Null when email already taken
*/
public function createClient(string $email, string $password, bool $agreementMarketing): ?array
{
$email = trim($email);
if ($email === '' || $password === '') {
return null;
}
if ($this->db->count('pp_shop_clients', ['email' => $email])) {
return null;
}
$hash = md5(time() . $email);
$registerDate = date('Y-m-d H:i:s');
$inserted = $this->db->insert('pp_shop_clients', [
'email' => $email,
'password' => md5($registerDate . $password),
'hash' => $hash,
'agremment_marketing' => $agreementMarketing ? 1 : 0,
'register_date' => $registerDate,
]);
if (!$inserted) {
return null;
}
return [
'id' => (int)$this->db->id(),
'hash' => $hash,
];
}
/**
* Confirms registration. Returns client email on success, null on failure.
*/
public function confirmRegistration(string $hash): ?string
{
$hash = trim($hash);
if ($hash === '') {
return null;
}
$id = $this->db->get('pp_shop_clients', 'id', [
'AND' => ['hash' => $hash, 'status' => 0],
]);
if (!$id) {
return null;
}
$this->db->update('pp_shop_clients', ['status' => 1], ['id' => $id]);
$email = $this->db->get('pp_shop_clients', 'email', ['id' => $id]);
return $email ? (string)$email : null;
}
/**
* Generates new password. Returns [email, password] on success, null on failure.
*
* @return array{email: string, password: string}|null
*/
public function generateNewPassword(string $hash): ?array
{
$hash = trim($hash);
if ($hash === '') {
return null;
}
$data = $this->db->get('pp_shop_clients', ['id', 'email', 'register_date'], [
'AND' => ['hash' => $hash, 'status' => 1, 'password_recovery' => 1],
]);
if (!$data) {
return null;
}
$newPassword = substr(md5(time()), 0, 10);
$this->db->update('pp_shop_clients', [
'password_recovery' => 0,
'password' => md5($data['register_date'] . $newPassword),
], ['id' => $data['id']]);
return [
'email' => (string)$data['email'],
'password' => $newPassword,
];
}
/**
* Initiates password recovery. Returns hash on success, null on failure.
*/
public function initiatePasswordRecovery(string $email): ?string
{
$email = trim($email);
if ($email === '') {
return null;
}
$hash = $this->db->get('pp_shop_clients', 'hash', [
'AND' => ['email' => $email, 'status' => 1],
]);
if (!$hash) {
return null;
}
$this->db->update('pp_shop_clients', ['password_recovery' => 1], ['email' => $email]);
return (string)$hash;
}
private function normalizeTextFilter($value): string
{
$value = trim((string)$value);
if ($value === '') {
return '';
}
if (strlen($value) > 255) {
return substr($value, 0, 255);
}
return $value;
}
}

View File

@@ -0,0 +1,458 @@
<?php
namespace Domain\Coupon;
class CouponRepository
{
private const MAX_PER_PAGE = 100;
private $db;
private ?string $defaultLangId = null;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'sc.id',
'status' => 'sc.status',
'used_count' => 'sc.used_count',
'name' => 'sc.name',
'type' => 'sc.type',
'amount' => 'sc.amount',
'one_time' => 'sc.one_time',
'send' => 'sc.send',
'used' => 'sc.used',
'date_used' => 'sc.date_used',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'sc.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'sc.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'sc.status = :status';
$params[':status'] = (int)$status;
}
$used = trim((string)($filters['used'] ?? ''));
if ($used === '0' || $used === '1') {
$where[] = 'sc.used = :used';
$params[':used'] = (int)$used;
}
$send = trim((string)($filters['send'] ?? ''));
if ($send === '0' || $send === '1') {
$where[] = 'sc.send = :send';
$params[':send'] = (int)$send;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_coupon AS sc
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
sc.id,
sc.name,
sc.status,
sc.used,
sc.type,
sc.amount,
sc.one_time,
sc.send,
sc.include_discounted_product,
sc.categories,
sc.date_used,
sc.used_count
FROM pp_shop_coupon AS sc
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, sc.id DESC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item['id'] = (int)($item['id'] ?? 0);
$item['status'] = $this->toSwitchValue($item['status'] ?? 0);
$item['used'] = $this->toSwitchValue($item['used'] ?? 0);
$item['one_time'] = $this->toSwitchValue($item['one_time'] ?? 0);
$item['send'] = $this->toSwitchValue($item['send'] ?? 0);
$item['used_count'] = (int)($item['used_count'] ?? 0);
$item['type'] = (int)($item['type'] ?? 1);
$item['categories'] = $this->decodeIdList($item['categories'] ?? null);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
public function find(int $couponId): array
{
if ($couponId <= 0) {
return $this->defaultCoupon();
}
$coupon = $this->db->get('pp_shop_coupon', '*', ['id' => $couponId]);
if (!is_array($coupon)) {
return $this->defaultCoupon();
}
$coupon['id'] = (int)($coupon['id'] ?? 0);
$coupon['status'] = $this->toSwitchValue($coupon['status'] ?? 0);
$coupon['send'] = $this->toSwitchValue($coupon['send'] ?? 0);
$coupon['used'] = $this->toSwitchValue($coupon['used'] ?? 0);
$coupon['one_time'] = $this->toSwitchValue($coupon['one_time'] ?? 0);
$coupon['include_discounted_product'] = $this->toSwitchValue($coupon['include_discounted_product'] ?? 0);
$coupon['type'] = (int)($coupon['type'] ?? 1);
$coupon['used_count'] = (int)($coupon['used_count'] ?? 0);
$coupon['categories'] = $this->decodeIdList($coupon['categories'] ?? null);
return $coupon;
}
public function save(array $data): ?int
{
$couponId = (int)($data['id'] ?? 0);
$row = [
'name' => trim((string)($data['name'] ?? '')),
'status' => $this->toSwitchValue($data['status'] ?? 0),
'send' => $this->toSwitchValue($data['send'] ?? 0),
'used' => $this->toSwitchValue($data['used'] ?? 0),
'type' => (int)($data['type'] ?? 1),
'amount' => $this->toNullableNumeric($data['amount'] ?? null),
'one_time' => $this->toSwitchValue($data['one_time'] ?? 0),
'include_discounted_product' => $this->toSwitchValue($data['include_discounted_product'] ?? 0),
'categories' => $this->encodeIdList($data['categories'] ?? null),
];
if ($couponId <= 0) {
$this->db->insert('pp_shop_coupon', $row);
$id = (int)$this->db->id();
return $id > 0 ? $id : null;
}
$this->db->update('pp_shop_coupon', $row, ['id' => $couponId]);
return $couponId;
}
public function delete(int $couponId): bool
{
if ($couponId <= 0) {
return false;
}
return (bool)$this->db->delete('pp_shop_coupon', ['id' => $couponId]);
}
public function findByName(string $name)
{
$name = trim($name);
if ($name === '') {
return null;
}
$coupon = $this->db->get('pp_shop_coupon', '*', ['name' => $name]);
if (!is_array($coupon)) {
return null;
}
$coupon['id'] = (int)($coupon['id'] ?? 0);
$coupon['status'] = (int)($coupon['status'] ?? 0);
$coupon['used'] = (int)($coupon['used'] ?? 0);
$coupon['one_time'] = (int)($coupon['one_time'] ?? 0);
$coupon['type'] = (int)($coupon['type'] ?? 0);
$coupon['include_discounted_product'] = (int)($coupon['include_discounted_product'] ?? 0);
$coupon['used_count'] = (int)($coupon['used_count'] ?? 0);
return (object)$coupon;
}
public function isAvailable($coupon)
{
if (!$coupon) {
return false;
}
$id = is_object($coupon) ? ($coupon->id ?? 0) : ($coupon['id'] ?? 0);
$status = is_object($coupon) ? ($coupon->status ?? 0) : ($coupon['status'] ?? 0);
$used = is_object($coupon) ? ($coupon->used ?? 0) : ($coupon['used'] ?? 0);
if (!(int)$id) {
return false;
}
if (!(int)$status) {
return false;
}
return !(int)$used;
}
public function markAsUsed(int $couponId)
{
if ($couponId <= 0) {
return;
}
$this->db->update('pp_shop_coupon', [
'used' => 1,
'date_used' => date('Y-m-d H:i:s'),
], ['id' => $couponId]);
}
public function incrementUsedCount(int $couponId)
{
if ($couponId <= 0) {
return;
}
$this->db->update('pp_shop_coupon', [
'used_count[+]' => 1,
], ['id' => $couponId]);
}
/**
* @return array<int, array<string, mixed>>
*/
public function categoriesTree($parentId = null): array
{
$rows = $this->db->select('pp_shop_categories', ['id'], [
'parent_id' => $parentId,
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$categories = [];
foreach ($rows as $row) {
$categoryId = (int)($row['id'] ?? 0);
if ($categoryId <= 0) {
continue;
}
$category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]);
if (!is_array($category)) {
continue;
}
$translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]);
$category['languages'] = [];
if (is_array($translations)) {
foreach ($translations as $translation) {
$langId = (string)($translation['lang_id'] ?? '');
if ($langId !== '') {
$category['languages'][$langId] = $translation;
}
}
}
$category['title'] = $this->categoryTitle($category['languages']);
$category['subcategories'] = $this->categoriesTree($categoryId);
$categories[] = $category;
}
return $categories;
}
private function defaultCoupon(): array
{
return [
'id' => 0,
'name' => '',
'status' => 1,
'send' => 0,
'used' => 0,
'type' => 1,
'amount' => null,
'one_time' => 1,
'include_discounted_product' => 0,
'categories' => [],
'used_count' => 0,
'date_used' => null,
];
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
private function toNullableNumeric($value): ?string
{
if ($value === null) {
return null;
}
$stringValue = trim((string)$value);
if ($stringValue === '') {
return null;
}
return str_replace(',', '.', $stringValue);
}
private function encodeIdList($values): ?string
{
$ids = $this->normalizeIdList($values);
if (empty($ids)) {
return null;
}
return json_encode($ids);
}
/**
* @return int[]
*/
private function decodeIdList($raw): array
{
if (is_array($raw)) {
return $this->normalizeIdList($raw);
}
$text = trim((string)$raw);
if ($text === '') {
return [];
}
$decoded = json_decode($text, true);
if (!is_array($decoded)) {
return [];
}
return $this->normalizeIdList($decoded);
}
/**
* @return int[]
*/
private function normalizeIdList($values): array
{
if ($values === null) {
return [];
}
if (!is_array($values)) {
$text = trim((string)$values);
if ($text === '') {
return [];
}
if (strpos($text, ',') !== false) {
$values = explode(',', $text);
} else {
$values = [$text];
}
}
$ids = [];
foreach ($values as $value) {
$id = (int)$value;
if ($id > 0) {
$ids[$id] = $id;
}
}
return array_values($ids);
}
private function categoryTitle(array $languages): string
{
$defaultLang = $this->defaultLanguageId();
if ($defaultLang !== '' && isset($languages[$defaultLang]['title'])) {
$title = trim((string)$languages[$defaultLang]['title']);
if ($title !== '') {
return $title;
}
}
foreach ($languages as $language) {
$title = trim((string)($language['title'] ?? ''));
if ($title !== '') {
return $title;
}
}
return '';
}
private function defaultLanguageId(): string
{
if ($this->defaultLangId !== null) {
return $this->defaultLangId;
}
$rows = $this->db->select('pp_langs', ['id', 'start', 'o'], [
'status' => 1,
'ORDER' => ['start' => 'DESC', 'o' => 'ASC'],
]);
if (is_array($rows) && !empty($rows)) {
$this->defaultLangId = (string)($rows[0]['id'] ?? '');
} else {
$this->defaultLangId = '';
}
return $this->defaultLangId;
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Domain\Dashboard;
class DashboardRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
public function summaryOrders(): int
{
try {
$redis = \Shared\Cache\RedisConnection::getInstance()->getConnection();
if ( $redis ) {
$cached = $redis->get( 'summary_ordersd' );
if ( $cached !== false ) {
return (int) unserialize( $cached );
}
$summary = (int) $this->db->count( 'pp_shop_orders', [ 'status' => 6 ] );
$redis->setex( 'summary_ordersd', 300, serialize( $summary ) );
return $summary;
}
} catch ( \RedisException $e ) {
// fallback
}
return (int) $this->db->count( 'pp_shop_orders', [ 'status' => 6 ] );
}
public function summarySales(): float
{
try {
$redis = \Shared\Cache\RedisConnection::getInstance()->getConnection();
if ( $redis ) {
$cached = $redis->get( 'summary_salesd' );
if ( $cached !== false ) {
return (float) unserialize( $cached );
}
$summary = $this->calculateTotalSales();
$redis->setex( 'summary_salesd', 300, serialize( $summary ) );
return $summary;
}
} catch ( \RedisException $e ) {
// fallback
}
return $this->calculateTotalSales();
}
private function calculateTotalSales(): float
{
return (float) $this->db->sum( 'pp_shop_orders', 'summary', [ 'status' => 6 ] )
- (float) $this->db->sum( 'pp_shop_orders', 'transport_cost', [ 'status' => 6 ] );
}
public function salesGrid(): array
{
$grid = [];
$rows = $this->db->select( 'pp_shop_orders', [ 'id', 'date_order' ], [ 'status' => 6 ] );
if ( is_array( $rows ) ) {
foreach ( $rows as $row ) {
$ts = strtotime( $row['date_order'] );
$dayOfWeek = date( 'N', $ts );
$hour = date( 'G', $ts );
if ( !isset( $grid[$dayOfWeek][$hour] ) ) {
$grid[$dayOfWeek][$hour] = 0;
}
$grid[$dayOfWeek][$hour]++;
}
}
return $grid;
}
public function mostViewedProducts(): array
{
$stmt = $this->db->query(
'SELECT id, SUM(visits) AS visits '
. 'FROM pp_shop_products '
. 'GROUP BY id '
. 'ORDER BY visits DESC '
. 'LIMIT 10'
);
return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
}
public function bestSalesProducts(): array
{
$stmt = $this->db->query(
'SELECT parent_product_id, SUM(quantity) AS quantity_summary, SUM(price_brutto_promo * quantity) AS sales '
. 'FROM pp_shop_order_products AS psop '
. 'INNER JOIN pp_shop_orders AS pso ON pso.id = psop.order_id '
. 'WHERE pso.status = 6 '
. 'GROUP BY parent_product_id '
. 'ORDER BY sales DESC '
. 'LIMIT 10'
);
return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
}
public function last24MonthsSales(): array
{
$sales = [];
$date = new \DateTime();
for ( $i = 0; $i < 24; $i++ ) {
$dateStart = $date->format( 'Y-m-01' );
$dateEnd = $date->format( 'Y-m-t' );
$where = [
'AND' => [
'status' => 6,
'date_order[>=]' => $dateStart,
'date_order[<=]' => $dateEnd,
]
];
$monthSales = (float) $this->db->sum( 'pp_shop_orders', 'summary', $where )
- (float) $this->db->sum( 'pp_shop_orders', 'transport_cost', $where );
$sales[] = [
'date' => $date->format( 'Y-m' ),
'sales' => $monthSales,
];
$date->sub( new \DateInterval( 'P1M' ) );
}
return $sales;
}
public function lastOrders( int $limit = 10 ): array
{
$stmt = $this->db->query(
'SELECT id, number, date_order, '
. 'CONCAT( client_name, \' \', client_surname ) AS client, '
. 'client_email, '
. 'CONCAT( client_street, \', \', client_postal_code, \' \', client_city ) AS address, '
. 'status, client_phone, summary '
. 'FROM pp_shop_orders '
. 'ORDER BY date_order DESC '
. 'LIMIT ' . (int) $limit
);
return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace Domain\Dictionaries;
class DictionariesRepository
{
private const MAX_PER_PAGE = 100;
private const CACHE_TTL = 86400;
private const CACHE_SUBDIR = 'dictionaries';
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function listForAdmin(
array $filters,
string $sortColumn = 'id',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'u.id',
'text' => 'text',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'u.id';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1=1'];
$params = [];
$text = trim((string)($filters['text'] ?? ''));
if (strlen($text) > 255) {
$text = substr($text, 0, 255);
}
if ($text !== '') {
$where[] = 'EXISTS (SELECT 1 FROM pp_units_langs AS ul2 WHERE ul2.unit_id = u.id AND ul2.text LIKE :text)';
$params[':text'] = '%' . $text . '%';
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_units AS u
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
u.*,
(
SELECT ul.text
FROM pp_units_langs AS ul
INNER JOIN pp_langs AS l ON l.id = ul.lang_id
WHERE ul.unit_id = u.id AND ul.text <> ''
ORDER BY l.o ASC
LIMIT 1
) AS text
FROM pp_units AS u
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, u.id {$sortDir}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
public function allUnits(): array
{
$sql = "
SELECT
u.*,
(
SELECT ul.text
FROM pp_units_langs AS ul
INNER JOIN pp_langs AS l ON l.id = ul.lang_id
WHERE ul.unit_id = u.id AND ul.text <> ''
ORDER BY l.o ASC
LIMIT 1
) AS text
FROM pp_units AS u
ORDER BY u.id ASC
";
$stmt = $this->db->query($sql);
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
if (!is_array($rows)) {
return [];
}
$units = [];
foreach ($rows as $row) {
$units[(int)$row['id']] = $row;
}
return $units;
}
public function find(int $unitId): ?array
{
$unit = $this->db->get('pp_units', '*', ['id' => $unitId]);
if (!$unit) {
return null;
}
$unit['languages'] = [];
$translations = $this->db->select('pp_units_langs', '*', [
'unit_id' => $unitId,
'ORDER' => ['lang_id' => 'ASC', 'id' => 'ASC'],
]);
if (is_array($translations)) {
foreach ($translations as $row) {
$unit['languages'][(string)$row['lang_id']] = $row;
}
}
return $unit;
}
public function save(array $data)
{
$unitId = isset($data['id']) ? (int)$data['id'] : 0;
if ($unitId <= 0) {
$this->db->insert('pp_units', []);
$unitId = (int)$this->db->id();
if ($unitId <= 0) {
return false;
}
}
$translations = $this->normalizeTranslations($data);
foreach ($translations as $langId => $text) {
$this->upsertTranslation($unitId, $langId, $text);
}
$this->clearCache();
return $unitId;
}
public function delete(int $unitId): bool
{
if ($unitId <= 0) {
return false;
}
$this->db->delete('pp_units_langs', ['unit_id' => $unitId]);
$result = $this->db->delete('pp_units', ['id' => $unitId]);
$this->clearCache();
return $result !== false;
}
public function getUnitNameById(int $unitId, $langId): string
{
$langId = trim((string)$langId);
if ($unitId <= 0 || $langId === '') {
return '';
}
$cacheKey = "get_name_by_id:$unitId:$langId";
$unitName = $this->cacheFetch($cacheKey);
if ($unitName === false) {
$unitName = $this->db->get('pp_units_langs', 'text', [
'AND' => [
'unit_id' => $unitId,
'lang_id' => $langId,
],
]);
$this->cacheStore($cacheKey, $unitName ?: '');
}
return (string)$unitName;
}
private function normalizeTranslations(array $data): array
{
$translations = [];
if (isset($data['translations']) && is_array($data['translations'])) {
foreach ($data['translations'] as $langId => $fields) {
$translations[(string)$langId] = trim((string)($fields['text'] ?? ''));
}
return $translations;
}
if (isset($data['text']) && is_array($data['text'])) {
foreach ($data['text'] as $langId => $text) {
$translations[(string)$langId] = trim((string)$text);
}
}
return $translations;
}
private function upsertTranslation(int $unitId, $langId, string $text): void
{
$langId = trim((string)$langId);
if ($langId === '') {
return;
}
$translationId = $this->db->get('pp_units_langs', 'id', [
'AND' => [
'unit_id' => $unitId,
'lang_id' => $langId,
],
]);
if ($translationId) {
$this->db->update('pp_units_langs', [
'text' => $text,
], [
'id' => (int)$translationId,
]);
return;
}
$this->db->insert('pp_units_langs', [
'unit_id' => $unitId,
'lang_id' => $langId,
'text' => $text,
]);
}
private function clearCache(): void
{
\Shared\Helpers\Helpers::delete_dir('../temp/');
\Shared\Helpers\Helpers::delete_dir('../temp/dictionaries');
}
private function cacheFetch(string $key)
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cached = $cacheHandler->get(self::CACHE_SUBDIR . ':' . $key);
if ($cached) {
return unserialize($cached);
}
return false;
}
private function cacheStore(string $key, string $value): void
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheHandler->set(self::CACHE_SUBDIR . ':' . $key, $value, self::CACHE_TTL);
}
}

View File

@@ -0,0 +1,712 @@
<?php
namespace Domain\Integrations;
class IntegrationsRepository
{
private $db;
private const SETTINGS_TABLES = [
'apilo' => 'pp_shop_apilo_settings',
'shoppro' => 'pp_shop_shoppro_settings',
];
public function __construct( $db )
{
$this->db = $db;
}
// ── Settings ────────────────────────────────────────────────
private function settingsTable( string $provider ): string
{
if ( !isset( self::SETTINGS_TABLES[$provider] ) )
throw new \InvalidArgumentException( "Unknown provider: $provider" );
return self::SETTINGS_TABLES[$provider];
}
public function getSettings( string $provider ): array
{
$table = $this->settingsTable( $provider );
$stmt = $this->db->query( "SELECT * FROM $table" );
$results = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
$settings = [];
foreach ( $results as $row )
$settings[$row['name']] = $row['value'];
return $settings;
}
public function getSetting( string $provider, string $name ): ?string
{
$table = $this->settingsTable( $provider );
$value = $this->db->get( $table, 'value', [ 'name' => $name ] );
return $value !== false ? $value : null;
}
public function saveSetting( string $provider, string $name, $value ): bool
{
$table = $this->settingsTable( $provider );
if ( $this->db->count( $table, [ 'name' => $name ] ) ) {
$this->db->update( $table, [ 'value' => $value ], [ 'name' => $name ] );
} else {
$this->db->insert( $table, [ 'name' => $name, 'value' => $value ] );
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
// ── Product linking (Apilo) ─────────────────────────────────
public function linkProduct( int $productId, $externalId, $externalName ): bool
{
return (bool) $this->db->update( 'pp_shop_products', [
'apilo_product_id' => $externalId,
'apilo_product_name' => \Shared\Helpers\Helpers::remove_special_chars( $externalName ),
], [ 'id' => $productId ] );
}
public function unlinkProduct( int $productId ): bool
{
return (bool) $this->db->update( 'pp_shop_products', [
'apilo_product_id' => null,
'apilo_product_name' => null,
], [ 'id' => $productId ] );
}
// ── Apilo OAuth ─────────────────────────────────────────────
public function apiloAuthorize( string $clientId, string $clientSecret, string $authCode ): bool
{
$postData = [
'grantType' => 'authorization_code',
'token' => $authCode,
];
$ch = curl_init( "https://projectpro.apilo.com/rest/auth/token/" );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $postData ) );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
"Authorization: Basic " . base64_encode( $clientId . ":" . $clientSecret ),
"Accept: application/json"
] );
$response = curl_exec( $ch );
if ( curl_errno( $ch ) ) {
curl_close( $ch );
return false;
}
curl_close( $ch );
$response = json_decode( $response, true );
if ( empty( $response['accessToken'] ) )
return false;
$this->saveSetting( 'apilo', 'access-token', $response['accessToken'] );
$this->saveSetting( 'apilo', 'refresh-token', $response['refreshToken'] );
$this->saveSetting( 'apilo', 'access-token-expire-at', $response['accessTokenExpireAt'] );
$this->saveSetting( 'apilo', 'refresh-token-expire-at', $response['refreshTokenExpireAt'] );
return true;
}
public function apiloGetAccessToken( int $refreshLeadSeconds = 300 ): ?string
{
$settings = $this->getSettings( 'apilo' );
$hasRefreshCredentials = !empty( $settings['refresh-token'] )
&& !empty( $settings['client-id'] )
&& !empty( $settings['client-secret'] );
$accessToken = trim( (string)($settings['access-token'] ?? '') );
$accessTokenExpireAt = trim( (string)($settings['access-token-expire-at'] ?? '') );
if ( $accessToken !== '' && $accessTokenExpireAt !== '' ) {
if ( !$this->shouldRefreshAccessToken( $accessTokenExpireAt, $refreshLeadSeconds ) ) {
return $accessToken;
}
}
if ( !$hasRefreshCredentials ) {
return null;
}
if (
!empty( $settings['refresh-token-expire-at'] ) &&
!$this->isFutureDate( (string)$settings['refresh-token-expire-at'] )
) {
return null;
}
return $this->refreshApiloAccessToken( $settings );
}
/**
* Keepalive tokenu Apilo do uzycia w CRON.
* Odswieza token, gdy wygasa lub jest bliski wygasniecia.
*
* @return array{success:bool,skipped:bool,message:string}
*/
public function apiloKeepalive( int $refreshLeadSeconds = 300 ): array
{
$settings = $this->getSettings( 'apilo' );
if ( (int)($settings['enabled'] ?? 0) !== 1 ) {
return [
'success' => false,
'skipped' => true,
'message' => 'Apilo disabled.',
];
}
if ( empty( $settings['client-id'] ) || empty( $settings['client-secret'] ) ) {
return [
'success' => false,
'skipped' => true,
'message' => 'Missing Apilo credentials.',
];
}
$token = $this->apiloGetAccessToken( $refreshLeadSeconds );
if ( !$token ) {
return [
'success' => false,
'skipped' => false,
'message' => 'Unable to refresh Apilo token.',
];
}
$this->saveSetting( 'apilo', 'token-keepalive-at', date( 'Y-m-d H:i:s' ) );
return [
'success' => true,
'skipped' => false,
'message' => 'Apilo token keepalive OK.',
];
}
private function refreshApiloAccessToken( array $settings ): ?string
{
$postData = [
'grantType' => 'refresh_token',
'token' => $settings['refresh-token'],
];
$ch = curl_init( "https://projectpro.apilo.com/rest/auth/token/" );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
"Authorization: Basic " . base64_encode( $settings['client-id'] . ":" . $settings['client-secret'] ),
"Accept: application/json"
] );
curl_setopt( $ch, CURLOPT_POST, true );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $postData ) );
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$response = curl_exec( $ch );
if ( curl_errno( $ch ) ) {
curl_close( $ch );
return null;
}
curl_close( $ch );
$response = json_decode( $response, true );
if ( empty( $response['accessToken'] ) ) {
return null;
}
$this->saveSetting( 'apilo', 'access-token', $response['accessToken'] );
$this->saveSetting( 'apilo', 'refresh-token', $response['refreshToken'] ?? ( $settings['refresh-token'] ?? '' ) );
$this->saveSetting( 'apilo', 'access-token-expire-at', $response['accessTokenExpireAt'] ?? null );
$this->saveSetting( 'apilo', 'refresh-token-expire-at', $response['refreshTokenExpireAt'] ?? null );
return $response['accessToken'];
}
private function shouldRefreshAccessToken( string $expiresAtRaw, int $leadSeconds = 300 ): bool
{
try {
$expiresAt = new \DateTime( $expiresAtRaw );
} catch ( \Exception $e ) {
return true;
}
$threshold = new \DateTime( date( 'Y-m-d H:i:s', time() + max( 0, $leadSeconds ) ) );
return $expiresAt <= $threshold;
}
private function isFutureDate( string $dateRaw ): bool
{
try {
$date = new \DateTime( $dateRaw );
} catch ( \Exception $e ) {
return false;
}
return $date > new \DateTime( date( 'Y-m-d H:i:s' ) );
}
/**
* Sprawdza aktualny stan integracji Apilo i zwraca komunikat dla UI.
*
* @return array{is_valid:bool,severity:string,message:string}
*/
public function apiloIntegrationStatus(): array
{
$settings = $this->getSettings( 'apilo' );
$missing = [];
foreach ( [ 'client-id', 'client-secret' ] as $field ) {
if ( trim( (string)($settings[$field] ?? '') ) === '' )
$missing[] = $field;
}
if ( !empty( $missing ) ) {
return [
'is_valid' => false,
'severity' => 'danger',
'message' => 'Brakuje konfiguracji Apilo: ' . implode( ', ', $missing ) . '.',
];
}
$accessToken = trim( (string)($settings['access-token'] ?? '') );
$authorizationCode = trim( (string)($settings['authorization-code'] ?? '') );
if ( $accessToken === '' ) {
if ( $authorizationCode === '' ) {
return [
'is_valid' => false,
'severity' => 'warning',
'message' => 'Brak authorization-code i access-token. Wpisz kod autoryzacji i uruchom autoryzacje.',
];
}
return [
'is_valid' => false,
'severity' => 'warning',
'message' => 'Brak access-token. Uruchom autoryzacje Apilo.',
];
}
$token = $this->apiloGetAccessToken();
if ( !$token ) {
return [
'is_valid' => false,
'severity' => 'danger',
'message' => 'Token Apilo jest niewazny lub wygasl i nie udal sie refresh. Wykonaj ponowna autoryzacje.',
];
}
$expiresAt = trim( (string)($settings['access-token-expire-at'] ?? '') );
$suffix = $expiresAt !== '' ? ( ' Token wazny do: ' . $expiresAt . '.' ) : '';
return [
'is_valid' => true,
'severity' => 'success',
'message' => 'Integracja Apilo jest aktywna.' . $suffix,
];
}
// ── Apilo API fetch lists ───────────────────────────────────
private const APILO_ENDPOINTS = [
'platform' => 'https://projectpro.apilo.com/rest/api/orders/platform/map/',
'status' => 'https://projectpro.apilo.com/rest/api/orders/status/map/',
'carrier' => 'https://projectpro.apilo.com/rest/api/orders/carrier-account/map/',
'payment' => 'https://projectpro.apilo.com/rest/api/orders/payment/map/',
];
private const APILO_SETTINGS_KEYS = [
'platform' => 'platform-list',
'status' => 'status-types-list',
'carrier' => 'carrier-account-list',
'payment' => 'payment-types-list',
];
/**
* Fetch list from Apilo API and save to settings.
* @param string $type platform|status|carrier|payment
*/
public function apiloFetchList( string $type ): bool
{
$result = $this->apiloFetchListResult( $type );
return !empty( $result['success'] );
}
/**
* Fetch list from Apilo API and return detailed status for UI.
*
* @param string $type platform|status|carrier|payment
* @return array{success:bool,count:int,message:string}
*/
public function apiloFetchListResult( string $type ): array
{
if ( !isset( self::APILO_ENDPOINTS[$type] ) )
throw new \InvalidArgumentException( "Unknown apilo list type: $type" );
$settings = $this->getSettings( 'apilo' );
$missingFields = [];
foreach ( [ 'client-id', 'client-secret' ] as $requiredField ) {
if ( trim( (string)($settings[$requiredField] ?? '') ) === '' )
$missingFields[] = $requiredField;
}
if ( !empty( $missingFields ) ) {
return [
'success' => false,
'count' => 0,
'message' => 'Brakuje konfiguracji Apilo: ' . implode( ', ', $missingFields ) . '. Uzupelnij pola i zapisz ustawienia.',
];
}
$accessToken = $this->apiloGetAccessToken();
if ( !$accessToken ) {
return [
'success' => false,
'count' => 0,
'message' => 'Brak aktywnego tokenu Apilo. Wykonaj autoryzacje Apilo i sprobuj ponownie.',
];
}
$ch = curl_init( self::APILO_ENDPOINTS[$type] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $accessToken,
"Accept: application/json"
] );
$response = curl_exec( $ch );
if ( curl_errno( $ch ) ) {
$error = curl_error( $ch );
curl_close( $ch );
return [
'success' => false,
'count' => 0,
'message' => 'Blad polaczenia z Apilo: ' . $error . '. Sprawdz polaczenie serwera i sprobuj ponownie.',
];
}
$httpCode = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
$data = json_decode( $response, true );
if ( !is_array( $data ) ) {
$responsePreview = substr( trim( (string)$response ), 0, 180 );
if ( $responsePreview === '' )
$responsePreview = '[pusta odpowiedz]';
return [
'success' => false,
'count' => 0,
'message' => 'Apilo zwrocilo niepoprawny format odpowiedzi (HTTP ' . $httpCode . '). Odpowiedz: ' . $responsePreview,
];
}
if ( $httpCode >= 400 ) {
return [
'success' => false,
'count' => 0,
'message' => 'Apilo zwrocilo blad HTTP ' . $httpCode . ': ' . $this->extractApiloErrorMessage( $data ),
];
}
$normalizedList = $this->normalizeApiloMapList( $data );
if ( $normalizedList === null ) {
return [
'success' => false,
'count' => 0,
'message' => 'Apilo zwrocilo dane w nieoczekiwanym formacie. Odswiez token i sproboj ponownie.',
];
}
$this->saveSetting( 'apilo', self::APILO_SETTINGS_KEYS[$type], $normalizedList );
return [
'success' => true,
'count' => count( $normalizedList ),
'message' => 'OK',
];
}
/**
* Normalizuje odpowiedz API mapowania do listy rekordow ['id' => ..., 'name' => ...].
* Zwraca null dla payloadu bledow lub nieoczekiwanego formatu.
*
* @return array<int, array{id:mixed,name:mixed}>|null
*/
private function normalizeApiloMapList( array $data ): ?array
{
if ( isset( $data['message'] ) && isset( $data['code'] ) )
return null;
if ( $this->isMapListShape( $data ) )
return $data;
if ( isset( $data['items'] ) && is_array( $data['items'] ) && $this->isMapListShape( $data['items'] ) )
return $data['items'];
if ( isset( $data['data'] ) && is_array( $data['data'] ) && $this->isMapListShape( $data['data'] ) )
return $data['data'];
// Dopuszczamy rowniez format asocjacyjny: [id => name, ...], ale tylko dla kluczy liczbowych.
if ( !empty( $data ) ) {
$normalized = [];
foreach ( $data as $key => $value ) {
if ( !( is_int( $key ) || ( is_string( $key ) && preg_match('/^-?\d+$/', $key) === 1 ) ) )
return null;
if ( !is_scalar( $value ) )
return null;
$normalized[] = [
'id' => $key,
'name' => (string) $value,
];
}
return !empty( $normalized ) ? $normalized : null;
}
return null;
}
private function isMapListShape( array $list ): bool
{
if ( empty( $list ) )
return false;
foreach ( $list as $row ) {
if ( !is_array( $row ) || !array_key_exists( 'id', $row ) || !array_key_exists( 'name', $row ) )
return false;
}
return true;
}
private function extractApiloErrorMessage( array $data ): string
{
foreach ( [ 'message', 'error', 'detail', 'title' ] as $key ) {
if ( isset( $data[$key] ) && is_scalar( $data[$key] ) ) {
$message = trim( (string)$data[$key] );
if ( $message !== '' )
return $message;
}
}
if ( isset( $data['errors'] ) ) {
if ( is_array( $data['errors'] ) ) {
$flat = [];
foreach ( $data['errors'] as $errorItem ) {
if ( is_scalar( $errorItem ) )
$flat[] = (string)$errorItem;
elseif ( is_array( $errorItem ) )
$flat[] = json_encode( $errorItem, JSON_UNESCAPED_UNICODE );
}
if ( !empty( $flat ) )
return implode( '; ', $flat );
} elseif ( is_scalar( $data['errors'] ) ) {
return (string)$data['errors'];
}
}
return 'Nieznany blad odpowiedzi API.';
}
// ── Apilo product operations ────────────────────────────────
public function getProductSku( int $productId ): ?string
{
$sku = $this->db->get( 'pp_shop_products', 'sku', [ 'id' => $productId ] );
return $sku ?: null;
}
public function apiloProductSearch( string $sku ): array
{
$accessToken = $this->apiloGetAccessToken();
if ( !$accessToken )
return [ 'status' => 'error', 'msg' => 'Brak tokenu Apilo.' ];
$url = "https://projectpro.apilo.com/rest/api/warehouse/product/?" . http_build_query( [ 'sku' => $sku ] );
$ch = curl_init( $url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $accessToken,
"Accept: application/json"
] );
$response = curl_exec( $ch );
if ( curl_errno( $ch ) ) {
$error = curl_error( $ch );
curl_close( $ch );
return [ 'status' => 'error', 'msg' => 'Błąd cURL: ' . $error ];
}
curl_close( $ch );
$data = json_decode( $response, true );
if ( $data && isset( $data['products'] ) ) {
$data['status'] = 'SUCCESS';
return $data;
}
return [ 'status' => 'SUCCESS', 'msg' => 'Brak wyników dla podanego SKU.', 'products' => '' ];
}
public function apiloCreateProduct( int $productId ): array
{
$accessToken = $this->apiloGetAccessToken();
if ( !$accessToken )
return [ 'success' => false, 'message' => 'Brak tokenu Apilo.' ];
$product = ( new \Domain\Product\ProductRepository( $this->db ) )->findCached( $productId );
$params = [
'sku' => $product['sku'],
'ean' => $product['ean'],
'name' => $product['language']['name'],
'tax' => (int) $product['vat'],
'status' => 1,
'quantity' => (int) $product['quantity'],
'priceWithTax' => $product['price_brutto'],
'description' => $product['language']['description'] . '<br>' . $product['language']['short_description'],
'shortDescription' => '',
'images' => [],
];
foreach ( $product['images'] as $image )
$params['images'][] = "https://" . $_SERVER['HTTP_HOST'] . $image['src'];
$ch = curl_init( "https://projectpro.apilo.com/rest/api/warehouse/product/" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( [ $params ] ) );
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $accessToken,
"Content-Type: application/json",
"Accept: application/json"
] );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$response = curl_exec( $ch );
$responseData = json_decode( $response, true );
if ( curl_errno( $ch ) ) {
$error = curl_error( $ch );
curl_close( $ch );
return [ 'success' => false, 'message' => 'Błąd cURL: ' . $error ];
}
curl_close( $ch );
if ( !empty( $responseData['products'] ) ) {
$this->db->update( 'pp_shop_products', [
'apilo_product_id' => reset( $responseData['products'] ),
'apilo_product_name' => $product['language']['name'],
], [ 'id' => $product['id'] ] );
return [ 'success' => true, 'message' => 'Produkt został dodany do magazynu APILO.' ];
}
return [ 'success' => false, 'message' => 'Podczas dodawania produktu wystąpił błąd.' ];
}
// ── ShopPRO import ──────────────────────────────────────────
public function shopproImportProduct( int $productId ): array
{
$settings = $this->getSettings( 'shoppro' );
$mdb2 = new \medoo( [
'database_type' => 'mysql',
'database_name' => $settings['db_name'],
'server' => $settings['db_host'],
'username' => $settings['db_user'],
'password' => $settings['db_password'],
'charset' => 'utf8'
] );
$product = $mdb2->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
if ( !$product )
return [ 'success' => false, 'message' => 'Podczas importowania produktu wystąpił błąd.' ];
$this->db->insert( 'pp_shop_products', [
'price_netto' => $product['price_netto'],
'price_brutto' => $product['price_brutto'],
'vat' => $product['vat'],
'stock_0_buy' => $product['stock_0_buy'],
'quantity' => $product['quantity'],
'wp' => $product['wp'],
'sku' => $product['sku'],
'ean' => $product['ean'],
'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'],
'additional_message_text' => $product['additional_message_text'],
'additional_message_required'=> $product['additional_message_required'],
'weight' => $product['weight'],
] );
$newProductId = $this->db->id();
if ( !$newProductId )
return [ 'success' => false, 'message' => 'Podczas importowania produktu wystąpił błąd.' ];
// Import translations
$languages = $mdb2->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
if ( is_array( $languages ) ) {
foreach ( $languages as $lang ) {
$this->db->insert( 'pp_shop_products_langs', [
'product_id' => $newProductId,
'lang_id' => $lang['lang_id'],
'name' => $lang['name'],
'short_description' => $lang['short_description'],
'description' => $lang['description'],
'tab_name_1' => $lang['tab_name_1'],
'tab_description_1' => $lang['tab_description_1'],
'tab_name_2' => $lang['tab_name_2'],
'tab_description_2' => $lang['tab_description_2'],
'meta_title' => $lang['meta_title'],
'meta_description' => $lang['meta_description'],
'meta_keywords' => $lang['meta_keywords'],
'seo_link' => $lang['seo_link'],
'copy_from' => $lang['copy_from'],
'warehouse_message_zero' => $lang['warehouse_message_zero'],
'warehouse_message_nonzero'=> $lang['warehouse_message_nonzero'],
'canonical' => $lang['canonical'],
'xml_name' => $lang['xml_name'],
] );
}
}
// Import images
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
if ( is_array( $images ) ) {
foreach ( $images as $image ) {
$imageUrl = 'https://' . $settings['domain'] . $image['src'];
$ch = curl_init( $imageUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );
$imageData = curl_exec( $ch );
curl_close( $ch );
$imageName = basename( $imageUrl );
$imageDir = '../upload/product_images/product_' . $newProductId;
$imagePath = $imageDir . '/' . $imageName;
if ( !file_exists( $imageDir ) )
mkdir( $imageDir, 0777, true );
file_put_contents( $imagePath, $imageData );
$this->db->insert( 'pp_shop_products_images', [
'product_id' => $newProductId,
'src' => '/upload/product_images/product_' . $newProductId . '/' . $imageName,
'o' => $image['o'],
] );
}
}
return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ];
}
}

View File

@@ -0,0 +1,429 @@
<?php
namespace Domain\Languages;
class LanguagesRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'o',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'pl.id',
'name' => 'pl.name',
'status' => 'pl.status',
'start' => 'pl.start',
'o' => 'pl.o',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pl.o';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'pl.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'pl.status = :status';
$params[':status'] = (int)$status;
}
$start = trim((string)($filters['start'] ?? ''));
if ($start === '0' || $start === '1') {
$where[] = 'pl.start = :start';
$params[':start'] = (int)$start;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_langs AS pl
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pl.id,
pl.name,
pl.status,
pl.start,
pl.o
FROM pp_langs AS pl
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pl.o ASC, pl.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listTranslationsForAdmin(
array $filters,
string $sortColumn = 'text',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'plt.id',
'text' => 'plt.text',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'plt.text';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$text = trim((string)($filters['text'] ?? ''));
if ($text !== '') {
if (strlen($text) > 255) {
$text = substr($text, 0, 255);
}
$where[] = 'plt.text LIKE :text';
$params[':text'] = '%' . $text . '%';
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_langs_translations AS plt
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
plt.id,
plt.text
FROM pp_langs_translations AS plt
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, plt.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
public function languageDetails(string $languageId): ?array
{
$language = $this->db->get('pp_langs', '*', ['id' => $languageId]);
return $language ?: null;
}
public function translationDetails(int $translationId): ?array
{
$translation = $this->db->get('pp_langs_translations', '*', ['id' => $translationId]);
return $translation ?: null;
}
public function maxOrder(): int
{
$max = $this->db->max('pp_langs', 'o');
return $max ? (int)$max : 0;
}
public function languagesList(bool $onlyActive = false): array
{
$where = [];
if ($onlyActive) {
$where['status'] = 1;
}
$rows = $this->db->select('pp_langs', '*', array_merge(['ORDER' => ['o' => 'ASC']], $where));
return is_array($rows) ? $rows : [];
}
public function defaultLanguageId(): string
{
$languages = $this->languagesList();
if (empty($languages)) {
return 'pl';
}
foreach ($languages as $language) {
if ((int)($language['start'] ?? 0) === 1 && !empty($language['id'])) {
return (string)$language['id'];
}
}
if (!empty($languages[0]['id'])) {
return (string)$languages[0]['id'];
}
return 'pl';
}
public function deleteLanguage(string $languageId): bool
{
$languageId = $this->sanitizeLanguageId($languageId);
if ($languageId === null) {
return false;
}
if ((int)$this->db->count('pp_langs') <= 1) {
return false;
}
if (!$this->db->count('pp_langs', ['id' => $languageId])) {
return false;
}
$dropResult = $this->db->query('ALTER TABLE pp_langs_translations DROP COLUMN `' . $languageId . '`');
if (!$dropResult) {
return false;
}
$deleteResult = $this->db->delete('pp_langs', ['id' => $languageId]);
if (!$deleteResult) {
return false;
}
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function saveLanguage(string $languageId, string $name, $status, $start, int $order): ?string
{
$languageId = $this->sanitizeLanguageId($languageId);
if ($languageId === null) {
return null;
}
$statusVal = $this->toSwitchValue($status);
$startVal = $this->toSwitchValue($start);
$exists = (bool)$this->db->count('pp_langs', ['id' => $languageId]);
if ($startVal === 1) {
$this->db->update('pp_langs', ['start' => 0], ['id[!]' => $languageId]);
}
if ($exists) {
$this->db->update('pp_langs', [
'status' => $statusVal,
'start' => $startVal,
'name' => $name,
'o' => $order,
], [
'id' => $languageId,
]);
} else {
$addResult = $this->db->query('ALTER TABLE pp_langs_translations ADD COLUMN `' . $languageId . '` TEXT NULL DEFAULT NULL');
if (!$addResult) {
return null;
}
$insertResult = $this->db->insert('pp_langs', [
'id' => $languageId,
'name' => $name,
'status' => $statusVal,
'start' => $startVal,
'o' => $order,
]);
if (!$insertResult) {
return null;
}
}
if (!(int)$this->db->count('pp_langs', ['start' => 1])) {
$idTmp = (string)$this->db->get('pp_langs', 'id', ['ORDER' => ['o' => 'ASC']]);
if ($idTmp !== '') {
$this->db->update('pp_langs', ['start' => 1], ['id' => $idTmp]);
}
}
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $languageId;
}
public function deleteTranslation(int $translationId): bool
{
$result = $this->db->delete('pp_langs_translations', ['id' => $translationId]);
return (bool)$result;
}
public function saveTranslation(int $translationId, string $text, array $translations): ?int
{
if ($translationId > 0) {
$this->db->update('pp_langs_translations', ['text' => $text], ['id' => $translationId]);
} else {
$insertResult = $this->db->insert('pp_langs_translations', ['text' => $text]);
if (!$insertResult) {
return null;
}
$translationId = (int)$this->db->id();
}
if ($translationId <= 0) {
return null;
}
foreach ($translations as $languageId => $value) {
$safeLanguageId = $this->sanitizeLanguageId((string)$languageId);
if ($safeLanguageId === null) {
continue;
}
$this->db->update('pp_langs_translations', [
$safeLanguageId => (string)$value,
], [
'id' => $translationId,
]);
}
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $translationId;
}
/**
* Zwraca ID domyslnego jezyka (z flaga start=1) z cache Redis.
*/
public function defaultLanguage(): string
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'Domain\Languages\LanguagesRepository::defaultLanguage';
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$stmt = $this->db->query(
'SELECT id FROM pp_langs WHERE status = 1 ORDER BY start DESC, o ASC LIMIT 1'
);
$results = $stmt ? $stmt->fetchAll() : [];
$defaultLanguage = $results[0][0] ?? 'pl';
$cacheHandler->set($cacheKey, $defaultLanguage);
return $defaultLanguage;
}
/**
* Zwraca liste aktywnych jezykow z cache Redis.
*/
public function activeLanguages(): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'Domain\Languages\LanguagesRepository::activeLanguages';
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$activeLanguages = $this->db->select(
'pp_langs',
['id', 'name'],
['status' => 1, 'ORDER' => ['o' => 'ASC']]
);
if (!is_array($activeLanguages)) {
$activeLanguages = [];
}
$cacheHandler->set($cacheKey, $activeLanguages);
return $activeLanguages;
}
/**
* Zwraca tlumaczenia dla danego jezyka z cache Redis.
*/
public function translations(string $language = 'pl'): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "Domain\Languages\LanguagesRepository::translations:$language";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$translations = ['0' => $language];
$results = $this->db->select('pp_langs_translations', ['text', $language]);
if (is_array($results)) {
foreach ($results as $row) {
$translations[$row['text']] = $row[$language];
}
}
$cacheHandler->set($cacheKey, $translations);
return $translations;
}
private function sanitizeLanguageId(string $languageId): ?string
{
$languageId = strtolower(trim($languageId));
if (!preg_match('/^[a-z]{2}$/', $languageId)) {
return null;
}
return $languageId;
}
private function toSwitchValue($value): int
{
return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0;
}
}

View File

@@ -0,0 +1,540 @@
<?php
namespace Domain\Layouts;
class LayoutsRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function delete(int $layoutId): bool
{
if ((int)$this->db->count('pp_layouts') <= 1) {
return false;
}
$deleted = (bool)$this->db->delete('pp_layouts', ['id' => $layoutId]);
if ($deleted) {
\Shared\Helpers\Helpers::delete_dir('../temp/');
$this->clearFrontLayoutsCache();
}
return $deleted;
}
public function find(int $layoutId): array
{
$layout = $this->db->get('pp_layouts', '*', ['id' => $layoutId]);
if (!is_array($layout)) {
return $this->defaultLayout();
}
$layout['pages'] = $this->db->select('pp_layouts_pages', 'page_id', ['layout_id' => $layoutId]);
$layout['categories'] = $this->db->select('pp_layouts_categories', 'category_id', ['layout_id' => $layoutId]);
return $layout;
}
public function save(array $data): ?int
{
$layoutId = (int)($data['id'] ?? 0);
$status = $this->toSwitchValue($data['status'] ?? 0);
$categoriesDefault = $this->toSwitchValue($data['categories_default'] ?? 0);
$row = [
'name' => (string)($data['name'] ?? ''),
'html' => (string)($data['html'] ?? ''),
'css' => (string)($data['css'] ?? ''),
'js' => (string)($data['js'] ?? ''),
'status' => $status,
'categories_default' => $categoriesDefault,
];
if ($status === 1) {
$this->db->update('pp_layouts', ['status' => 0]);
}
if ($categoriesDefault === 1) {
$this->db->update('pp_layouts', ['categories_default' => 0]);
}
if ($layoutId <= 0) {
$this->db->insert('pp_layouts', $row);
$layoutId = (int)$this->db->id();
if ($layoutId <= 0) {
return null;
}
} else {
$this->db->update('pp_layouts', $row, ['id' => $layoutId]);
}
$this->db->delete('pp_layouts_pages', ['layout_id' => $layoutId]);
$this->syncPages($layoutId, $data['pages'] ?? []);
$this->db->delete('pp_layouts_categories', ['layout_id' => $layoutId]);
$this->syncCategories($layoutId, $data['categories'] ?? []);
\Shared\Helpers\Helpers::delete_dir('../temp/');
$this->clearFrontLayoutsCache();
return $layoutId;
}
public function listAll(): array
{
$rows = $this->db->select('pp_layouts', '*', ['ORDER' => ['name' => 'ASC']]);
return is_array($rows) ? $rows : [];
}
public function menusWithPages(): array
{
$menus = $this->db->select('pp_menus', '*', ['ORDER' => ['id' => 'ASC']]);
if (!is_array($menus)) {
return [];
}
foreach ($menus as $key => $menu) {
$menuId = (int)($menu['id'] ?? 0);
$menus[$key]['pages'] = $this->menuPages($menuId, null);
}
return $menus;
}
public function categoriesTree($parentId = null): array
{
$rows = $this->db->select('pp_shop_categories', ['id'], [
'parent_id' => $parentId,
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$categories = [];
foreach ($rows as $row) {
$categoryId = (int)($row['id'] ?? 0);
if ($categoryId <= 0) {
continue;
}
$category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]);
if (!is_array($category)) {
continue;
}
$translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]);
$category['languages'] = [];
if (is_array($translations)) {
foreach ($translations as $translation) {
$langId = (string)($translation['lang_id'] ?? '');
if ($langId !== '') {
$category['languages'][$langId] = $translation;
}
}
}
$category['subcategories'] = $this->categoriesTree($categoryId);
$categories[] = $category;
}
return $categories;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'pl.id',
'name' => 'pl.name',
'status' => 'pl.status',
'categories_default' => 'pl.categories_default',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pl.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'pl.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'pl.status = :status';
$params[':status'] = (int)$status;
}
$categoriesDefault = trim((string)($filters['categories_default'] ?? ''));
if ($categoriesDefault === '0' || $categoriesDefault === '1') {
$where[] = 'pl.categories_default = :categories_default';
$params[':categories_default'] = (int)$categoriesDefault;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_layouts AS pl
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pl.id,
pl.name,
pl.status,
pl.categories_default
FROM pp_layouts AS pl
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pl.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
// ── Frontend methods ──────────────────────────────────────────
public function categoryDefaultLayoutId()
{
return $this->db->get('pp_layouts', 'id', ['categories_default' => 1]);
}
public function getDefaultLayout(): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'LayoutsRepository::getDefaultLayout';
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached) && !empty($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
if (!is_array($layout) || empty($layout)) {
return null;
}
$cacheHandler->set($cacheKey, $layout);
return $layout;
}
public function getProductLayout(int $productId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "LayoutsRepository::getProductLayout:$productId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached) && !empty($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$stmt = $this->db->query(
"SELECT pp_layouts.*
FROM pp_layouts
JOIN pp_shop_products ON pp_layouts.id = pp_shop_products.layout_id
WHERE pp_shop_products.id = " . (int)$productId . "
ORDER BY pp_layouts.id DESC"
);
$layoutRows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
if (is_array($layoutRows) && isset($layoutRows[0])) {
$layout = $layoutRows[0];
} else {
$stmt2 = $this->db->query(
"SELECT pp_layouts.*
FROM pp_layouts
JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id
JOIN pp_shop_products_categories ON pp_shop_products_categories.category_id = pp_layouts_categories.category_id
WHERE pp_shop_products_categories.product_id = " . (int)$productId . "
ORDER BY pp_shop_products_categories.o ASC, pp_layouts.id DESC"
);
$layoutRows = $stmt2 ? $stmt2->fetchAll(\PDO::FETCH_ASSOC) : [];
if (is_array($layoutRows) && isset($layoutRows[0])) {
$layout = $layoutRows[0];
} else {
$layout = $this->db->get('pp_layouts', '*', ['categories_default' => 1]);
}
}
if (!$layout) {
$layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
}
if (!is_array($layout) || empty($layout)) {
return null;
}
$cacheHandler->set($cacheKey, $layout);
return $layout;
}
public function getArticleLayout(int $articleId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "LayoutsRepository::getArticleLayout:$articleId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$layout = $this->db->get('pp_layouts', ['[><]pp_articles' => ['id' => 'layout_id']], '*', ['pp_articles.id' => (int)$articleId]);
if (is_array($layout)) {
$cacheHandler->set($cacheKey, $layout);
return $layout;
}
return null;
}
public function getCategoryLayout(int $categoryId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "LayoutsRepository::getCategoryLayout:$categoryId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached) && !empty($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$stmt = $this->db->query(
"SELECT pp_layouts.*
FROM pp_layouts
JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id
WHERE pp_layouts_categories.category_id = " . (int)$categoryId . "
ORDER BY pp_layouts.id DESC"
);
$layoutRows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
if (is_array($layoutRows) && isset($layoutRows[0])) {
$layout = $layoutRows[0];
} else {
$layout = $this->db->get('pp_layouts', '*', ['categories_default' => 1]);
}
if (!$layout) {
$layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
}
if (!is_array($layout) || empty($layout)) {
return null;
}
$cacheHandler->set($cacheKey, $layout);
return $layout;
}
public function getActiveLayout(int $pageId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "LayoutsRepository::getActiveLayout:$pageId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$layout = $this->db->get('pp_layouts', ['[><]pp_layouts_pages' => ['id' => 'layout_id']], '*', ['page_id' => (int)$pageId]);
if (!$layout) {
$layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
}
if (is_array($layout)) {
$cacheHandler->set($cacheKey, $layout);
return $layout;
}
return null;
}
// ── Private helpers ──────────────────────────────────────────
private function syncPages(int $layoutId, $pages): void
{
foreach ($this->normalizeIds($pages) as $pageId) {
$this->db->delete('pp_layouts_pages', ['page_id' => $pageId]);
$this->db->insert('pp_layouts_pages', [
'layout_id' => $layoutId,
'page_id' => $pageId,
]);
}
}
private function syncCategories(int $layoutId, $categories): void
{
foreach ($this->normalizeIds($categories) as $categoryId) {
$this->db->delete('pp_layouts_categories', ['category_id' => $categoryId]);
$this->db->insert('pp_layouts_categories', [
'layout_id' => $layoutId,
'category_id' => $categoryId,
]);
}
}
/**
* @return int[]
*/
private function normalizeIds($values): array
{
if (!is_array($values)) {
$values = [$values];
}
$ids = [];
foreach ($values as $value) {
$id = (int)$value;
if ($id > 0) {
$ids[$id] = $id;
}
}
return array_values($ids);
}
private function toSwitchValue($value): int
{
return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0;
}
private function defaultLayout(): array
{
return [
'id' => 0,
'name' => '',
'status' => 0,
'categories_default' => 0,
'html' => '',
'css' => '',
'js' => '',
'pages' => [],
'categories' => [],
];
}
private function clearFrontLayoutsCache(): void
{
if (!class_exists('\Shared\Cache\CacheHandler')) {
return;
}
try {
$cacheHandler = new \Shared\Cache\CacheHandler();
if (method_exists($cacheHandler, 'deletePattern')) {
$cacheHandler->deletePattern('*Layouts::*');
}
} catch (\Throwable $e) {
// Inwalidacja cache nie moze blokowac zapisu/usuwania.
}
}
private function menuPages(int $menuId, $parentId = null): array
{
if ($menuId <= 0) {
return [];
}
$rows = $this->db->select('pp_pages', ['id', 'menu_id', 'status', 'parent_id', 'start'], [
'AND' => [
'menu_id' => $menuId,
'parent_id' => $parentId,
],
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$pages = [];
foreach ($rows as $row) {
$pageId = (int)($row['id'] ?? 0);
if ($pageId <= 0) {
continue;
}
$row['title'] = $this->pageTitle($pageId);
$row['subpages'] = $this->menuPages($menuId, $pageId);
$pages[] = $row;
}
return $pages;
}
private function pageTitle(int $pageId): string
{
$result = $this->db->select('pp_pages_langs', [
'[><]pp_langs' => ['lang_id' => 'id'],
], 'title', [
'AND' => [
'page_id' => $pageId,
'title[!]' => '',
],
'ORDER' => ['o' => 'ASC'],
'LIMIT' => 1,
]);
if (is_array($result) && isset($result[0])) {
return (string)$result[0];
}
return '';
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Domain\Newsletter;
class NewsletterPreviewRenderer
{
public function render(array $articles, array $settings, ?array $template, string $dates = ''): string
{
$out = '<div style="border-bottom: 1px solid #ccc;">';
$out .= !empty($settings['newsletter_header'])
? (string)$settings['newsletter_header']
: '<p style="text-align: center;">--- brak zdefiniowanego naglowka ---</p>';
$out .= '</div>';
$out .= '<div style="border-bottom: 1px solid #ccc; padding: 10px 0 0 0;">';
if (is_array($template) && !empty($template)) {
$out .= '<div style="padding: 10px; background: #F1F1F1; margin-bottom: 10px">';
$out .= (string)($template['text'] ?? '');
$out .= '</div>';
}
if (is_array($articles) && !empty($articles)) {
foreach ($articles as $article) {
$articleId = (int)($article['id'] ?? 0);
$title = (string)($article['language']['title'] ?? '');
$seoLink = trim((string)($article['language']['seo_link'] ?? ''));
$url = $seoLink !== '' ? $seoLink : ('a-' . $articleId . '-' . \Shared\Helpers\Helpers::seo($title));
$entry = !empty($article['language']['entry'])
? (string)$article['language']['entry']
: (string)($article['language']['text'] ?? '');
$out .= '<div style="padding: 10px; background: #F1F1F1; margin-bottom: 10px">';
$out .= '<a href="http://' . htmlspecialchars((string)($_SERVER['SERVER_NAME'] ?? ''), ENT_QUOTES, 'UTF-8') . '/'
. htmlspecialchars($url, ENT_QUOTES, 'UTF-8')
. '" title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8')
. '" style="margin-bottom: 10px; display: block; font-size: 14px; color: #5b7fb1; font-weight: 600;">'
. htmlspecialchars($title, ENT_QUOTES, 'UTF-8')
. '</a>';
$out .= '<div>' . $entry . '</div>';
$out .= '<div style="clear: both;"></div>';
$out .= '</div>';
}
} elseif (trim($dates) !== '') {
$out .= '<div style="padding: 10px; background: #F1F1F1; margin-bottom: 10px; text-align: center;">';
$out .= '--- brak artykulow w danym okresie ---';
$out .= '</div>';
}
$out .= '</div>';
$out .= '<div style="border-bottom: 1px solid #ccc; padding: 10px 0 0 0;">';
$out .= !empty($settings['newsletter_footer'])
? (string)$settings['newsletter_footer']
: '<p style="text-align: center;">--- brak zdefiniowanej stopki ---</p>';
$out .= '</div>';
return $out;
}
}

View File

@@ -0,0 +1,445 @@
<?php
namespace Domain\Newsletter;
use Domain\Settings\SettingsRepository;
use Domain\Article\ArticleRepository;
class NewsletterRepository
{
private const MAX_PER_PAGE = 100;
private $db;
private SettingsRepository $settingsRepository;
private ?ArticleRepository $articleRepository;
private ?NewsletterPreviewRenderer $previewRenderer;
public function __construct(
$db,
?SettingsRepository $settingsRepository = null,
?ArticleRepository $articleRepository = null,
?NewsletterPreviewRenderer $previewRenderer = null
) {
$this->db = $db;
$this->settingsRepository = $settingsRepository ?? new SettingsRepository($db);
$this->articleRepository = $articleRepository;
$this->previewRenderer = $previewRenderer;
}
private function getArticleRepository(): ArticleRepository
{
return $this->articleRepository ?? ($this->articleRepository = new ArticleRepository($this->db));
}
private function getPreviewRenderer(): NewsletterPreviewRenderer
{
return $this->previewRenderer ?? ($this->previewRenderer = new NewsletterPreviewRenderer());
}
public function getSettings(): array
{
return $this->settingsRepository->getSettings();
}
public function saveSettings(array $values): bool
{
$this->settingsRepository->updateSetting('newsletter_footer', (string)($values['newsletter_footer'] ?? ''));
$this->settingsRepository->updateSetting('newsletter_header', (string)($values['newsletter_header'] ?? ''));
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function queueSend(string $dates = '', int $templateId = 0): bool
{
$subscribers = $this->db->select('pp_newsletter', 'email', ['status' => 1]);
if (!is_array($subscribers) || empty($subscribers)) {
return true;
}
$cleanDates = trim($dates);
$templateId = $templateId > 0 ? $templateId : 0;
foreach ($subscribers as $subscriber) {
$email = is_array($subscriber) ? (string)($subscriber['email'] ?? '') : (string)$subscriber;
if ($email === '') {
continue;
}
$this->db->insert('pp_newsletter_send', [
'email' => $email,
'dates' => $cleanDates,
'id_template' => $templateId > 0 ? $templateId : null,
]);
}
return true;
}
public function templateByName(string $templateName): string
{
return (string)$this->db->get('pp_newsletter_templates', 'text', ['name' => $templateName]);
}
public function templateDetails(int $templateId): ?array
{
if ($templateId <= 0) {
return null;
}
$row = $this->db->get('pp_newsletter_templates', '*', ['id' => $templateId]);
if (!is_array($row)) {
return null;
}
return $row;
}
public function isAdminTemplate(int $templateId): bool
{
$isAdmin = $this->db->get('pp_newsletter_templates', 'is_admin', ['id' => $templateId]);
return (int)$isAdmin === 1;
}
public function deleteTemplate(int $templateId): bool
{
if ($templateId <= 0 || $this->isAdminTemplate($templateId)) {
return false;
}
return (bool)$this->db->delete('pp_newsletter_templates', ['id' => $templateId]);
}
public function saveTemplate(int $templateId, string $name, string $text): ?int
{
$templateId = max(0, $templateId);
$name = trim($name);
if ($templateId <= 0) {
if ($name === '') {
return null;
}
$ok = $this->db->insert('pp_newsletter_templates', [
'name' => $name,
'text' => $text,
'is_admin' => 0,
]);
if (!$ok) {
return null;
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return (int)$this->db->id();
}
$current = $this->templateDetails($templateId);
if (!is_array($current)) {
return null;
}
if ((int)($current['is_admin'] ?? 0) === 1) {
$name = (string)($current['name'] ?? '');
}
$this->db->update('pp_newsletter_templates', [
'name' => $name,
'text' => $text,
], [
'id' => $templateId,
]);
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $templateId;
}
public function listTemplatesSimple(bool $adminTemplates = false): array
{
$rows = $this->db->select('pp_newsletter_templates', '*', [
'is_admin' => $adminTemplates ? 1 : 0,
'ORDER' => ['name' => 'ASC'],
]);
return is_array($rows) ? $rows : [];
}
public function deleteSubscriber(int $subscriberId): bool
{
if ($subscriberId <= 0) {
return false;
}
return (bool)$this->db->delete('pp_newsletter', ['id' => $subscriberId]);
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listSubscribersForAdmin(
array $filters,
string $sortColumn = 'email',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'pn.id',
'email' => 'pn.email',
'status' => 'pn.status',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pn.email';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$email = trim((string)($filters['email'] ?? ''));
if ($email !== '') {
if (strlen($email) > 255) {
$email = substr($email, 0, 255);
}
$where[] = 'pn.email LIKE :email';
$params[':email'] = '%' . $email . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'pn.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_newsletter AS pn
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pn.id,
pn.email,
pn.status
FROM pp_newsletter AS pn
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pn.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listTemplatesForAdmin(
bool $adminTemplates,
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'pnt.id',
'name' => 'pnt.name',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pnt.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['pnt.is_admin = :is_admin'];
$params = [':is_admin' => $adminTemplates ? 1 : 0];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'pnt.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_newsletter_templates AS pnt
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pnt.id,
pnt.name,
pnt.is_admin
FROM pp_newsletter_templates AS pnt
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pnt.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
// ── Frontend methods ──
public function unsubscribe(string $hash): bool
{
$id = $this->db->get('pp_newsletter', 'id', ['hash' => $hash]);
if (!$id) {
return false;
}
$this->db->delete('pp_newsletter', ['id' => $id]);
return true;
}
public function confirmSubscription(string $hash): bool
{
$id = $this->db->get('pp_newsletter', 'id', ['AND' => ['hash' => $hash, 'status' => 0]]);
if (!$id) {
return false;
}
$this->db->update('pp_newsletter', ['status' => 1], ['id' => $id]);
return true;
}
public function getHashByEmail(string $email): ?string
{
$hash = $this->db->get('pp_newsletter', 'hash', ['email' => $email]);
return $hash ? (string)$hash : null;
}
public function removeByEmail(string $email): bool
{
if (!$this->db->get('pp_newsletter', 'id', ['email' => $email])) {
return false;
}
return (bool)$this->db->delete('pp_newsletter', ['email' => $email]);
}
public function signup(string $email, string $serverName, bool $ssl, array $settings): bool
{
if (!\Shared\Helpers\Helpers::email_check($email)) {
return false;
}
if ($this->db->get('pp_newsletter', 'id', ['email' => $email])) {
return false;
}
$hash = md5(time() . $email);
$text = ($settings['newsletter_header'] ?? '');
$text .= $this->templateByName('#potwierdzenie-zapisu-do-newslettera');
$text .= ($settings['newsletter_footer'] ?? '');
$base = $ssl ? 'https' : 'http';
$link = '/newsletter/confirm/hash=' . $hash;
$text = str_replace('[LINK]', $link, $text);
$text = str_replace('[WYPISZ_SIE]', '', $text);
$text = preg_replace(
"-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i",
"$1" . $base . "://" . $serverName . "$2$4",
$text
);
$text = preg_replace(
"-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i",
"$1" . $base . "://" . $serverName . "$2$4",
$text
);
$lang = \Shared\Helpers\Helpers::get_session('lang-' . \Shared\Helpers\Helpers::get_session('current-lang'));
$subject = $lang['potwierdz-zapisanie-sie-do-newslettera'] ?? 'Newsletter';
\Shared\Helpers\Helpers::send_email($email, $subject, $text);
$this->db->insert('pp_newsletter', ['email' => $email, 'hash' => $hash, 'status' => 0]);
return true;
}
public function sendQueued(int $limit, string $serverName, bool $ssl, string $unsubscribeLabel): bool
{
$settingsDetails = $this->settingsRepository->getSettings();
$results = $this->db->query('SELECT * FROM pp_newsletter_send ORDER BY id ASC LIMIT ' . (int)$limit);
$results = $results ? $results->fetchAll() : [];
if (!is_array($results) || empty($results)) {
return false;
}
$renderer = $this->getPreviewRenderer();
$articleRepo = $this->getArticleRepository();
foreach ($results as $row) {
$dates = explode(' - ', $row['dates']);
$articles = [];
if (isset($dates[0], $dates[1])) {
$articles = $articleRepo->articlesByDateAdd($dates[0], $dates[1]);
}
$text = $renderer->render(
is_array($articles) ? $articles : [],
$settingsDetails,
$this->templateDetails((int)$row['id_template']),
(string)$row['dates']
);
$base = $ssl ? 'https' : 'http';
$text = preg_replace(
"-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i",
"$1" . $base . "://" . $serverName . "$2$4",
$text
);
$text = preg_replace(
"-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|http://).)*)(['\"][^>]*>)-i",
"$1" . $base . "://" . $serverName . "$2$4",
$text
);
$hash = $this->getHashByEmail($row['email']);
$link = $base . "://" . $serverName . '/newsletter/unsubscribe/hash=' . $hash;
$text = str_replace('[WYPISZ_SIE]', '<a href="' . $link . '">' . $unsubscribeLabel . '</a>', $text);
\Shared\Helpers\Helpers::send_email($row['email'], 'Newsletter ze strony: ' . $serverName, $text);
$this->db->delete('pp_newsletter_send', ['id' => $row['id']]);
}
return true;
}
}

View File

@@ -0,0 +1,803 @@
<?php
namespace Domain\Order;
class OrderAdminService
{
private OrderRepository $orders;
private $productRepo;
private $settingsRepo;
private $transportRepo;
public function __construct(
OrderRepository $orders,
$productRepo = null,
$settingsRepo = null,
$transportRepo = null
) {
$this->orders = $orders;
$this->productRepo = $productRepo;
$this->settingsRepo = $settingsRepo;
$this->transportRepo = $transportRepo;
}
public function details(int $orderId): array
{
return $this->orders->findForAdmin($orderId);
}
public function statuses(): array
{
return $this->orders->orderStatuses();
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'date_order',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
return $this->orders->listForAdmin($filters, $sortColumn, $sortDir, $page, $perPage);
}
public function nextOrderId(int $orderId): ?int
{
return $this->orders->nextOrderId($orderId);
}
public function prevOrderId(int $orderId): ?int
{
return $this->orders->prevOrderId($orderId);
}
public function saveNotes(int $orderId, string $notes): bool
{
return $this->orders->saveNotes($orderId, $notes);
}
public function saveOrderByAdmin(array $input): bool
{
$saved = $this->orders->saveOrderByAdmin(
(int)($input['order_id'] ?? 0),
(string)($input['client_name'] ?? ''),
(string)($input['client_surname'] ?? ''),
(string)($input['client_street'] ?? ''),
(string)($input['client_postal_code'] ?? ''),
(string)($input['client_city'] ?? ''),
(string)($input['client_email'] ?? ''),
(string)($input['firm_name'] ?? ''),
(string)($input['firm_street'] ?? ''),
(string)($input['firm_postal_code'] ?? ''),
(string)($input['firm_city'] ?? ''),
(string)($input['firm_nip'] ?? ''),
(int)($input['transport_id'] ?? 0),
(string)($input['inpost_paczkomat'] ?? ''),
(int)($input['payment_method_id'] ?? 0)
);
return $saved;
}
// =========================================================================
// Order products management (admin)
// =========================================================================
public function searchProducts(string $query, string $langId): array
{
if (!$this->productRepo || trim($query) === '') {
return [];
}
$rows = $this->productRepo->searchProductByNameAjax($query, $langId);
$results = [];
foreach ($rows as $row) {
$productId = (int)($row['product_id'] ?? 0);
if ($productId <= 0) {
continue;
}
$product = $this->productRepo->findCached($productId, $langId);
if (!is_array($product)) {
continue;
}
$name = isset($product['language']['name']) ? (string)$product['language']['name'] : '';
$img = $this->productRepo->getProductImg($productId);
$results[] = [
'product_id' => $productId,
'parent_product_id' => (int)($product['parent_id'] ?? 0),
'name' => $name,
'sku' => (string)($product['sku'] ?? ''),
'ean' => (string)($product['ean'] ?? ''),
'price_brutto' => (float)($product['price_brutto'] ?? 0),
'price_brutto_promo' => (float)($product['price_brutto_promo'] ?? 0),
'vat' => (float)($product['vat'] ?? 0),
'quantity' => (int)($product['quantity'] ?? 0),
'image' => $img,
];
}
return $results;
}
public function saveOrderProducts(int $orderId, array $productsData): bool
{
if ($orderId <= 0) {
return false;
}
$currentProducts = $this->orders->orderProducts($orderId);
$currentById = [];
foreach ($currentProducts as $cp) {
$currentById[(int)$cp['id']] = $cp;
}
$submittedIds = [];
foreach ($productsData as $item) {
$orderProductId = (int)($item['order_product_id'] ?? 0);
$deleted = !empty($item['delete']);
if ($deleted && $orderProductId > 0) {
// Usunięcie — zwrot na stan
$existing = isset($currentById[$orderProductId]) ? $currentById[$orderProductId] : null;
if ($existing) {
$this->adjustStock((int)$existing['product_id'], (int)$existing['quantity']);
}
$this->orders->deleteOrderProduct($orderProductId);
$submittedIds[] = $orderProductId;
continue;
}
if ($deleted) {
continue;
}
if ($orderProductId > 0 && isset($currentById[$orderProductId])) {
// Istniejący produkt — aktualizacja
$existing = $currentById[$orderProductId];
$newQty = max(1, (int)($item['quantity'] ?? 1));
$oldQty = (int)$existing['quantity'];
$qtyDiff = $oldQty - $newQty;
$update = [
'quantity' => $newQty,
'price_brutto' => (float)($item['price_brutto'] ?? $existing['price_brutto']),
'price_brutto_promo' => (float)($item['price_brutto_promo'] ?? $existing['price_brutto_promo']),
];
$this->orders->updateOrderProduct($orderProductId, $update);
// Korekta stanu: qtyDiff > 0 = zmniejszono ilość = zwrot na stan
if ($qtyDiff !== 0) {
$this->adjustStock((int)$existing['product_id'], $qtyDiff);
}
$submittedIds[] = $orderProductId;
} elseif ($orderProductId === 0) {
// Nowy produkt
$productId = (int)($item['product_id'] ?? 0);
$qty = max(1, (int)($item['quantity'] ?? 1));
$this->orders->addOrderProduct($orderId, [
'product_id' => $productId,
'parent_product_id' => (int)($item['parent_product_id'] ?? $productId),
'name' => (string)($item['name'] ?? ''),
'attributes' => '',
'vat' => (float)($item['vat'] ?? 0),
'price_brutto' => (float)($item['price_brutto'] ?? 0),
'price_brutto_promo' => (float)($item['price_brutto_promo'] ?? 0),
'quantity' => $qty,
'message' => '',
'custom_fields' => '',
]);
// Zmniejsz stan magazynowy
$this->adjustStock($productId, -$qty);
}
}
// Usunięte z formularza (nie przesłane) — zwrot na stan
foreach ($currentById as $cpId => $cp) {
if (!in_array($cpId, $submittedIds)) {
$this->adjustStock((int)$cp['product_id'], (int)$cp['quantity']);
$this->orders->deleteOrderProduct($cpId);
}
}
// Przelicz koszt dostawy (próg darmowej dostawy)
$this->recalculateTransportCost($orderId);
return true;
}
public function getFreeDeliveryThreshold(): float
{
if (!$this->settingsRepo) {
return 0.0;
}
return (float)$this->settingsRepo->getSingleValue('free_delivery');
}
private function adjustStock(int $productId, int $delta): void
{
if (!$this->productRepo || $productId <= 0 || $delta === 0) {
return;
}
$currentQty = $this->productRepo->getQuantity($productId);
if ($currentQty === null) {
return;
}
$newQty = max(0, $currentQty + $delta);
$this->productRepo->updateQuantity($productId, $newQty);
}
private function recalculateTransportCost(int $orderId): void
{
$order = $this->orders->findRawById($orderId);
if (!$order) {
return;
}
$transportId = (int)($order['transport_id'] ?? 0);
if ($transportId <= 0 || !$this->transportRepo || !$this->settingsRepo) {
return;
}
$transport = $this->transportRepo->findActiveById($transportId);
if (!is_array($transport)) {
return;
}
// Oblicz sumę produktów (bez dostawy)
$productsSummary = $this->calculateProductsTotal($orderId);
$freeDelivery = (float)$this->settingsRepo->getSingleValue('free_delivery');
if ((int)($transport['delivery_free'] ?? 0) === 1 && $freeDelivery > 0 && $productsSummary >= $freeDelivery) {
$transportCost = 0.0;
} else {
$transportCost = (float)($transport['cost'] ?? 0);
}
$this->orders->updateTransportCost($orderId, $transportCost);
}
private function calculateProductsTotal(int $orderId): float
{
$products = $this->orders->orderProducts($orderId);
$summary = 0.0;
foreach ($products as $row) {
$pricePromo = (float)($row['price_brutto_promo'] ?? 0);
$price = (float)($row['price_brutto'] ?? 0);
$quantity = (float)($row['quantity'] ?? 0);
if ($pricePromo > 0) {
$summary += $pricePromo * $quantity;
} else {
$summary += $price * $quantity;
}
}
return $summary;
}
public function changeStatus(int $orderId, int $status, bool $sendEmail): array
{
$order = $this->orders->findRawById($orderId);
if (!$order || (int)$order['status'] === $status) {
return ['result' => false];
}
$db = $this->orders->getDb();
if ($this->orders->updateOrderStatus($orderId, $status))
{
$this->orders->insertStatusHistory($orderId, $status, $sendEmail ? 1 : 0);
$response = ['result' => true];
if ($sendEmail)
{
$order['status'] = $status;
$response['email'] = $this->sendStatusChangeEmail($order);
}
// Apilo status sync
$this->syncApiloStatusIfNeeded($order, $status);
return $response;
}
return ['result' => false];
}
public function resendConfirmationEmail(int $orderId): bool
{
global $settings;
$db = $this->orders->getDb();
$order = $this->orders->orderDetailsFrontend($orderId);
if (!$order || !$order['id']) {
return false;
}
$coupon = (int)$order['coupon_id'] ? (new \Domain\Coupon\CouponRepository($db))->find((int)$order['coupon_id']) : null;
$mail_order = \Shared\Tpl\Tpl::view('shop-order/mail-summary', [
'settings' => $settings,
'order' => $order,
'coupon' => $coupon,
]);
$settings['ssl'] ? $base = 'https' : $base = 'http';
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order);
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order);
\Shared\Helpers\Helpers::send_email($order['client_email'], \Shared\Helpers\Helpers::lang('potwierdzenie-zamowienia-ze-sklepu') . ' ' . $settings['firm_name'], $mail_order);
\Shared\Helpers\Helpers::send_email($settings['contact_email'], 'Nowe zamówienie / ' . $settings['firm_name'] . ' / ' . $order['number'] . ' - ' . $order['client_surname'] . ' ' . $order['client_name'], $mail_order);
return true;
}
public function setOrderAsUnpaid(int $orderId): bool
{
$this->orders->setAsUnpaid($orderId);
return true;
}
public function setOrderAsPaid(int $orderId, bool $sendMail): bool
{
$order = $this->orders->findRawById($orderId);
if (!$order) {
return false;
}
// Apilo payment sync
$this->syncApiloPaymentIfNeeded($order);
// Mark as paid
$this->orders->setAsPaid($orderId);
// Set status to 1 (opłacone) without email
$this->changeStatus($orderId, 1, false);
// Set status to 4 (przyjęte do realizacji) with email
$this->changeStatus($orderId, 4, $sendMail);
return true;
}
public function sendOrderToApilo(int $orderId): bool
{
global $mdb;
if ($orderId <= 0) {
return false;
}
$order = $this->orders->findForAdmin($orderId);
if (empty($order) || empty($order['apilo_order_id'])) {
return false;
}
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
$accessToken = $integrationsRepository -> apiloGetAccessToken();
if (!$accessToken) {
return false;
}
$newStatus = 8;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/status/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'id' => (int)$order['apilo_order_id'],
'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository($mdb) )->getApiloStatusId( (int)$newStatus ),
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json',
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$apiloResultRaw = curl_exec($ch);
$apiloResult = json_decode((string)$apiloResultRaw, true);
if (!is_array($apiloResult) || (int)($apiloResult['updates'] ?? 0) !== 1) {
curl_close($ch);
return false;
}
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_orders' AND COLUMN_NAME != 'id'";
$stmt = $mdb->query($query);
$columns = $stmt ? $stmt->fetchAll(\PDO::FETCH_COLUMN) : [];
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_orders (' . $columnsList . ') SELECT ' . $columnsList . ' FROM pp_shop_orders pso WHERE pso.id = ' . $orderId);
$newOrderId = (int)$mdb->id();
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_products' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'";
$stmt2 = $mdb->query($query);
$columns = $stmt2 ? $stmt2->fetchAll(\PDO::FETCH_COLUMN) : [];
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_order_products (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_products psop WHERE psop.order_id = ' . $orderId);
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_statuses' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'";
$stmt3 = $mdb->query($query);
$columns = $stmt3 ? $stmt3->fetchAll(\PDO::FETCH_COLUMN) : [];
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_order_statuses (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_statuses psos WHERE psos.order_id = ' . $orderId);
$mdb->delete('pp_shop_orders', ['id' => $orderId]);
$mdb->delete('pp_shop_order_products', ['order_id' => $orderId]);
$mdb->delete('pp_shop_order_statuses', ['order_id' => $orderId]);
$mdb->update('pp_shop_orders', ['apilo_order_id' => null], ['id' => $newOrderId]);
curl_close($ch);
return true;
}
public function toggleTrustmateSend(int $orderId): array
{
$newValue = $this->orders->toggleTrustmateSend($orderId);
if ($newValue === null) {
return [
'result' => false,
];
}
return [
'result' => true,
'trustmate_send' => $newValue,
];
}
public function deleteOrder(int $orderId): bool
{
return $this->orders->deleteOrder($orderId);
}
// =========================================================================
// Apilo sync queue (migrated from \shop\Order)
// =========================================================================
private const APILO_SYNC_QUEUE_FILE = '/temp/apilo-sync-queue.json';
public function processApiloSyncQueue(int $limit = 10): int
{
$queue = self::loadApiloSyncQueue();
if (!\Shared\Helpers\Helpers::is_array_fix($queue)) {
return 0;
}
$processed = 0;
foreach ($queue as $key => $task)
{
if ($processed >= $limit) {
break;
}
$order_id = (int)($task['order_id'] ?? 0);
if ($order_id <= 0) {
unset($queue[$key]);
continue;
}
$order = $this->orders->findRawById($order_id);
if (!$order) {
unset($queue[$key]);
continue;
}
$error = '';
$sync_failed = false;
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
if ($payment_pending && (int)$order['apilo_order_id']) {
if (!$this->syncApiloPayment($order)) {
$sync_failed = true;
$error = 'payment_sync_failed';
}
}
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) {
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
$sync_failed = true;
$error = 'status_sync_failed';
}
}
if ($sync_failed) {
$task['attempts'] = (int)($task['attempts'] ?? 0) + 1;
$task['last_error'] = $error;
$task['updated_at'] = date('Y-m-d H:i:s');
$queue[$key] = $task;
} else {
unset($queue[$key]);
}
$processed++;
}
self::saveApiloSyncQueue($queue);
return $processed;
}
// =========================================================================
// Private: email
// =========================================================================
private function sendStatusChangeEmail(array $order): bool
{
if (!$order['client_email']) {
return false;
}
$db = $this->orders->getDb();
$order_statuses = $this->orders->orderStatuses();
$firm_name = (new \Domain\Settings\SettingsRepository($db))->getSingleValue('firm_name');
$status = (int)$order['status'];
$number = $order['number'];
$subjects = [
0 => $firm_name . ' - zamówienie [NUMER] zostało złożone',
1 => $firm_name . ' - zamówienie [NUMER] zostało opłacone',
2 => $firm_name . ' - płatność za zamówienie [NUMER] została odrzucona',
3 => $firm_name . ' - płatność za zamówienie [NUMER] jest sprawdzania ręcznie',
4 => $firm_name . ' - zamówienie [NUMER] zostało przyjęte do realizacji',
5 => $firm_name . ' - zamówienie [NUMER] zostało wysłane',
6 => $firm_name . ' - zamówienie [NUMER] zostało zrealizowane',
7 => $firm_name . ' - zamówienie [NUMER] zostało przygotowane go wysłania',
8 => $firm_name . ' - zamówienie [NUMER] zostało anulowane',
];
$subject = isset($subjects[$status]) ? str_replace('[NUMER]', $number, $subjects[$status]) : '';
if (!$subject) {
return false;
}
$email = new \Shared\Email\Email();
$email->load_by_name('#sklep-zmiana-statusu-zamowienia');
$email->text = str_replace('[NUMER_ZAMOWIENIA]', $number, $email->text);
$email->text = str_replace('[DATA_ZAMOWIENIA]', date('Y/m/d', strtotime($order['date_order'])), $email->text);
$email->text = str_replace('[STATUS]', isset($order_statuses[$status]) ? $order_statuses[$status] : '', $email->text);
return $email->send($order['client_email'], $subject, true);
}
// =========================================================================
// Private: Apilo sync
// =========================================================================
private function syncApiloPaymentIfNeeded(array $order): void
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
return;
}
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) {
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
}
}
private function syncApiloStatusIfNeeded(array $order, int $status): void
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
return;
}
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) {
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
}
}
private function syncApiloPayment(array $order): bool
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
return true;
}
$payment_type = (int)(new \Domain\PaymentMethod\PaymentMethodRepository($db))->getApiloPaymentTypeId((int)$order['payment_method_id']);
if ($payment_type <= 0) {
$payment_type = 1;
}
$payment_date = new \DateTime($order['date_order']);
$access_token = $integrationsRepository->apiloGetAccessToken();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $order['apilo_order_id'] . '/payment/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'amount' => str_replace(',', '.', $order['summary']),
'paymentDate' => $payment_date->format('Y-m-d\TH:i:s\Z'),
'type' => $payment_type,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $access_token,
"Accept: application/json",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$apilo_response = curl_exec($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_errno($ch) ? curl_error($ch) : '';
curl_close($ch);
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("PAYMENT RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_response, true));
}
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;
return true;
}
private function syncApiloStatus(array $order, int $status): bool
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
return true;
}
$access_token = $integrationsRepository->apiloGetAccessToken();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $order['apilo_order_id'] . '/status/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'id' => $order['apilo_order_id'],
'status' => (int)(new \Domain\ShopStatus\ShopStatusRepository($db))->getApiloStatusId($status),
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $access_token,
"Accept: application/json",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$apilo_result = curl_exec($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_errno($ch) ? curl_error($ch) : '';
curl_close($ch);
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("STATUS RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_result, true));
}
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;
return true;
}
// =========================================================================
// Private: Apilo sync queue file helpers
// =========================================================================
private static function queueApiloSync(int $order_id, bool $payment, ?int $status, string $error): void
{
if ($order_id <= 0) return;
$queue = self::loadApiloSyncQueue();
$key = (string)$order_id;
$row = is_array($queue[$key] ?? null) ? $queue[$key] : [];
$row['order_id'] = $order_id;
$row['payment'] = !empty($row['payment']) || $payment ? 1 : 0;
if ($status !== null) {
$row['status'] = $status;
}
$row['attempts'] = (int)($row['attempts'] ?? 0) + 1;
$row['last_error'] = $error;
$row['updated_at'] = date('Y-m-d H:i:s');
$queue[$key] = $row;
self::saveApiloSyncQueue($queue);
}
private static function apiloSyncQueuePath(): string
{
return dirname(__DIR__, 2) . self::APILO_SYNC_QUEUE_FILE;
}
private static function loadApiloSyncQueue(): array
{
$path = self::apiloSyncQueuePath();
if (!file_exists($path)) return [];
$content = file_get_contents($path);
if (!$content) return [];
$decoded = json_decode($content, true);
if (!is_array($decoded)) return [];
return $decoded;
}
private static function saveApiloSyncQueue(array $queue): void
{
$path = self::apiloSyncQueuePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
private static function appendApiloLog(string $message): void
{
$base = isset($_SERVER['DOCUMENT_ROOT']) && $_SERVER['DOCUMENT_ROOT']
? rtrim($_SERVER['DOCUMENT_ROOT'], '/\\')
: dirname(__DIR__, 2);
$dir = $base . '/logs';
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents(
$dir . '/apilo.txt',
date('Y-m-d H:i:s') . ' --- ' . $message . "\n\n",
FILE_APPEND
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,785 @@
<?php
namespace Domain\Pages;
class PagesRepository
{
/**
* @var array<int, string>
*/
private const PAGE_TYPES = [
0 => 'pelne artykuly',
1 => 'wprowadzenia',
2 => 'miniaturki',
3 => 'link',
4 => 'kontakt',
5 => 'kategoria sklepu',
];
/**
* @var array<int, string>
*/
private const SORT_TYPES = [
0 => 'data dodania - najstarsze na poczatku',
1 => 'data dodania - najnowsze na poczatku',
2 => 'data modyfikacji - rosnaco',
3 => 'data modyfikacji - malejaco',
4 => 'reczne',
5 => 'alfabetycznie - A - Z',
6 => 'alfabetycznie - Z - A',
];
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array<int, string>
*/
public function pageTypes(): array
{
return self::PAGE_TYPES;
}
/**
* @return array<int, string>
*/
public function sortTypes(): array
{
return self::SORT_TYPES;
}
/**
* @return array<int, array<string, mixed>>
*/
public function menusList(): array
{
$rows = $this->db->select('pp_menus', '*', ['ORDER' => ['id' => 'ASC']]);
return is_array($rows) ? $rows : [];
}
/**
* @return array<int, array<string, mixed>>
*/
public function menusWithPages(): array
{
$menus = $this->menusList();
foreach ($menus as $index => $menu) {
$menuId = (int)($menu['id'] ?? 0);
$menus[$index]['pages'] = $this->menuPages($menuId);
}
return $menus;
}
/**
* @return array<int, array<string, mixed>>
*/
public function menuPages(int $menuId, ?int $parentId = null): array
{
if ($menuId <= 0) {
return [];
}
$rows = $this->db->select('pp_pages', ['id', 'menu_id', 'status', 'parent_id', 'start'], [
'AND' => [
'menu_id' => $menuId,
'parent_id' => $parentId,
],
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$pages = [];
foreach ($rows as $row) {
$pageId = (int)($row['id'] ?? 0);
if ($pageId <= 0) {
continue;
}
$row['title'] = $this->pageTitle($pageId);
$row['languages'] = $this->pageLanguages($pageId);
$row['subpages'] = $this->menuPages($menuId, $pageId);
$pages[] = $row;
}
return $pages;
}
public function menuDelete(int $menuId): bool
{
if ($menuId <= 0) {
return false;
}
if ((int)$this->db->count('pp_pages', ['menu_id' => $menuId]) > 0) {
return false;
}
return (bool)$this->db->delete('pp_menus', ['id' => $menuId]);
}
public function pageDelete(int $pageId): bool
{
if ($pageId <= 0) {
return false;
}
if ((int)$this->db->count('pp_pages', ['parent_id' => $pageId]) > 0) {
return false;
}
return (bool)$this->db->delete('pp_pages', ['id' => $pageId]);
}
/**
* @return array<string, mixed>
*/
public function menuDetails(int $menuId): array
{
if ($menuId <= 0) {
return [
'id' => 0,
'name' => '',
'status' => 1,
];
}
$menu = $this->db->get('pp_menus', '*', ['id' => $menuId]);
if (!is_array($menu)) {
return [
'id' => 0,
'name' => '',
'status' => 1,
];
}
return $menu;
}
public function menuSave(int $menuId, string $name, $status): bool
{
$statusValue = $this->toSwitchValue($status);
if ($menuId <= 0) {
$result = $this->db->insert('pp_menus', [
'name' => $name,
'status' => $statusValue,
]);
if ($result) {
\Shared\Helpers\Helpers::delete_dir('../temp/');
}
return (bool)$result;
}
$this->db->update('pp_menus', [
'name' => $name,
'status' => $statusValue,
], [
'id' => $menuId,
]);
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
/**
* @return array<string, mixed>
*/
public function pageDetails(int $pageId): array
{
if ($pageId <= 0) {
return $this->defaultPage();
}
$page = $this->db->get('pp_pages', '*', ['id' => $pageId]);
if (!is_array($page)) {
return $this->defaultPage();
}
$translations = $this->db->select('pp_pages_langs', '*', ['page_id' => $pageId]);
if (is_array($translations)) {
foreach ($translations as $row) {
$langId = (string)($row['lang_id'] ?? '');
if ($langId !== '') {
$page['languages'][$langId] = $row;
}
}
}
$page['layout_id'] = (int)$this->db->get('pp_layouts_pages', 'layout_id', ['page_id' => $pageId]);
return $page;
}
/**
* @return array<int, array<string, mixed>>
*/
public function pageArticles(int $pageId): array
{
if ($pageId <= 0) {
return [];
}
$sql = '
SELECT
ap.article_id,
ap.o,
a.status,
(
SELECT title
FROM pp_articles_langs AS pal
JOIN pp_langs AS pl ON pal.lang_id = pl.id
WHERE pal.article_id = ap.article_id
AND pal.title != ""
ORDER BY pl.o ASC
LIMIT 1
) AS title
FROM pp_articles_pages AS ap
JOIN pp_articles AS a ON a.id = ap.article_id
WHERE ap.page_id = :page_id
AND a.status != -1
ORDER BY ap.o ASC
';
$stmt = $this->db->query($sql, [':page_id' => $pageId]);
$rows = $stmt ? $stmt->fetchAll() : [];
return is_array($rows) ? $rows : [];
}
public function saveArticlesOrder(int $pageId, $articles): bool
{
if ($pageId <= 0) {
return false;
}
if (!is_array($articles)) {
return true;
}
$this->db->update('pp_articles_pages', ['o' => 0], ['page_id' => $pageId]);
$position = 0;
foreach ($articles as $item) {
$articleId = (int)($item['item_id'] ?? 0);
if ($articleId <= 0) {
continue;
}
$position++;
$this->db->update('pp_articles_pages', ['o' => $position], [
'AND' => [
'page_id' => $pageId,
'article_id' => $articleId,
],
]);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function savePagesOrder(int $menuId, $pages): bool
{
if ($menuId <= 0) {
return false;
}
if (!is_array($pages)) {
return true;
}
$this->db->update('pp_pages', ['o' => 0], ['menu_id' => $menuId]);
$position = 0;
foreach ($pages as $item) {
$itemId = (int)($item['item_id'] ?? 0);
$depth = (int)($item['depth'] ?? 0);
if ($itemId <= 0 || $depth <= 1) {
continue;
}
$parentId = (int)($item['parent_id'] ?? 0);
if ($depth === 2) {
$parentId = null;
}
$position++;
$this->db->update('pp_pages', [
'o' => $position,
'parent_id' => $parentId,
], [
'id' => $itemId,
]);
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
return true;
}
public function pageSave(array $data): ?int
{
$pageId = (int)($data['id'] ?? 0);
$menuId = (int)($data['menu_id'] ?? 0);
$parentId = $this->normalizeNullableInt($data['parent_id'] ?? null);
$pageType = (int)($data['page_type'] ?? 0);
$sortType = (int)($data['sort_type'] ?? 0);
$layoutId = (int)($data['layout_id'] ?? 0);
$articlesLimit = (int)($data['articles_limit'] ?? 0);
$showTitle = $this->toSwitchValue($data['show_title'] ?? 0);
$status = $this->toSwitchValue($data['status'] ?? 0);
$start = $this->toSwitchValue($data['start'] ?? 0);
$categoryId = $this->normalizeNullableInt($data['category_id'] ?? null);
if ($pageType !== 5) {
$categoryId = null;
}
if ($pageId <= 0) {
$order = $this->maxPageOrder() + 1;
$result = $this->db->insert('pp_pages', [
'menu_id' => $menuId,
'page_type' => $pageType,
'sort_type' => $sortType,
'articles_limit' => $articlesLimit,
'show_title' => $showTitle,
'status' => $status,
'o' => $order,
'parent_id' => $parentId,
'start' => $start,
'category_id' => $categoryId,
]);
if (!$result) {
return null;
}
$pageId = (int)$this->db->id();
if ($pageId <= 0) {
return null;
}
} else {
$this->db->update('pp_pages', [
'menu_id' => $menuId,
'page_type' => $pageType,
'sort_type' => $sortType,
'articles_limit' => $articlesLimit,
'show_title' => $showTitle,
'status' => $status,
'parent_id' => $parentId,
'start' => $start,
'category_id' => $categoryId,
], [
'id' => $pageId,
]);
}
if ($start === 1) {
$this->db->update('pp_pages', ['start' => 0], ['id[!]' => $pageId]);
$this->db->update('pp_pages', ['start' => 1], ['id' => $pageId]);
}
$this->db->delete('pp_layouts_pages', ['page_id' => $pageId]);
if ($layoutId > 0) {
$this->db->insert('pp_layouts_pages', [
'layout_id' => $layoutId,
'page_id' => $pageId,
]);
}
$this->saveTranslations($pageId, $pageType, $data);
$this->updateSubpagesMenuId($pageId, $menuId);
\Shared\Helpers\Helpers::htacces();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $pageId;
}
public function generateSeoLink(string $title, int $pageId = 0, int $articleId = 0, int $categoryId = 0): string
{
$base = trim((string)\Shared\Helpers\Helpers::seo($title));
if ($base === '') {
return '';
}
$candidate = $base;
$suffix = 0;
while ($this->isSeoLinkUsed('pp_pages_langs', 'page_id', $candidate, $pageId)
|| $this->isSeoLinkUsed('pp_articles_langs', 'article_id', $candidate, $articleId)
|| $this->isSeoLinkUsed('pp_shop_categories_langs', 'category_id', $candidate, $categoryId)) {
$suffix++;
$candidate = $base . '-' . $suffix;
}
return $candidate;
}
public function pageUrlPreview(int $pageId, string $langId, string $title, string $seoLink, string $defaultLanguageId): string
{
$url = trim($seoLink) !== ''
? '/' . ltrim($seoLink, '/')
: '/s-' . $pageId . '-' . \Shared\Helpers\Helpers::seo($title);
if ($langId !== '' && $langId !== $defaultLanguageId && $url !== '#') {
$url = '/' . $langId . $url;
}
return $url;
}
public function toggleCookieValue(string $cookieName, int $itemId): void
{
if ($cookieName === '' || $itemId <= 0) {
return;
}
$state = [];
if (!empty($_COOKIE[$cookieName])) {
$decoded = @unserialize((string)$_COOKIE[$cookieName], ['allowed_classes' => false]);
if (is_array($decoded)) {
$state = $decoded;
}
}
$state[$itemId] = empty($state[$itemId]) ? 1 : 0;
setcookie($cookieName, serialize($state), time() + 3600 * 24 * 365);
}
public function pageTitle(int $pageId): string
{
if ($pageId <= 0) {
return '';
}
$rows = $this->db->select('pp_pages_langs', [
'[><]pp_langs' => ['lang_id' => 'id'],
], 'title', [
'AND' => [
'page_id' => $pageId,
'title[!]' => '',
],
'ORDER' => ['o' => 'ASC'],
'LIMIT' => 1,
]);
if (!is_array($rows) || !isset($rows[0])) {
return '';
}
return (string)$rows[0];
}
/**
* @return array<int, array<string, mixed>>
*/
public function pageLanguages(int $pageId): array
{
if ($pageId <= 0) {
return [];
}
$rows = $this->db->select('pp_pages_langs', '*', [
'AND' => [
'page_id' => $pageId,
'title[!]' => null,
],
]);
return is_array($rows) ? $rows : [];
}
/**
* @return array<string, mixed>
*/
private function defaultPage(): array
{
return [
'id' => 0,
'menu_id' => 0,
'page_type' => 0,
'sort_type' => 0,
'articles_limit' => 2,
'show_title' => 1,
'status' => 1,
'start' => 0,
'parent_id' => null,
'category_id' => null,
'layout_id' => 0,
'languages' => [],
];
}
private function saveTranslations(int $pageId, int $pageType, array $data): void
{
$titles = is_array($data['title'] ?? null) ? $data['title'] : [];
$seoLinks = is_array($data['seo_link'] ?? null) ? $data['seo_link'] : [];
$metaTitles = is_array($data['meta_title'] ?? null) ? $data['meta_title'] : [];
$metaDescriptions = is_array($data['meta_description'] ?? null) ? $data['meta_description'] : [];
$metaKeywords = is_array($data['meta_keywords'] ?? null) ? $data['meta_keywords'] : [];
$noindexValues = is_array($data['noindex'] ?? null) ? $data['noindex'] : [];
$pageTitles = is_array($data['page_title'] ?? null) ? $data['page_title'] : [];
$links = is_array($data['link'] ?? null) ? $data['link'] : [];
$canonicals = is_array($data['canonical'] ?? null) ? $data['canonical'] : [];
foreach ($titles as $langId => $title) {
$langId = (string)$langId;
if ($langId === '') {
continue;
}
$row = [
'lang_id' => $langId,
'title' => $this->nullIfEmpty($title),
'meta_description' => $this->nullIfEmpty($metaDescriptions[$langId] ?? null),
'meta_keywords' => $this->nullIfEmpty($metaKeywords[$langId] ?? null),
'meta_title' => $this->nullIfEmpty($metaTitles[$langId] ?? null),
'seo_link' => $this->nullIfEmpty(\Shared\Helpers\Helpers::seo((string)($seoLinks[$langId] ?? ''))),
'noindex' => (int)($noindexValues[$langId] ?? 0),
'page_title' => $this->nullIfEmpty($pageTitles[$langId] ?? null),
'link' => $pageType === 3 ? $this->nullIfEmpty($links[$langId] ?? null) : null,
'canonical' => $this->nullIfEmpty($canonicals[$langId] ?? null),
];
$translationId = (int)$this->db->get('pp_pages_langs', 'id', [
'AND' => [
'page_id' => $pageId,
'lang_id' => $langId,
],
]);
if ($translationId > 0) {
$this->db->update('pp_pages_langs', $row, ['id' => $translationId]);
} else {
$row['page_id'] = $pageId;
$this->db->insert('pp_pages_langs', $row);
}
}
}
private function updateSubpagesMenuId(int $parentId, int $menuId): void
{
if ($parentId <= 0 || $menuId <= 0) {
return;
}
$this->db->update('pp_pages', ['menu_id' => $menuId], ['parent_id' => $parentId]);
$children = $this->db->select('pp_pages', ['id'], ['parent_id' => $parentId]);
if (!is_array($children)) {
return;
}
foreach ($children as $row) {
$childId = (int)($row['id'] ?? 0);
if ($childId > 0) {
$this->updateSubpagesMenuId($childId, $menuId);
}
}
}
private function isSeoLinkUsed(string $table, string $idColumn, string $seoLink, int $exceptId): bool
{
$where = [
'seo_link' => $seoLink,
];
if ($exceptId > 0) {
$where[$idColumn . '[!]'] = $exceptId;
}
return (int)$this->db->count($table, ['AND' => $where]) > 0;
}
private function maxPageOrder(): int
{
$max = $this->db->max('pp_pages', 'o');
return $max ? (int)$max : 0;
}
private function toSwitchValue($value): int
{
if ($value === 'on' || $value === '1' || $value === 1 || $value === true) {
return 1;
}
return 0;
}
private function normalizeNullableInt($value): ?int
{
if ($value === null || $value === '' || (int)$value === 0) {
return null;
}
return (int)$value;
}
private function nullIfEmpty($value): ?string
{
$value = trim((string)$value);
return $value === '' ? null : $value;
}
// ── Frontend methods ──────────────────────────────────────────
public function frontPageDetails($id = '', $langId = ''): ?array
{
$langId = (string)$langId;
if (!$id) {
$id = $this->frontMainPageId();
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontPageDetails:$id:$langId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$page = $this->db->get('pp_pages', '*', ['id' => (int)$id]);
if (!is_array($page)) {
return null;
}
$page['language'] = $this->db->get('pp_pages_langs', '*', ['AND' => ['page_id' => (int)$id, 'lang_id' => $langId]]);
$cacheHandler->set($cacheKey, $page);
return $page;
}
public function frontPageSort(int $pageId)
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontPageSort:$pageId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if ($cached !== false) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$sort = $this->db->get('pp_pages', 'sort_type', ['id' => $pageId]);
$cacheHandler->set($cacheKey, $sort);
return $sort;
}
public function frontMainPageId()
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'PagesRepository::frontMainPageId';
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if ($cached) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$id = $this->db->get('pp_pages', 'id', ['AND' => ['status' => 1, 'start' => 1]]);
if (!$id) {
$id = $this->db->get('pp_pages', 'id', ['status' => 1, 'ORDER' => ['menu_id' => 'ASC', 'o' => 'ASC'], 'LIMIT' => 1]);
}
$cacheHandler->set($cacheKey, $id);
return $id;
}
public function frontLangUrl(int $pageId, string $langId): string
{
$page = $this->frontPageDetails($pageId, $langId);
if (!is_array($page) || !is_array($page['language'] ?? null)) {
return '/';
}
$seoLink = $page['language']['seo_link'] ?? '';
$title = $page['language']['title'] ?? '';
$url = $seoLink ? '/' . $seoLink : '/s-' . $page['id'] . '-' . \Shared\Helpers\Helpers::seo($title);
$defaultLang = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage();
if ($langId !== $defaultLang && $url !== '#') {
$url = '/' . $langId . $url;
}
return $url;
}
public function frontMenuDetails(int $menuId, string $langId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontMenuDetails:$menuId:$langId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$menu = $this->db->get('pp_menus', '*', ['id' => (int)$menuId]);
if (!is_array($menu)) {
return null;
}
$menu['pages'] = $this->frontMenuPages($menuId, $langId);
$cacheHandler->set($cacheKey, $menu);
return $menu;
}
public function frontMenuPages(int $menuId, string $langId, $parentId = null): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PagesRepository::frontMenuPages:$menuId:$langId:$parentId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$results = $this->db->select('pp_pages', ['id'], [
'AND' => ['status' => 1, 'menu_id' => (int)$menuId, 'parent_id' => $parentId],
'ORDER' => ['o' => 'ASC'],
]);
$pages = [];
if (is_array($results)) {
foreach ($results as $row) {
$page = $this->frontPageDetails($row['id'], $langId);
if (is_array($page)) {
$page['pages'] = $this->frontMenuPages($menuId, $langId, $row['id']);
$pages[] = $page;
}
}
}
$cacheHandler->set($cacheKey, $pages);
return $pages;
}
}

View File

@@ -0,0 +1,370 @@
<?php
namespace Domain\PaymentMethod;
class PaymentMethodRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'spm.id',
'name' => 'spm.name',
'status' => 'spm.status',
'apilo_payment_type_id' => 'spm.apilo_payment_type_id',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'spm.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'spm.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'spm.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_payment_methods AS spm
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
spm.id,
spm.name,
spm.description,
spm.status,
spm.apilo_payment_type_id
FROM pp_shop_payment_methods AS spm
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, spm.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item = $this->normalizePaymentMethod($item);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
public function find(int $paymentMethodId): ?array
{
if ($paymentMethodId <= 0) {
return null;
}
$paymentMethod = $this->db->get('pp_shop_payment_methods', '*', ['id' => $paymentMethodId]);
if (!is_array($paymentMethod)) {
return null;
}
return $this->normalizePaymentMethod($paymentMethod);
}
public function save(int $paymentMethodId, array $data): ?int
{
if ($paymentMethodId <= 0) {
return null;
}
$row = [
'description' => trim((string)($data['description'] ?? '')),
'status' => $this->toSwitchValue($data['status'] ?? 0),
'apilo_payment_type_id' => $this->normalizeApiloPaymentTypeId($data['apilo_payment_type_id'] ?? null),
];
$this->db->update('pp_shop_payment_methods', $row, ['id' => $paymentMethodId]);
return $paymentMethodId;
}
/**
* @return array<int, array<string, mixed>>
*/
public function allActive(): array
{
$rows = $this->db->select('pp_shop_payment_methods', '*', [
'status' => 1,
'ORDER' => ['id' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$result = [];
foreach ($rows as $row) {
if (is_array($row)) {
$result[] = $this->normalizePaymentMethod($row);
}
}
return $result;
}
/**
* @return array<int, array<string, mixed>>
*/
public function allForAdmin(): array
{
$rows = $this->db->select('pp_shop_payment_methods', '*', [
'ORDER' => ['name' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$result = [];
foreach ($rows as $row) {
if (is_array($row)) {
$result[] = $this->normalizePaymentMethod($row);
}
}
return $result;
}
public function findActiveById(int $paymentMethodId): ?array
{
if ($paymentMethodId <= 0) {
return null;
}
$paymentMethod = $this->db->get('pp_shop_payment_methods', '*', [
'AND' => [
'id' => $paymentMethodId,
'status' => 1,
],
]);
if (!is_array($paymentMethod)) {
return null;
}
return $this->normalizePaymentMethod($paymentMethod);
}
public function isActive(int $paymentMethodId): int
{
if ($paymentMethodId <= 0) {
return 0;
}
$status = $this->db->get('pp_shop_payment_methods', 'status', ['id' => $paymentMethodId]);
return $this->toSwitchValue($status);
}
/**
* @return int|string|null
*/
public function getApiloPaymentTypeId(int $paymentMethodId)
{
if ($paymentMethodId <= 0) {
return null;
}
$value = $this->db->get('pp_shop_payment_methods', 'apilo_payment_type_id', ['id' => $paymentMethodId]);
return $this->normalizeApiloPaymentTypeId($value);
}
/**
* @return array<int, array<string, mixed>>
*/
public function forTransport(int $transportMethodId): array
{
if ($transportMethodId <= 0) {
return [];
}
$sql = "
SELECT
spm.id,
spm.name,
spm.description,
spm.status,
spm.apilo_payment_type_id
FROM pp_shop_payment_methods AS spm
INNER JOIN pp_shop_transport_payment_methods AS stpm
ON stpm.id_payment_method = spm.id
WHERE spm.status = 1
AND stpm.id_transport = :transport_id
";
$stmt = $this->db->query($sql, [':transport_id' => $transportMethodId]);
$rows = $stmt ? $stmt->fetchAll() : [];
if (!is_array($rows)) {
return [];
}
$result = [];
foreach ($rows as $row) {
if (is_array($row)) {
$result[] = $this->normalizePaymentMethod($row);
}
}
return $result;
}
/**
* Metody platnosci dla danego transportu — z Redis cache (frontend).
*
* @return array<int, array<string, mixed>>
*/
public function paymentMethodsByTransport(int $transportMethodId): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'payment_methods_by_transport' . $transportMethodId;
$cached = $cacheHandler->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
$result = $this->forTransport($transportMethodId);
$cacheHandler->set($cacheKey, $result);
return $result;
}
/**
* Pojedyncza aktywna metoda platnosci — z Redis cache (frontend).
*/
public function paymentMethodCached(int $paymentMethodId): ?array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'payment_method' . $paymentMethodId;
$cached = $cacheHandler->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
$result = $this->findActiveById($paymentMethodId);
$cacheHandler->set($cacheKey, $result);
return $result;
}
/**
* Wszystkie aktywne metody platnosci — z Redis cache (frontend).
*
* @return array<int, array<string, mixed>>
*/
public function paymentMethodsCached(): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'payment_methods';
$cached = $cacheHandler->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
$result = $this->allActive();
$cacheHandler->set($cacheKey, $result);
return $result;
}
private function normalizePaymentMethod(array $row): array
{
$row['id'] = (int)($row['id'] ?? 0);
$row['name'] = trim((string)($row['name'] ?? ''));
$row['description'] = (string)($row['description'] ?? '');
$row['status'] = $this->toSwitchValue($row['status'] ?? 0);
$row['apilo_payment_type_id'] = $this->normalizeApiloPaymentTypeId($row['apilo_payment_type_id'] ?? null);
return $row;
}
/**
* @return int|string|null
*/
private function normalizeApiloPaymentTypeId($value)
{
if ($value === null || $value === false) {
return null;
}
$text = trim((string)$value);
if ($text === '') {
return null;
}
if (preg_match('/^-?\d+$/', $text) === 1) {
return (int)$text;
}
return $text;
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
}

View File

@@ -0,0 +1,360 @@
<?php
namespace Domain\Producer;
class ProducerRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* Lista producentów dla panelu admin (paginowana, filtrowalna, sortowalna).
*
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'p.id',
'name' => 'p.name',
'status' => 'p.status',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'p.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'p.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'p.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_producer AS p
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
p.id,
p.name,
p.status,
p.img
FROM pp_shop_producer AS p
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, p.id DESC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item['id'] = (int)($item['id'] ?? 0);
$item['status'] = $this->toSwitchValue($item['status'] ?? 0);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
/**
* Pobiera producenta z tłumaczeniami.
*/
public function find(int $id): array
{
if ($id <= 0) {
return $this->defaultProducer();
}
$producer = $this->db->get('pp_shop_producer', '*', ['id' => $id]);
if (!is_array($producer)) {
return $this->defaultProducer();
}
$producer['id'] = (int)($producer['id'] ?? 0);
$producer['status'] = $this->toSwitchValue($producer['status'] ?? 0);
// Tłumaczenia
$rows = $this->db->select('pp_shop_producer_lang', '*', ['producer_id' => $id]);
$languages = [];
if (is_array($rows)) {
foreach ($rows as $row) {
$langId = $row['lang_id'] ?? '';
$languages[$langId] = [
'description' => $row['description'] ?? null,
'data' => $row['data'] ?? null,
'meta_title' => $row['meta_title'] ?? null,
];
}
}
$producer['languages'] = $languages;
return $producer;
}
/**
* Zapisuje producenta (insert / update) wraz z tłumaczeniami.
*
* @return int|null ID producenta lub null przy błędzie
*/
public function save(
int $id,
string $name,
int $status,
?string $img,
array $description,
array $data,
array $metaTitle,
array $langs
): ?int {
$row = [
'name' => trim($name),
'status' => $status === 1 ? 1 : 0,
'img' => $img,
];
if ($id <= 0) {
$this->db->insert('pp_shop_producer', $row);
$id = (int)$this->db->id();
if ($id <= 0) {
return null;
}
} else {
$this->db->update('pp_shop_producer', $row, ['id' => $id]);
}
// Tłumaczenia
foreach ($langs as $lg) {
$langId = $lg['id'] ?? '';
$translationData = [
'description' => $description[$langId] ?? null,
'data' => $data[$langId] ?? null,
'meta_title' => $metaTitle[$langId] ?? null,
];
$translationId = $this->db->get(
'pp_shop_producer_lang',
'id',
['AND' => ['producer_id' => $id, 'lang_id' => $langId]]
);
if ($translationId) {
$this->db->update('pp_shop_producer_lang', $translationData, ['id' => $translationId]);
} else {
$this->db->insert('pp_shop_producer_lang', array_merge($translationData, [
'producer_id' => $id,
'lang_id' => $langId,
]));
}
}
return $id;
}
/**
* Usuwa producenta (kaskadowo z pp_shop_producer_lang przez FK).
*/
public function delete(int $id): bool
{
if ($id <= 0) {
return false;
}
$result = (bool)$this->db->delete('pp_shop_producer', ['id' => $id]);
return $result;
}
/**
* Wszystkie producenty (do select w edycji produktu).
*
* @return array<int, array{id: int, name: string}>
*/
public function allProducers(): array
{
$rows = $this->db->select('pp_shop_producer', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]);
if (!is_array($rows)) {
return [];
}
$producers = [];
foreach ($rows as $row) {
$producers[] = [
'id' => (int)($row['id'] ?? 0),
'name' => (string)($row['name'] ?? ''),
];
}
return $producers;
}
/**
* Pobiera producenta z tłumaczeniami dla danego języka (frontend).
*/
public function findForFrontend(int $id, string $langId): ?array
{
if ($id <= 0) {
return null;
}
$producer = $this->db->get('pp_shop_producer', '*', ['id' => $id]);
if (!is_array($producer)) {
return null;
}
$producer['id'] = (int)($producer['id'] ?? 0);
$langRow = $this->db->get('pp_shop_producer_lang', '*', [
'AND' => ['producer_id' => $id, 'lang_id' => $langId],
]);
$producer['languages'] = [];
if (is_array($langRow)) {
$producer['languages'][$langId] = [
'description' => $langRow['description'] ?? null,
'data' => $langRow['data'] ?? null,
'meta_title' => $langRow['meta_title'] ?? null,
];
}
return $producer;
}
/**
* Produkty producenta (paginowane) — frontend.
*
* @return array{products: array, ls: int}
*/
public function producerProducts(int $producerId, int $perPage = 12, int $page = 1): array
{
$count = $this->db->count('pp_shop_products', [
'AND' => ['producer_id' => $producerId, 'status' => 1],
]);
$totalPages = max(1, (int)ceil($count / $perPage));
$page = max(1, min($page, $totalPages));
$offset = $perPage * ($page - 1);
$products = $this->db->select('pp_shop_products', 'id', [
'AND' => ['producer_id' => $producerId, 'status' => 1],
'LIMIT' => [$offset, $perPage],
]);
return [
'products' => is_array($products) ? $products : [],
'ls' => $totalPages,
];
}
/**
* Aktywni producenci (frontend lista).
*
* @return array<int>
*/
public function allActiveIds(): array
{
$rows = $this->db->select('pp_shop_producer', 'id', [
'status' => 1,
'ORDER' => ['name' => 'ASC'],
]);
return is_array($rows) ? array_map('intval', $rows) : [];
}
/**
* Aktywni producenci z pelnym danymi (frontend lista).
*
* @return array<int, array{id: int, name: string, img: string|null}>
*/
public function allActiveProducers(): array
{
$rows = $this->db->select('pp_shop_producer', ['id', 'name', 'img'], [
'status' => 1,
'ORDER' => ['name' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$producers = [];
foreach ($rows as $row) {
$producers[] = [
'id' => (int)($row['id'] ?? 0),
'name' => (string)($row['name'] ?? ''),
'img' => $row['img'] ?? null,
];
}
return $producers;
}
private function defaultProducer(): array
{
return [
'id' => 0,
'name' => '',
'status' => 1,
'img' => null,
'languages' => [],
];
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
<?php
namespace Domain\ProductSet;
class ProductSetRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'ps.id',
'name' => 'ps.name',
'status' => 'ps.status',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'ps.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'ps.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'ps.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_product_sets AS ps
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
ps.id,
ps.name,
ps.status
FROM pp_shop_product_sets AS ps
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, ps.id DESC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item['id'] = (int)($item['id'] ?? 0);
$item['status'] = $this->toSwitchValue($item['status'] ?? 0);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
public function find(int $id): array
{
if ($id <= 0) {
return $this->defaultSet();
}
$set = $this->db->get('pp_shop_product_sets', '*', ['id' => $id]);
if (!is_array($set)) {
return $this->defaultSet();
}
$set['id'] = (int)($set['id'] ?? 0);
$set['status'] = $this->toSwitchValue($set['status'] ?? 0);
$products = $this->db->select('pp_shop_product_sets_products', 'product_id', ['set_id' => $id]);
$set['products'] = is_array($products) ? array_map('intval', $products) : [];
return $set;
}
public function save(int $id, string $name, int $status, array $productIds): ?int
{
$row = [
'name' => trim($name),
'status' => $status === 1 ? 1 : 0,
];
if ($id <= 0) {
$this->db->insert('pp_shop_product_sets', $row);
$id = (int)$this->db->id();
if ($id <= 0) {
return null;
}
} else {
$this->db->update('pp_shop_product_sets', $row, ['id' => $id]);
}
$this->syncProducts($id, $productIds);
$this->clearTempAndCache();
return $id;
}
public function delete(int $id): bool
{
if ($id <= 0) {
return false;
}
$this->db->delete('pp_shop_product_sets_products', ['set_id' => $id]);
$result = (bool)$this->db->delete('pp_shop_product_sets', ['id' => $id]);
if ($result) {
$this->clearTempAndCache();
}
return $result;
}
/**
* @return array<int, array{id: int, name: string}>
*/
public function allSets(): array
{
$rows = $this->db->select('pp_shop_product_sets', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]);
if (!is_array($rows)) {
return [];
}
$sets = [];
foreach ($rows as $row) {
$sets[] = [
'id' => (int)($row['id'] ?? 0),
'name' => (string)($row['name'] ?? ''),
];
}
return $sets;
}
/**
* @return array<int, string>
*/
public function allProductsMap(): array
{
$rows = $this->db->select('pp_shop_products', 'id', ['parent_id' => null]);
if (!is_array($rows)) {
return [];
}
$products = [];
foreach ($rows as $productId) {
$name = $this->db->get('pp_shop_products_langs', 'name', [
'AND' => ['product_id' => $productId, 'lang_id' => 'pl'],
]);
if ($name) {
$products[(int)$productId] = (string)$name;
}
}
return $products;
}
private function syncProducts(int $setId, array $productIds): void
{
$this->db->delete('pp_shop_product_sets_products', ['set_id' => $setId]);
$seen = [];
foreach ($productIds as $productId) {
$pid = (int)$productId;
if ($pid > 0 && !isset($seen[$pid])) {
$this->db->insert('pp_shop_product_sets_products', [
'set_id' => $setId,
'product_id' => $pid,
]);
$seen[$pid] = true;
}
}
}
private function defaultSet(): array
{
return [
'id' => 0,
'name' => '',
'status' => 1,
'products' => [],
];
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
private function clearTempAndCache(): void
{
\Shared\Helpers\Helpers::delete_dir('../temp/');
\Shared\Helpers\Helpers::delete_dir('../thumbs/');
}
}

View File

@@ -0,0 +1,710 @@
<?php
namespace Domain\Promotion;
class PromotionRepository
{
private const MAX_PER_PAGE = 100;
private $db;
private ?string $defaultLangId = null;
public static $condition_type = [
1 => 'Rabat procentowy na produkty z kategorii 1 jeżeli w koszyku jest produkt z kategorii 2',
2 => 'Rabat procentowy na produkty z kategorii 1 i 2',
3 => 'Najtańszy produkt w koszyku (z wybranych kategorii) za X zł',
4 => 'Rabat procentowy na cały koszyk',
5 => 'Rabat procentowy na produkty z kategorii 1 lub 2',
];
public static $discount_type = [ 1 => 'Rabat procentowy' ];
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'id',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'sp.id',
'name' => 'sp.name',
'status' => 'sp.status',
'condition_type' => 'sp.condition_type',
'date_from' => 'sp.date_from',
'date_to' => 'sp.date_to',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'sp.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 = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'sp.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'sp.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_promotion AS sp
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
sp.id,
sp.name,
sp.status,
sp.condition_type,
sp.discount_type,
sp.amount,
sp.date_from,
sp.date_to,
sp.include_coupon,
sp.include_product_promo,
sp.min_product_count,
sp.price_cheapest_product
FROM pp_shop_promotion AS sp
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, sp.id DESC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
public function find(int $promotionId): array
{
if ($promotionId <= 0) {
return $this->defaultPromotion();
}
$promotion = $this->db->get('pp_shop_promotion', '*', ['id' => $promotionId]);
if (!is_array($promotion)) {
return $this->defaultPromotion();
}
$promotion['id'] = (int)($promotion['id'] ?? 0);
$promotion['status'] = $this->toSwitchValue($promotion['status'] ?? 0);
$promotion['include_coupon'] = $this->toSwitchValue($promotion['include_coupon'] ?? 0);
$promotion['include_product_promo'] = $this->toSwitchValue($promotion['include_product_promo'] ?? 0);
$promotion['condition_type'] = (int)($promotion['condition_type'] ?? 1);
$promotion['discount_type'] = (int)($promotion['discount_type'] ?? 1);
$promotion['categories'] = $this->decodeIdList($promotion['categories'] ?? null);
$promotion['condition_categories'] = $this->decodeIdList($promotion['condition_categories'] ?? null);
return $promotion;
}
public function save(array $data): ?int
{
$promotionId = (int)($data['id'] ?? 0);
$row = [
'name' => trim((string)($data['name'] ?? '')),
'status' => $this->toSwitchValue($data['status'] ?? 0),
'condition_type' => (int)($data['condition_type'] ?? 1),
'discount_type' => (int)($data['discount_type'] ?? 1),
'amount' => $this->toNullableNumeric($data['amount'] ?? null),
'date_from' => $this->toNullableDate($data['date_from'] ?? null),
'date_to' => $this->toNullableDate($data['date_to'] ?? null),
'categories' => $this->encodeIdList($data['categories'] ?? null),
'condition_categories' => $this->encodeIdList($data['condition_categories'] ?? null),
'include_coupon' => $this->toSwitchValue($data['include_coupon'] ?? 0),
'include_product_promo' => $this->toSwitchValue($data['include_product_promo'] ?? 0),
'min_product_count' => $this->toNullableInt($data['min_product_count'] ?? null),
'price_cheapest_product' => $this->toNullableNumeric($data['price_cheapest_product'] ?? null),
];
if ($promotionId <= 0) {
$this->db->insert('pp_shop_promotion', $row);
$id = (int)$this->db->id();
if ($id <= 0) {
return null;
}
$this->invalidateActivePromotionsCache();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $id;
}
$this->db->update('pp_shop_promotion', $row, ['id' => $promotionId]);
$this->invalidateActivePromotionsCache();
\Shared\Helpers\Helpers::delete_dir('../temp/');
return $promotionId;
}
public function delete(int $promotionId): bool
{
if ($promotionId <= 0) {
return false;
}
$deleted = $this->db->delete('pp_shop_promotion', ['id' => $promotionId]);
$ok = (bool)$deleted;
if ($ok) {
$this->invalidateActivePromotionsCache();
}
return $ok;
}
/**
* @return array<int, array<string, mixed>>
*/
public function categoriesTree($parentId = null): array
{
$rows = $this->db->select('pp_shop_categories', ['id'], [
'parent_id' => $parentId,
'ORDER' => ['o' => 'ASC'],
]);
if (!is_array($rows)) {
return [];
}
$categories = [];
foreach ($rows as $row) {
$categoryId = (int)($row['id'] ?? 0);
if ($categoryId <= 0) {
continue;
}
$category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]);
if (!is_array($category)) {
continue;
}
$translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]);
$category['languages'] = [];
if (is_array($translations)) {
foreach ($translations as $translation) {
$langId = (string)($translation['lang_id'] ?? '');
if ($langId !== '') {
$category['languages'][$langId] = $translation;
}
}
}
$category['title'] = $this->categoryTitle($category['languages']);
$category['subcategories'] = $this->categoriesTree($categoryId);
$categories[] = $category;
}
return $categories;
}
private function defaultPromotion(): array
{
return [
'id' => 0,
'name' => '',
'status' => 1,
'condition_type' => 1,
'discount_type' => 1,
'amount' => null,
'date_from' => null,
'date_to' => null,
'categories' => [],
'condition_categories' => [],
'include_coupon' => 0,
'include_product_promo' => 0,
'min_product_count' => null,
'price_cheapest_product' => null,
];
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
private function toNullableInt($value): ?int
{
if ($value === null) {
return null;
}
if (is_string($value)) {
$value = trim($value);
if ($value === '') {
return null;
}
}
$intValue = (int)$value;
return $intValue > 0 ? $intValue : null;
}
private function toNullableNumeric($value): ?string
{
if ($value === null) {
return null;
}
$stringValue = trim((string)$value);
if ($stringValue === '') {
return null;
}
return str_replace(',', '.', $stringValue);
}
private function toNullableDate($value): ?string
{
$date = trim((string)$value);
if ($date === '') {
return null;
}
return $date;
}
private function encodeIdList($values): ?string
{
$ids = $this->normalizeIdList($values);
if (empty($ids)) {
return null;
}
return json_encode($ids);
}
/**
* @return int[]
*/
private function decodeIdList($raw): array
{
if (is_array($raw)) {
return $this->normalizeIdList($raw);
}
$text = trim((string)$raw);
if ($text === '') {
return [];
}
$decoded = json_decode($text, true);
if (!is_array($decoded)) {
return [];
}
return $this->normalizeIdList($decoded);
}
/**
* @return int[]
*/
private function normalizeIdList($values): array
{
if ($values === null) {
return [];
}
if (!is_array($values)) {
$text = trim((string)$values);
if ($text === '') {
return [];
}
if (strpos($text, ',') !== false) {
$values = explode(',', $text);
} else {
$values = [$text];
}
}
$ids = [];
foreach ($values as $value) {
$id = (int)$value;
if ($id > 0) {
$ids[$id] = $id;
}
}
return array_values($ids);
}
private function categoryTitle(array $languages): string
{
$defaultLang = $this->defaultLanguageId();
if ($defaultLang !== '' && isset($languages[$defaultLang]['title'])) {
$title = trim((string)$languages[$defaultLang]['title']);
if ($title !== '') {
return $title;
}
}
foreach ($languages as $language) {
$title = trim((string)($language['title'] ?? ''));
if ($title !== '') {
return $title;
}
}
return '';
}
private function defaultLanguageId(): string
{
if ($this->defaultLangId !== null) {
return $this->defaultLangId;
}
$rows = $this->db->select('pp_langs', ['id', 'start', 'o'], [
'status' => 1,
'ORDER' => ['start' => 'DESC', 'o' => 'ASC'],
]);
if (is_array($rows) && !empty($rows)) {
$this->defaultLangId = (string)($rows[0]['id'] ?? '');
} else {
$this->defaultLangId = '';
}
return $this->defaultLangId;
}
private function invalidateActivePromotionsCache(): void
{
if (!class_exists('\Shared\Cache\CacheHandler')) {
return;
}
try {
$cache = new \Shared\Cache\CacheHandler();
if (method_exists($cache, 'delete')) {
$cache->delete('PromotionRepository::getActivePromotions');
}
} catch (\Throwable $e) {
// Cache invalidation should not block save/delete.
}
}
public function getActivePromotions()
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "PromotionRepository::getActivePromotions";
$objectData = $cacheHandler->get( $cacheKey );
if ( !$objectData )
{
$results = $this->db->select( 'pp_shop_promotion', 'id', [ 'AND' => [ 'status' => 1, 'OR #date_from' => [ 'date_from' => null, 'date_from[<=]' => date( 'Y-m-d' ) ], 'OR #date_to' => [ 'date_to' => null, 'date_to[>=]' => date( 'Y-m-d' ) ] ], 'ORDER' => [ 'id' => 'DESC' ] ] );
$cacheHandler->set( $cacheKey, $results );
}
else
{
return unserialize( $objectData );
}
return $results;
}
public function findPromotion( $basket )
{
if ( !is_array( $basket ) || empty( $basket ) )
return is_array( $basket ) ? $basket : [];
foreach ( $basket as $key => $val )
{
unset( $basket[$key]['discount_type'] );
unset( $basket[$key]['discount_amount'] );
unset( $basket[$key]['discount_include_coupon'] );
unset( $basket[$key]['include_product_promo'] );
}
$basket_tmp = $basket;
$results = $this->getActivePromotions();
if ( is_array( $results ) and count( $results ) )
{
foreach ( $results as $row )
{
$promotion = $this->find( (int)$row );
if ( $promotion['id'] <= 0 )
continue;
if ( $promotion['condition_type'] == 4 )
return $this->applyTypeWholeBasket( $basket_tmp, $promotion );
if ( $promotion['condition_type'] == 3 )
return $this->applyTypeCheapestProduct( $basket_tmp, $promotion );
if ( $promotion['condition_type'] == 5 )
return $this->applyTypeCategoriesOr( $basket_tmp, $promotion );
if ( $promotion['condition_type'] == 2 )
return $this->applyTypeCategoriesAnd( $basket_tmp, $promotion );
if ( $promotion['condition_type'] == 1 )
return $this->applyTypeCategoryCondition( $basket_tmp, $promotion );
}
}
return $basket;
}
// =========================================================================
// Frontend: basket promotion logic (migrated from front\factory\ShopPromotion)
// =========================================================================
/**
* Promocja na cały koszyk (condition_type=4)
*/
public function applyTypeWholeBasket(array $basket, $promotion): array
{
$productRepo = new \Domain\Product\ProductRepository( $this->db );
foreach ( $basket as $key => $val )
{
$product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] );
if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
$basket[$key]['discount_type'] = $promotion['discount_type'];
$basket[$key]['discount_amount'] = $promotion['amount'];
$basket[$key]['discount_include_coupon'] = $promotion['include_coupon'];
$basket[$key]['include_product_promo'] = $promotion['include_product_promo'];
}
}
}
return $basket;
}
/**
* Promocja na najtańszy produkt z kategorii 1 lub 2 (condition_type=3)
*/
public function applyTypeCheapestProduct(array $basket, $promotion): array
{
$productRepo = new \Domain\Product\ProductRepository( $this->db );
$condition_1 = false;
$categories = $promotion['categories'];
if ( is_array( $categories ) and is_array( $categories ) )
{
foreach ( $basket as $key => $val )
{
$product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] );
if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( !$condition_1[$key] and in_array( $category_tmp['category_id'], $categories ) )
$condition_1[$key] = true;
}
}
}
}
if ( count( $condition_1 ) >= $promotion['min_product_count'] )
{
$cheapest_position = false;
foreach ( $basket as $key => $val )
{
$price = $productRepo->getPrice( $val['product-id'] );
if ( !$cheapest_position or $cheapest_position['price'] > $price )
{
$cheapest_position['price'] = $price;
$cheapest_position['key'] = $key;
}
}
$basket[$cheapest_position['key']]['quantity'] = 1;
$basket[$cheapest_position['key']]['discount_type'] = 3;
$basket[$cheapest_position['key']]['discount_amount'] = $promotion['price_cheapest_product'];
$basket[$cheapest_position['key']]['discount_include_coupon'] = $promotion['include_coupon'];
$basket[$cheapest_position['key']]['include_product_promo'] = $promotion['include_product_promo'];
}
return $basket;
}
/**
* Promocja na wszystkie produkty z kategorii 1 lub 2 (condition_type=5)
*/
public function applyTypeCategoriesOr(array $basket, $promotion): array
{
$productRepo = new \Domain\Product\ProductRepository( $this->db );
$categories = $promotion['categories'];
$condition_categories = $promotion['condition_categories'];
foreach ( $basket as $key => $val )
{
$product_promotion = $productRepo->isProductOnPromotion( $val['product-id'] );
if ( !$product_promotion or $product_promotion and $promotion['include_product_promo'] )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( in_array( $category_tmp['category_id'], $condition_categories ) or in_array( $category_tmp['category_id'], $categories ) )
{
$basket[$key]['discount_type'] = $promotion['discount_type'];
$basket[$key]['discount_amount'] = $promotion['amount'];
$basket[$key]['discount_include_coupon'] = $promotion['include_coupon'];
$basket[$key]['include_product_promo'] = $promotion['include_product_promo'];
}
}
}
}
return $basket;
}
/**
* Promocja na produkty z kategorii 1 i 2 (condition_type=2)
*/
public function applyTypeCategoriesAnd(array $basket, $promotion): array
{
$productRepo = new \Domain\Product\ProductRepository( $this->db );
$condition_1 = false;
$condition_2 = false;
$categories = $promotion['categories'];
$condition_categories = $promotion['condition_categories'];
if ( is_array( $condition_categories ) and is_array( $categories ) )
{
foreach ( $basket as $key => $val )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( !$condition_1 and in_array( $category_tmp['category_id'], $condition_categories ) )
{
$condition_1 = true;
}
}
}
foreach ( $basket as $key => $val )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( !$condition_2 and in_array( $category_tmp['category_id'], $categories ) )
$condition_2 = true;
}
}
}
if ( $condition_1 and $condition_2 )
{
foreach ( $basket as $key => $val )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( in_array( $category_tmp['category_id'], $categories ) or in_array( $category_tmp['category_id'], $condition_categories ) )
{
$basket[$key]['discount_type'] = $promotion['discount_type'];
$basket[$key]['discount_amount'] = $promotion['amount'];
$basket[$key]['discount_include_coupon'] = $promotion['include_coupon'];
$basket[$key]['include_product_promo'] = $promotion['include_product_promo'];
}
}
}
}
return $basket;
}
/**
* Rabat procentowy na produkty z kategorii I jeżeli w koszyku jest produkt z kategorii II (condition_type=1)
*/
public function applyTypeCategoryCondition(array $basket, $promotion): array
{
$productRepo = new \Domain\Product\ProductRepository( $this->db );
$condition = false;
$categories = $promotion['categories'];
$condition_categories = $promotion['condition_categories'];
if ( is_array( $condition_categories ) and is_array( $categories ) )
{
foreach ( $basket as $key => $val )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( in_array( $category_tmp['category_id'], $condition_categories ) )
{
$condition = true;
}
}
}
}
if ( $condition )
{
foreach ( $basket as $key => $val )
{
$product_categories = $productRepo->productCategoriesFront( (int) $val['product-id'] );
foreach ( $product_categories as $category_tmp )
{
if ( in_array( $category_tmp['category_id'], $categories ) )
{
$basket[$key]['discount_type'] = $promotion['discount_type'];
$basket[$key]['discount_amount'] = $promotion['amount'];
$basket[$key]['discount_include_coupon'] = $promotion['include_coupon'];
$basket[$key]['include_product_promo'] = $promotion['include_product_promo'];
}
}
}
}
return $basket;
}
}

View File

@@ -0,0 +1,345 @@
<?php
namespace Domain\Scontainers;
class ScontainersRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'id',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'q1.id',
'title' => 'q1.title',
'status' => 'q1.status',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'q1.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 = ['1 = 1'];
$params = [];
$title = trim((string)($filters['title'] ?? ''));
if ($title !== '') {
if (strlen($title) > 255) {
$title = substr($title, 0, 255);
}
$where[] = 'q1.title LIKE :title';
$params[':title'] = '%' . $title . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'q1.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$baseSelect = $this->baseListSelect();
$sqlCount = "
SELECT COUNT(0)
FROM ({$baseSelect}) AS q1
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT q1.*
FROM ({$baseSelect}) AS q1
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, q1.id DESC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
public function find(int $containerId): array
{
if ($containerId <= 0) {
return $this->defaultContainer();
}
$container = $this->db->get('pp_scontainers', '*', ['id' => $containerId]);
if (!is_array($container)) {
return $this->defaultContainer();
}
$container['languages'] = $this->translationsMap($containerId);
return $container;
}
public function detailsForLanguage(int $containerId, string $langId): ?array
{
if ($containerId <= 0 || trim($langId) === '') {
return null;
}
$container = $this->db->get('pp_scontainers', '*', ['id' => $containerId]);
if (!is_array($container)) {
return null;
}
$translation = $this->db->get('pp_scontainers_langs', '*', [
'AND' => [
'container_id' => $containerId,
'lang_id' => $langId,
],
]);
$container['languages'] = is_array($translation) ? $translation : [
'lang_id' => $langId,
'title' => '',
'text' => '',
];
return $container;
}
public function save(array $data): ?int
{
$containerId = (int)($data['id'] ?? 0);
$status = $this->toSwitchValue($data['status'] ?? 0);
$showTitle = $this->toSwitchValue($data['show_title'] ?? 0);
$translations = $this->extractTranslations($data);
if ($containerId <= 0) {
$this->db->insert('pp_scontainers', [
'status' => $status,
'show_title' => $showTitle,
]);
$containerId = (int)$this->db->id();
if ($containerId <= 0) {
return null;
}
} else {
$this->db->update('pp_scontainers', [
'status' => $status,
'show_title' => $showTitle,
], [
'id' => $containerId,
]);
}
foreach ($translations as $langId => $row) {
$translationId = $this->db->get('pp_scontainers_langs', 'id', [
'AND' => [
'container_id' => $containerId,
'lang_id' => $langId,
],
]);
if ($translationId) {
$this->db->update('pp_scontainers_langs', [
'title' => (string)($row['title'] ?? ''),
'text' => (string)($row['text'] ?? ''),
], [
'id' => (int)$translationId,
]);
} else {
$this->db->insert('pp_scontainers_langs', [
'container_id' => $containerId,
'lang_id' => $langId,
'title' => (string)($row['title'] ?? ''),
'text' => (string)($row['text'] ?? ''),
]);
}
}
\Shared\Helpers\Helpers::delete_dir('../temp/');
$this->clearFrontCache($containerId);
return $containerId;
}
public function delete(int $containerId): bool
{
if ($containerId <= 0) {
return false;
}
$result = (bool)$this->db->delete('pp_scontainers', ['id' => $containerId]);
if ($result) {
$this->clearFrontCache($containerId);
}
return $result;
}
private function baseListSelect(): string
{
return "
SELECT
ps.id,
ps.status,
(
SELECT psl.title
FROM pp_scontainers_langs AS psl
JOIN pp_langs AS pl ON psl.lang_id = pl.id
WHERE psl.container_id = ps.id
AND psl.title <> ''
ORDER BY pl.o ASC
LIMIT 1
) AS title
FROM pp_scontainers AS ps
";
}
// ── Frontend methods ──────────────────────────────────────────
public function frontScontainerDetails(int $scontainerId, string $langId): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "ScontainersRepository::frontScontainerDetails:$scontainerId";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
$cached = @unserialize($objectData);
if (is_array($cached)) {
return $cached;
}
$cacheHandler->delete($cacheKey);
}
$scontainer = $this->detailsForLanguage($scontainerId, $langId);
if (!is_array($scontainer)) {
$scontainer = [
'id' => $scontainerId,
'status' => 0,
'show_title' => 0,
'languages' => [
'lang_id' => $langId,
'title' => '',
'text' => '',
],
];
}
$cacheHandler->set($cacheKey, $scontainer);
return $scontainer;
}
private function clearFrontCache(int $containerId): void
{
if ($containerId <= 0 || !class_exists('\Shared\Cache\CacheHandler')) {
return;
}
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheHandler->delete('ScontainersRepository::frontScontainerDetails:' . $containerId);
}
/**
* @return array<string, array<string, mixed>>
*/
private function translationsMap(int $containerId): array
{
$rows = $this->db->select('pp_scontainers_langs', '*', ['container_id' => $containerId]);
if (!is_array($rows)) {
return [];
}
$result = [];
foreach ($rows as $row) {
$langId = (string)($row['lang_id'] ?? '');
if ($langId !== '') {
$result[$langId] = $row;
}
}
return $result;
}
/**
* @return array<string, array<string, string>>
*/
private function extractTranslations(array $data): array
{
$translations = [];
if (isset($data['translations']) && is_array($data['translations'])) {
foreach ($data['translations'] as $langId => $row) {
if (!is_array($row)) {
continue;
}
$safeLangId = trim((string)$langId);
if ($safeLangId === '') {
continue;
}
$translations[$safeLangId] = [
'title' => (string)($row['title'] ?? ''),
'text' => (string)($row['text'] ?? ''),
];
}
}
$legacyTitles = isset($data['title']) && is_array($data['title']) ? $data['title'] : [];
$legacyTexts = isset($data['text']) && is_array($data['text']) ? $data['text'] : [];
foreach ($legacyTitles as $langId => $title) {
$safeLangId = trim((string)$langId);
if ($safeLangId === '') {
continue;
}
if (!isset($translations[$safeLangId])) {
$translations[$safeLangId] = [
'title' => '',
'text' => '',
];
}
$translations[$safeLangId]['title'] = (string)$title;
$translations[$safeLangId]['text'] = (string)($legacyTexts[$safeLangId] ?? '');
}
return $translations;
}
private function toSwitchValue($value): int
{
return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0;
}
private function defaultContainer(): array
{
return [
'id' => 0,
'status' => 1,
'show_title' => 0,
'languages' => [],
];
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Domain\Settings;
/**
* Repozytorium ustawien — wspolne dla admin i frontendu.
*/
class SettingsRepository
{
private $db;
public function __construct($db = null)
{
if ($db) {
$this->db = $db;
return;
}
global $mdb;
$this->db = $mdb;
}
/**
* Zapis ustawien.
*
* @param array $values Tablica wartosci z formularza
* @return array ['status' => string, 'msg' => string]
*/
public function saveSettings(array $values): array
{
$currentSettings = $this->getSettings();
$settingsToSave = [
'firm_name' => $values['firm_name'] ?? '',
'firm_adress' => $values['firm_adress'] ?? '',
'additional_info' => $values['additional_info'] ?? '',
'contact_form' => $this->isEnabled($values['contact_form'] ?? null) ? 1 : 0,
'contact_email' => $values['contact_email'] ?? '',
'email_host' => $values['email_host'] ?? '',
'email_port' => $values['email_port'] ?? '',
'email_login' => $values['email_login'] ?? '',
'email_password' => $values['email_password'] ?? '',
'google_maps' => $values['google_maps'] ?? '',
'facebook_link' => $values['facebook_link'] ?? '',
'statistic_code' => $values['statistic_code'] ?? '',
'htaccess' => $values['htaccess'] ?? '',
'robots' => $values['robots'] ?? '',
'shop_bank_account_info' => $values['shop_bank_account_info'] ?? '',
'update' => $this->isEnabled($values['update'] ?? null) ? 1 : 0,
'boot_animation' => $values['boot_animation'] ?? '',
// Te pola sa edytowane w module newsletter i musza zostac zachowane.
'newsletter_header' => $currentSettings['newsletter_header'] ?? '',
'newsletter_footer' => $currentSettings['newsletter_footer'] ?? '',
'hotpay_api' => $values['hotpay_api'] ?? '',
'devel' => $this->isEnabled($values['devel'] ?? null) ? 1 : 0,
'ssl' => $this->isEnabled($values['ssl'] ?? null) ? 1 : 0,
'htaccess_cache' => $this->isEnabled($values['htaccess_cache'] ?? null) ? 1 : 0,
'free_delivery' => $values['free_delivery'] ?? '',
'przelewy24_sandbox' => $this->isEnabled($values['przelewy24_sandbox'] ?? null) ? 1 : 0,
'przelewy24_merchant_id' => $values['przelewy24_merchant_id'] ?? '',
'przelewy24_crc_key' => $values['przelewy24_crc_key'] ?? '',
'update_key' => $values['update_key'] ?? '',
'tpay_id' => $values['tpay_id'] ?? '',
'tpay_sandbox' => $this->isEnabled($values['tpay_sandbox'] ?? null) ? 1 : 0,
'tpay_security_code' => $values['tpay_security_code'] ?? '',
'piksel' => $values['piksel'] ?? '',
'generate_webp' => $this->isEnabled($values['generate_webp'] ?? null) ? 1 : 0,
'lazy_loading' => $this->isEnabled($values['lazy_loading'] ?? null) ? 1 : 0,
'orlen_paczka_map_token' => $values['orlen_paczka_map_token'] ?? '',
'google_tag_manager_id' => $values['google_tag_manager_id'] ?? '',
'infinitescroll' => $this->isEnabled($values['infinitescroll'] ?? null) ? 1 : 0,
'own_gtm_js' => $values['own_gtm_js'] ?? '',
'own_gtm_html' => $values['own_gtm_html'] ?? '',
];
$warehouseMessageZero = $values['warehouse_message_zero'] ?? [];
if (is_array($warehouseMessageZero)) {
foreach ($warehouseMessageZero as $key => $value) {
$settingsToSave['warehouse_message_zero_' . $key] = $value;
}
}
$warehouseMessageNonZero = $values['warehouse_message_nonzero'] ?? [];
if (is_array($warehouseMessageNonZero)) {
foreach ($warehouseMessageNonZero as $key => $value) {
$settingsToSave['warehouse_message_nonzero_' . $key] = $value;
}
}
// Zachowanie zgodne z dotychczasowym flow: pelna podmiana zestawu ustawien.
$this->db->query('TRUNCATE pp_settings');
$this->updateSettings($settingsToSave);
\Shared\Helpers\Helpers::delete_dir('../temp/');
\Shared\Helpers\Helpers::set_message('Ustawienia zostaly zapisane');
return ['status' => 'ok', 'msg' => 'Ustawienia zostaly zapisane.'];
}
/**
* Aktualizacja pojedynczego parametru.
*/
public function updateSetting(string $param, $value): bool
{
$this->db->delete('pp_settings', ['param' => $param]);
$this->db->insert('pp_settings', ['param' => $param, 'value' => $value]);
return true;
}
/**
* Aktualizacja wielu parametrow przez jedna sciezke.
*/
public function updateSettings(array $settings): bool
{
foreach ($settings as $param => $value) {
$this->updateSetting((string)$param, $value);
}
return true;
}
/**
* Pobranie wszystkich ustawien.
*
* @return array Tablica ustawien [param => value]
*/
public function getSettings(): array
{
$results = $this->db->select('pp_settings', '*', ['ORDER' => ['id' => 'ASC']]);
$settings = [];
if (is_array($results)) {
foreach ($results as $row) {
if (isset($row['param'])) {
$settings[$row['param']] = $row['value'] ?? '';
}
}
}
return $settings;
}
/**
* Pobranie wszystkich ustawien z cache Redis.
*
* @param bool $skipCache Pomija cache (np. przy generowaniu htaccess)
*/
public function allSettings(bool $skipCache = false): array
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'Domain\Settings\SettingsRepository::allSettings';
if (!$skipCache) {
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
}
$results = $this->db->select('pp_settings', '*');
$settings = [];
if (is_array($results)) {
foreach ($results as $row) {
$settings[$row['param']] = $row['value'];
}
}
$cacheHandler->set($cacheKey, $settings);
return $settings;
}
/**
* Pobranie pojedynczej wartosci ustawienia po nazwie parametru.
*/
public function getSingleValue(string $param): string
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = "Domain\Settings\SettingsRepository::getSingleValue:$param";
$objectData = $cacheHandler->get($cacheKey);
if ($objectData) {
return unserialize($objectData);
}
$value = $this->db->get('pp_settings', 'value', ['param' => $param]);
$value = (string)($value ?? '');
$cacheHandler->set($cacheKey, $value);
return $value;
}
private function isEnabled($value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_int($value) || is_float($value)) {
return (int)$value === 1;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true);
}
return false;
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Domain\ShopStatus;
class ShopStatusRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'o',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'ss.id',
'status' => 'ss.status',
'color' => 'ss.color',
'o' => 'ss.o',
'apilo_status_id' => 'ss.apilo_status_id',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'ss.o';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$status = trim((string)($filters['status'] ?? ''));
if ($status !== '') {
if (strlen($status) > 255) {
$status = substr($status, 0, 255);
}
$where[] = 'ss.status LIKE :status';
$params[':status'] = '%' . $status . '%';
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_statuses AS ss
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
ss.id,
ss.status,
ss.color,
ss.o,
ss.apilo_status_id
FROM pp_shop_statuses AS ss
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, ss.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item['id'] = (int)($item['id'] ?? 0);
$item['apilo_status_id'] = $item['apilo_status_id'] !== null
? (int)$item['apilo_status_id']
: null;
$item['o'] = (int)($item['o'] ?? 0);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
public function find(int $statusId): ?array
{
if ($statusId < 0) {
return null;
}
$status = $this->db->get('pp_shop_statuses', '*', ['id' => $statusId]);
if (!is_array($status)) {
return null;
}
$status['id'] = (int)($status['id'] ?? 0);
$status['apilo_status_id'] = $status['apilo_status_id'] !== null
? (int)$status['apilo_status_id']
: null;
$status['o'] = (int)($status['o'] ?? 0);
return $status;
}
public function save(int $statusId, array $data): int
{
if ($statusId < 0) {
return 0;
}
$row = [
'color' => trim((string)($data['color'] ?? '')),
'apilo_status_id' => isset($data['apilo_status_id']) && $data['apilo_status_id'] !== ''
? (int)$data['apilo_status_id']
: null,
];
$this->db->update('pp_shop_statuses', $row, ['id' => $statusId]);
return $statusId;
}
/**
* Pobiera Apilo status ID dla danego statusu sklepowego.
* Odpowiednik front\factory\ShopStatuses::get_apilo_status_id()
*/
public function getApiloStatusId(int $statusId): ?int
{
$value = $this->db->get('pp_shop_statuses', 'apilo_status_id', ['id' => $statusId]);
return $value !== null && $value !== false ? (int)$value : null;
}
/**
* Pobiera shop status ID na podstawie ID statusu integracji.
* Odpowiednik front\factory\ShopStatuses::get_shop_status_by_integration_status_id()
*/
public function getByIntegrationStatusId(string $integration, int $integrationStatusId): ?int
{
if ($integration === 'apilo') {
$value = $this->db->get('pp_shop_statuses', 'id', [
'apilo_status_id' => $integrationStatusId,
]);
return $value !== null && $value !== false ? (int)$value : null;
}
return null;
}
/**
* Zwraca liste wszystkich statusow (id => nazwa) posortowanych wg kolejnosci.
* Odpowiednik shop\Order::order_statuses()
*/
public function allStatuses(): array
{
$results = $this->db->select('pp_shop_statuses', ['id', 'status'], [
'ORDER' => ['o' => 'ASC'],
]);
$statuses = [];
if (is_array($results)) {
foreach ($results as $row) {
$statuses[(int)$row['id']] = $row['status'];
}
}
return $statuses;
}
}

View File

@@ -0,0 +1,471 @@
<?php
namespace Domain\Transport;
class TransportRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function listForAdmin(
array $filters = [],
string $sortColumn = 'name',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'id' => 'st.id',
'name' => 'st.name',
'status' => 'st.status',
'cost' => 'st.cost',
'max_wp' => 'st.max_wp',
'default' => 'st.default',
'o' => 'st.o',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'st.name';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['1 = 1'];
$params = [];
$name = trim((string)($filters['name'] ?? ''));
if ($name !== '') {
if (strlen($name) > 255) {
$name = substr($name, 0, 255);
}
$where[] = 'st.name LIKE :name';
$params[':name'] = '%' . $name . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'st.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_transports AS st
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
st.id,
st.name,
st.name_visible,
st.description,
st.status,
st.cost,
st.max_wp,
st.default,
st.apilo_carrier_account_id,
st.delivery_free,
st.o
FROM pp_shop_transports AS st
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, st.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
if (!is_array($items)) {
$items = [];
}
foreach ($items as &$item) {
$item = $this->normalizeTransport($item);
}
unset($item);
return [
'items' => $items,
'total' => $total,
];
}
public function find(int $transportId): ?array
{
if ($transportId <= 0) {
return null;
}
$transport = $this->db->get('pp_shop_transports', '*', ['id' => $transportId]);
if (!is_array($transport)) {
return null;
}
$transport = $this->normalizeTransport($transport);
$paymentMethods = $this->db->select(
'pp_shop_transport_payment_methods',
'id_payment_method',
['id_transport' => $transportId]
);
$transport['payment_methods'] = is_array($paymentMethods) ? $paymentMethods : [];
return $transport;
}
public function save(array $data): ?int
{
$transportId = isset($data['id']) ? (int)$data['id'] : 0;
$name = trim((string)($data['name'] ?? ''));
$nameVisible = trim((string)($data['name_visible'] ?? ''));
$description = trim((string)($data['description'] ?? ''));
$status = $this->toSwitchValue($data['status'] ?? 0);
$cost = isset($data['cost']) ? (float)$data['cost'] : 0.0;
$maxWp = isset($data['max_wp']) && $data['max_wp'] !== '' ? (int)$data['max_wp'] : null;
$default = $this->toSwitchValue($data['default'] ?? 0);
$apiloCarrierAccountId = isset($data['apilo_carrier_account_id']) && $data['apilo_carrier_account_id'] !== ''
? (int)$data['apilo_carrier_account_id']
: null;
$deliveryFree = $this->toSwitchValue($data['delivery_free'] ?? 0);
$paymentMethods = $data['payment_methods'] ?? [];
if ($default === 1) {
$this->db->update('pp_shop_transports', ['default' => 0]);
}
$transportData = [
'name' => $name,
'name_visible' => $nameVisible,
'description' => $description,
'status' => $status,
'default' => $default,
'cost' => $cost,
'max_wp' => $maxWp,
'apilo_carrier_account_id' => $apiloCarrierAccountId,
'delivery_free' => $deliveryFree,
];
if (!$transportId) {
$this->db->insert('pp_shop_transports', $transportData);
$id = $this->db->id();
if ($id) {
$this->savePaymentMethodLinks((int)$id, $paymentMethods);
return (int)$id;
}
return null;
} else {
$this->db->update('pp_shop_transports', $transportData, ['id' => $transportId]);
$this->db->delete('pp_shop_transport_payment_methods', ['id_transport' => $transportId]);
$this->savePaymentMethodLinks($transportId, $paymentMethods);
return $transportId;
}
}
public function allActive(): array
{
$transports = $this->db->select(
'pp_shop_transports',
'*',
[
'status' => 1,
'ORDER' => ['o' => 'ASC'],
]
);
if (!is_array($transports)) {
return [];
}
foreach ($transports as &$transport) {
$transport = $this->normalizeTransport($transport);
}
unset($transport);
return $transports;
}
public function findActiveById(int $transportId): ?array
{
if ($transportId <= 0) {
return null;
}
$transport = $this->db->get(
'pp_shop_transports',
'*',
['AND' => ['id' => $transportId, 'status' => 1]]
);
if (!$transport) {
return null;
}
return $this->normalizeTransport($transport);
}
public function getApiloCarrierAccountId(int $transportId): ?int
{
if ($transportId <= 0) {
return null;
}
$result = $this->db->get(
'pp_shop_transports',
'apilo_carrier_account_id',
['id' => $transportId]
);
return $result !== null ? (int)$result : null;
}
public function getTransportCost(int $transportId): ?float
{
if ($transportId <= 0) {
return null;
}
$result = $this->db->get(
'pp_shop_transports',
'cost',
['AND' => ['id' => $transportId, 'status' => 1]]
);
return $result !== null ? (float)$result : null;
}
public function lowestTransportPrice(int $wp): ?float
{
$result = $this->db->get(
'pp_shop_transports',
'cost',
[
'AND' => [
'status' => 1,
'id' => [2, 4, 6, 8, 9],
'max_wp[>=]' => $wp
],
'ORDER' => ['cost' => 'ASC']
]
);
return $result !== null ? (float)$result : null;
}
public function allForAdmin(): array
{
$transports = $this->db->select(
'pp_shop_transports',
'*',
['ORDER' => ['o' => 'ASC']]
);
if (!is_array($transports)) {
return [];
}
foreach ($transports as &$transport) {
$transport = $this->normalizeTransport($transport);
}
unset($transport);
return $transports;
}
// =========================================================================
// Frontend methods (migrated from front\factory\ShopTransport)
// =========================================================================
/**
* Lista metod transportu dla koszyka (z filtrowaniem wagi + darmowa dostawa)
*/
public function transportMethodsFront( $basket, $coupon ): array
{
global $settings;
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'transport_methods_front';
$cached = $cacheHandler->get( $cacheKey );
if ( $cached )
{
$transports_tmp = unserialize( $cached );
}
else
{
$transports_tmp = $this->allActive();
$cacheHandler->set( $cacheKey, $transports_tmp );
}
$wp_summary = \Domain\Basket\BasketCalculator::summaryWp( $basket );
$transports = [];
foreach ( $transports_tmp as $tr )
{
if ( $tr['max_wp'] == null )
$transports[] = $tr;
elseif ( $tr['max_wp'] != null and $wp_summary <= $tr['max_wp'] )
$transports[] = $tr;
}
if ( \Shared\Helpers\Helpers::normalize_decimal( \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) ) >= \Shared\Helpers\Helpers::normalize_decimal( $settings['free_delivery'] ) )
{
for ( $i = 0; $i < count( $transports ); $i++ ) {
if ( $transports[$i]['delivery_free'] == 1 ) {
$transports[$i]['cost'] = 0.00;
}
}
}
return $transports;
}
/**
* Koszt transportu z cache
*/
public function transportCostCached( $transportId )
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'transport_cost_' . $transportId;
$cached = $cacheHandler->get( $cacheKey );
if ( $cached )
{
return unserialize( $cached );
}
$cost = $this->getTransportCost( (int)$transportId );
$cacheHandler->set( $cacheKey, $cost );
return $cost;
}
/**
* Aktywny transport z cache
*/
public function findActiveByIdCached( $transportId )
{
$cacheHandler = new \Shared\Cache\CacheHandler();
$cacheKey = 'transport' . $transportId;
$cached = $cacheHandler->get( $cacheKey );
if ( $cached )
{
return unserialize( $cached );
}
$transport = $this->findActiveById( (int)$transportId );
$cacheHandler->set( $cacheKey, $transport );
return $transport;
}
/**
* Transporty powiązane z metodą płatności
*/
public function forPaymentMethod( int $paymentMethodId ): array
{
if ( $paymentMethodId <= 0 )
{
return [];
}
$transportIds = $this->db->select(
'pp_shop_transport_payment_methods',
'id_transport',
['id_payment_method' => $paymentMethodId]
);
if ( !is_array( $transportIds ) || empty( $transportIds ) )
{
return [];
}
$transports = $this->db->select(
'pp_shop_transports',
'*',
['AND' => ['id' => $transportIds, 'status' => 1], 'ORDER' => ['o' => 'ASC']]
);
if ( !is_array( $transports ) )
{
return [];
}
foreach ( $transports as &$transport )
{
$transport = $this->normalizeTransport( $transport );
}
unset( $transport );
return $transports;
}
private function savePaymentMethodLinks(int $transportId, $paymentMethods): void
{
if (is_array($paymentMethods)) {
foreach ($paymentMethods as $paymentMethodId) {
$this->db->insert('pp_shop_transport_payment_methods', [
'id_payment_method' => (int)$paymentMethodId,
'id_transport' => $transportId,
]);
}
} elseif ($paymentMethods) {
$this->db->insert('pp_shop_transport_payment_methods', [
'id_payment_method' => (int)$paymentMethods,
'id_transport' => $transportId,
]);
}
}
private function normalizeTransport(array $transport): array
{
$transport['id'] = isset($transport['id']) ? (int)$transport['id'] : 0;
$transport['status'] = $this->toSwitchValue($transport['status'] ?? 0);
$transport['default'] = $this->toSwitchValue($transport['default'] ?? 0);
$transport['delivery_free'] = $this->toSwitchValue($transport['delivery_free'] ?? 0);
$transport['cost'] = isset($transport['cost']) ? (float)$transport['cost'] : 0.0;
$transport['max_wp'] = isset($transport['max_wp']) && $transport['max_wp'] !== null
? (int)$transport['max_wp']
: null;
$transport['apilo_carrier_account_id'] = isset($transport['apilo_carrier_account_id']) && $transport['apilo_carrier_account_id'] !== null
? (int)$transport['apilo_carrier_account_id']
: null;
$transport['o'] = isset($transport['o']) ? (int)$transport['o'] : 0;
return $transport;
}
private function toSwitchValue($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return ((int)$value) === 1 ? 1 : 0;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0;
}
return 0;
}
}

View File

@@ -0,0 +1,319 @@
<?php
namespace Domain\Update;
class UpdateRepository
{
private $db;
public function __construct( $db )
{
$this->db = $db;
}
/**
* Wykonuje aktualizację do następnej wersji.
*
* @return array{success: bool, log: array, no_updates?: bool}
*/
public function update(): array
{
global $settings;
@file_put_contents( '../libraries/update_log.txt', '' );
$log = [];
$log[] = '[START] Rozpoczęcie aktualizacji - ' . date( 'Y-m-d H:i:s' );
$log[] = '[INFO] Aktualna wersja: ' . \Shared\Helpers\Helpers::get_version();
\Shared\Helpers\Helpers::delete_session( 'new-version' );
$versionsUrl = 'https://shoppro.project-dc.pl/updates/versions.php?key=' . $settings['update_key'];
$versions = @file_get_contents( $versionsUrl );
if ( $versions === false ) {
$log[] = '[ERROR] Nie udało się pobrać listy wersji z: ' . $versionsUrl;
$this->saveLog( $log );
return [ 'success' => false, 'log' => $log ];
}
$log[] = '[OK] Pobrano listę wersji';
$versions = explode( PHP_EOL, $versions );
$log[] = '[INFO] Znaleziono ' . count( $versions ) . ' wersji do sprawdzenia';
foreach ( $versions as $ver ) {
$ver = trim( $ver );
if ( floatval( $ver ) <= (float) \Shared\Helpers\Helpers::get_version() ) {
continue;
}
$log[] = '[INFO] Aktualizacja do wersji: ' . $ver;
$dir = strlen( $ver ) == 5
? substr( $ver, 0, strlen( $ver ) - 2 ) . '0'
: substr( $ver, 0, strlen( $ver ) - 1 ) . '0';
$result = $this->downloadAndApply( $ver, $dir, $log );
$this->saveLog( $result['log'] );
return $result;
}
$log[] = '[INFO] Brak nowych wersji do zainstalowania';
$this->saveLog( $log );
return [ 'success' => true, 'log' => $log, 'no_updates' => true ];
}
private function downloadAndApply( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
// Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;
$file = @file_get_contents( $zipUrl );
if ( $file === false ) {
$log[] = '[ERROR] Nie udało się pobrać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$fileSize = strlen( $file );
$log[] = '[OK] Pobrano plik ZIP, rozmiar: ' . $fileSize . ' bajtów';
if ( $fileSize < 100 ) {
$log[] = '[ERROR] Plik ZIP jest za mały (prawdopodobnie błąd pobierania)';
return [ 'success' => false, 'log' => $log ];
}
$dlHandler = @fopen( 'update.zip', 'w' );
if ( !$dlHandler ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku update.zip do zapisu';
$log[] = '[INFO] Katalog roboczy: ' . getcwd();
return [ 'success' => false, 'log' => $log ];
}
$written = fwrite( $dlHandler, $file );
fclose( $dlHandler );
if ( $written === false || $written === 0 ) {
$log[] = '[ERROR] Nie udało się zapisać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$log[] = '[OK] Zapisano plik ZIP (' . $written . ' bajtów)';
// Wykonanie SQL
$log = $this->executeSql( $baseUrl . '/ver_' . $ver . '_sql.txt', $log );
// Usuwanie plików
$log = $this->deleteFiles( $baseUrl . '/ver_' . $ver . '_files.txt', $log );
// Rozpakowywanie ZIP
$log = $this->extractZip( 'update.zip', $log );
// Aktualizacja wersji
$versionFile = '../libraries/version.ini';
$handle = @fopen( $versionFile, 'w' );
if ( !$handle ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku version.ini do zapisu';
return [ 'success' => false, 'log' => $log ];
}
fwrite( $handle, $ver );
fclose( $handle );
$log[] = '[OK] Zaktualizowano plik version.ini do wersji: ' . $ver;
$log[] = '[SUCCESS] Aktualizacja do wersji ' . $ver . ' zakończona pomyślnie';
return [ 'success' => true, 'log' => $log ];
}
private function executeSql( string $sqlUrl, array $log ): array
{
$log[] = '[INFO] Sprawdzanie aktualizacji SQL: ' . $sqlUrl;
$ch = curl_init( $sqlUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HEADER, false );
$response = curl_exec( $ch );
$contentType = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE );
$httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( !$response || strpos( $contentType, 'text/plain' ) === false ) {
$log[] = '[INFO] Brak aktualizacji SQL (HTTP: ' . $httpCode . ')';
return $log;
}
$queries = explode( PHP_EOL, $response );
$log[] = '[OK] Pobrano ' . count( $queries ) . ' zapytań SQL';
$success = 0;
$errors = 0;
foreach ( $queries as $query ) {
$query = trim( $query );
if ( $query !== '' ) {
if ( $this->db->query( $query ) ) {
$success++;
} else {
$errors++;
}
}
}
$log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors;
return $log;
}
private function deleteFiles( string $filesUrl, array $log ): array
{
$log[] = '[INFO] Sprawdzanie plików do usunięcia: ' . $filesUrl;
$ch = curl_init( $filesUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HEADER, false );
$response = curl_exec( $ch );
$contentType = curl_getinfo( $ch, CURLINFO_CONTENT_TYPE );
curl_close( $ch );
if ( !$response || strpos( $contentType, 'text/plain' ) === false ) {
$log[] = '[INFO] Brak plików do usunięcia';
return $log;
}
$files = explode( PHP_EOL, $response );
$deletedFiles = 0;
$deletedDirs = 0;
foreach ( $files as $entry ) {
if ( strpos( $entry, 'F: ' ) !== false ) {
$path = substr( $entry, 3 );
if ( file_exists( $path ) ) {
if ( @unlink( $path ) ) {
$deletedFiles++;
} else {
$log[] = '[WARNING] Nie udało się usunąć pliku: ' . $path;
}
}
}
if ( strpos( $entry, 'D: ' ) !== false ) {
$path = substr( $entry, 3 );
if ( is_dir( $path ) ) {
\Shared\Helpers\Helpers::delete_dir( $path );
$deletedDirs++;
}
}
}
$log[] = '[INFO] Usunięto plików: ' . $deletedFiles . ', katalogów: ' . $deletedDirs;
return $log;
}
private function extractZip( string $fileName, array $log ): array
{
$log[] = '[INFO] Rozpoczęcie rozpakowywania pliku ZIP';
$path = pathinfo( realpath( $fileName ), PATHINFO_DIRNAME );
$path = substr( $path, 0, strlen( $path ) - 5 );
if ( !is_dir( $path ) || !is_writable( $path ) ) {
$log[] = '[ERROR] Ścieżka docelowa nie istnieje lub brak uprawnień: ' . $path;
return $log;
}
$zip = new \ZipArchive;
$res = $zip->open( $fileName );
if ( $res !== true ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku ZIP (kod: ' . $res . ')';
return $log;
}
$log[] = '[OK] Otwarto archiwum ZIP, liczba plików: ' . $zip->numFiles;
$extracted = 0;
$errors = 0;
for ( $i = 0; $i < $zip->numFiles; $i++ ) {
$filename = str_replace( '\\', '/', $zip->getNameIndex( $i ) );
if ( substr( $filename, -1 ) === '/' ) {
$dirPath = $path . '/' . $filename;
if ( !is_dir( $dirPath ) ) {
@mkdir( $dirPath, 0755, true );
}
continue;
}
$targetFile = $path . '/' . $filename;
$targetDir = dirname( $targetFile );
if ( !is_dir( $targetDir ) ) {
@mkdir( $targetDir, 0755, true );
}
$existed = file_exists( $targetFile );
$content = $zip->getFromIndex( $i );
if ( $content === false ) {
$log[] = '[ERROR] Nie udało się odczytać z ZIP: ' . $filename;
$errors++;
continue;
}
if ( @file_put_contents( $targetFile, $content ) === false ) {
$log[] = '[ERROR] Nie udało się zapisać: ' . $filename;
$errors++;
} else {
$tag = $existed ? '[UPDATED]' : '[NEW]';
$log[] = $tag . ' ' . $filename . ' (' . strlen( $content ) . ' bajtów)';
$extracted++;
}
}
$log[] = '[OK] Rozpakowano ' . $extracted . ' plików, błędów: ' . $errors;
$zip->close();
if ( @unlink( $fileName ) ) {
$log[] = '[OK] Usunięto plik update.zip';
}
return $log;
}
private function saveLog( array $log ): void
{
@file_put_contents( '../libraries/update_log.txt', implode( "\n", $log ) );
}
/**
* Wykonuje zaległe migracje z tabeli pp_updates.
*/
public function runPendingMigrations(): void
{
$results = $this->db->select( 'pp_updates', [ 'name' ], [ 'done' => 0 ] );
if ( !is_array( $results ) ) {
return;
}
foreach ( $results as $row ) {
$method = $row['name'];
if ( method_exists( $this, $method ) ) {
$this->$method();
}
}
}
public function update0197(): void
{
$rows = $this->db->select( 'pp_shop_order_products', [ 'id', 'product_id' ], [ 'parent_product_id' => null ] );
if ( is_array( $rows ) ) {
foreach ( $rows as $row ) {
$parentId = $this->db->get( 'pp_shop_products', 'parent_id', [ 'id' => $row['product_id'] ] );
$this->db->update( 'pp_shop_order_products', [
'parent_product_id' => $parentId ?: $row['product_id'],
], [ 'id' => $row['id'] ] );
}
}
$this->db->update( 'pp_updates', [ 'done' => 1 ], [ 'name' => 'update0197' ] );
}
}

View File

@@ -0,0 +1,339 @@
<?php
namespace Domain\User;
/**
* Repository odpowiedzialny za dostep do danych uzytkownikow admina.
*/
class UserRepository
{
private const MAX_PER_PAGE = 100;
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function getById(int $userId): ?array
{
$user = $this->db->get('pp_users', '*', ['id' => $userId]);
return $user ?: null;
}
public function updateById(int $userId, array $data): bool
{
return (bool)$this->db->update('pp_users', $data, ['id' => $userId]);
}
public function verifyTwofaCode(int $userId, string $code): bool
{
$user = $this->getById($userId);
if (!$user) {
return false;
}
if ((int)($user['twofa_failed_attempts'] ?? 0) >= 5) {
return false;
}
if (empty($user['twofa_expires_at']) || time() > strtotime((string)$user['twofa_expires_at'])) {
$this->updateById($userId, [
'twofa_code_hash' => null,
'twofa_expires_at' => null,
]);
return false;
}
$ok = (!empty($user['twofa_code_hash']) && password_verify($code, (string)$user['twofa_code_hash']));
if ($ok) {
$this->updateById($userId, [
'twofa_code_hash' => null,
'twofa_expires_at' => null,
'twofa_sent_at' => null,
'twofa_failed_attempts' => 0,
'last_logged' => date('Y-m-d H:i:s'),
]);
return true;
}
$this->updateById($userId, [
'twofa_failed_attempts' => (int)($user['twofa_failed_attempts'] ?? 0) + 1,
'last_error_logged' => date('Y-m-d H:i:s'),
]);
return false;
}
public function sendTwofaCode(int $userId, bool $resend = false): bool
{
$user = $this->getById($userId);
if (!$user) {
return false;
}
if ((int)($user['twofa_enabled'] ?? 0) !== 1) {
return false;
}
$to = !empty($user['twofa_email']) ? (string)$user['twofa_email'] : (string)$user['login'];
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
return false;
}
if ($resend && !empty($user['twofa_sent_at'])) {
$last = strtotime((string)$user['twofa_sent_at']);
if ($last && (time() - $last) < 30) {
return false;
}
}
$code = random_int(100000, 999999);
$hash = password_hash((string)$code, PASSWORD_DEFAULT);
$this->updateById($userId, [
'twofa_code_hash' => $hash,
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60),
'twofa_sent_at' => date('Y-m-d H:i:s'),
'twofa_failed_attempts' => 0,
]);
$subject = 'Twoj kod logowania 2FA';
$body = 'Twoj kod logowania do panelu administratora: ' . $code . '. Kod jest wazny przez 10 minut.';
$sent = \Shared\Helpers\Helpers::send_email($to, $subject, $body);
if ($sent) {
return true;
}
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
$headers .= "From: no-reply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
return mail($to, $encodedSubject, $body, $headers);
}
public function delete(int $userId): bool
{
return (bool)$this->db->delete('pp_users', ['id' => $userId]);
}
public function find(int $userId): ?array
{
$user = $this->db->get('pp_users', '*', ['id' => $userId]);
return $user ?: null;
}
public function save(
int $userId,
string $login,
$status,
string $password,
string $passwordRepeat,
$admin,
$twofaEnabled = 0,
string $twofaEmail = ''
): array {
if ($userId <= 0) {
if (strlen($password) < 5) {
return ['status' => 'error', 'msg' => 'Podane haslo jest zbyt krotkie.'];
}
if ($password !== $passwordRepeat) {
return ['status' => 'error', 'msg' => 'Podane hasla sa rozne'];
}
$inserted = $this->db->insert('pp_users', [
'login' => $login,
'status' => $this->toSwitchValue($status),
'admin' => (int)$admin,
'password' => md5($password),
'twofa_enabled' => $this->toSwitchValue($twofaEnabled),
'twofa_email' => $twofaEmail,
]);
if ($inserted) {
\Shared\Helpers\Helpers::delete_dir('../temp/');
return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.'];
}
return ['status' => 'error', 'msg' => 'Podczas zapisywania uzytkownika wystapil blad.'];
}
if ($password !== '' && strlen($password) < 5) {
return ['status' => 'error', 'msg' => 'Podane haslo jest zbyt krotkie.'];
}
if ($password !== '' && $password !== $passwordRepeat) {
return ['status' => 'error', 'msg' => 'Podane hasla sa rozne'];
}
if ($password !== '') {
$this->db->update('pp_users', [
'password' => md5($password),
], [
'id' => $userId,
]);
}
$this->db->update('pp_users', [
'login' => $login,
'admin' => (int)$admin,
'status' => $this->toSwitchValue($status),
'twofa_enabled' => $this->toSwitchValue($twofaEnabled),
'twofa_email' => $twofaEmail,
], [
'id' => $userId,
]);
\Shared\Helpers\Helpers::delete_dir('../temp/');
return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.'];
}
public function checkLogin(string $login, int $userId): array
{
$existing = $this->db->get('pp_users', 'login', [
'AND' => [
'login' => $login,
'id[!]' => $userId,
],
]);
if ($existing) {
return ['status' => 'error', 'msg' => 'Podany login jest juz zajety.'];
}
return ['status' => 'ok'];
}
public function logon(string $login, string $password): int
{
if (!$this->db->get('pp_users', '*', ['login' => $login])) {
return 0;
}
if (!$this->db->get('pp_users', '*', [
'AND' => [
'login' => $login,
'status' => 1,
'error_logged_count[<]' => 5,
],
])) {
return -1;
}
if ($this->db->get('pp_users', '*', [
'AND' => [
'login' => $login,
'status' => 1,
'password' => md5($password),
],
])) {
$this->db->update('pp_users', [
'last_logged' => date('Y-m-d H:i:s'),
'error_logged_count' => 0,
], [
'login' => $login,
]);
return 1;
}
$this->db->update('pp_users', [
'last_error_logged' => date('Y-m-d H:i:s'),
'error_logged_count[+]' => 1,
], [
'login' => $login,
]);
if ((int)$this->db->get('pp_users', 'error_logged_count', ['login' => $login]) >= 5) {
$this->db->update('pp_users', ['status' => 0], ['login' => $login]);
return -1;
}
return 0;
}
public function details(string $login): ?array
{
$user = $this->db->get('pp_users', '*', ['login' => $login]);
return $user ?: null;
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'login',
string $sortDir = 'ASC',
int $page = 1,
int $perPage = 15
): array {
$allowedSortColumns = [
'login' => 'pu.login',
'status' => 'pu.status',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pu.login';
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['pu.id != 1'];
$params = [];
$login = trim((string)($filters['login'] ?? ''));
if ($login !== '') {
if (strlen($login) > 255) {
$login = substr($login, 0, 255);
}
$where[] = 'pu.login LIKE :login';
$params[':login'] = '%' . $login . '%';
}
$status = trim((string)($filters['status'] ?? ''));
if ($status === '0' || $status === '1') {
$where[] = 'pu.status = :status';
$params[':status'] = (int)$status;
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_users AS pu
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
pu.id,
pu.login,
pu.status
FROM pp_users AS pu
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, pu.id ASC
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
private function toSwitchValue($value): int
{
return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0;
}
}