*/
- private function toTableRow(array $row, array $statusLabelMap): array
+ private function toTableRow(array $row, array $statusLabelMap, array $statusColorMap = []): array
{
$internalOrderNumber = trim((string) ($row['internal_order_number'] ?? ''));
$sourceOrderId = trim((string) ($row['source_order_id'] ?? ''));
@@ -257,8 +258,8 @@ final class OrdersController
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
. ''
. ''
- . '' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . ''
- . '' . htmlspecialchars($source, ENT_QUOTES, 'UTF-8') . ''
+ . '' . htmlspecialchars($this->sourceLabel($source), ENT_QUOTES, 'UTF-8') . ''
+ . 'ID: ' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . ''
. '
'
. '',
'buyer' => ''
@@ -269,7 +270,7 @@ final class OrdersController
. '
'
. '',
'status_badges' => ''
- . $this->statusBadge($status, $this->statusLabel($status, $statusLabelMap))
+ . $this->statusBadge($status, $this->statusLabel($status, $statusLabelMap), $statusColorMap[strtolower(trim($status))] ?? '')
. '
',
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty),
'totals' => ''
@@ -285,10 +286,16 @@ final class OrdersController
];
}
- private function statusBadge(string $statusCode, string $statusLabel): string
+ private function statusBadge(string $statusCode, string $statusLabel, string $colorHex = ''): string
{
$label = $statusLabel !== '' ? $statusLabel : '-';
$code = strtolower(trim($statusCode));
+
+ if ($colorHex !== '') {
+ $style = 'background-color:' . htmlspecialchars($colorHex, ENT_QUOTES, 'UTF-8') . ';color:#fff';
+ return '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '';
+ }
+
$class = 'is-neutral';
if (in_array($code, ['shipped', 'delivered'], true)) {
$class = 'is-success';
@@ -303,6 +310,16 @@ final class OrdersController
return '' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '';
}
+ private function sourceLabel(string $source): string
+ {
+ return match (strtolower(trim($source))) {
+ 'allegro' => 'Allegro',
+ 'shoppro' => 'shopPRO',
+ 'erli' => 'Erli',
+ default => ucfirst(strtolower(trim($source))),
+ };
+ }
+
private function statusLabel(string $statusCode, array $statusLabelMap = []): string
{
$key = strtolower(trim($statusCode));
@@ -475,6 +492,30 @@ final class OrdersController
return $map;
}
+ /**
+ * @param array}> $config
+ * @return array
+ */
+ private function statusColorMap(array $config): array
+ {
+ $map = [];
+ foreach ($config as $group) {
+ $groupColor = StringHelper::normalizeColorHex((string) ($group['color_hex'] ?? ''));
+ if ($groupColor === '') {
+ continue;
+ }
+ $items = is_array($group['items'] ?? null) ? $group['items'] : [];
+ foreach ($items as $item) {
+ $code = strtolower(trim((string) ($item['code'] ?? '')));
+ if ($code !== '') {
+ $map[$code] = $groupColor;
+ }
+ }
+ }
+
+ return $map;
+ }
+
/**
* @param array $statusCodes
* @param array $statusLabelMap
diff --git a/src/Modules/Orders/OrdersRepository.php b/src/Modules/Orders/OrdersRepository.php
index 8580615..b1f1ca5 100644
--- a/src/Modules/Orders/OrdersRepository.php
+++ b/src/Modules/Orders/OrdersRepository.php
@@ -9,7 +9,7 @@ use Throwable;
final class OrdersRepository
{
- private ?bool $supportsMappedMedia = null;
+ private static ?bool $supportsMappedMedia = null;
public function __construct(private readonly PDO $pdo)
{
@@ -158,13 +158,25 @@ final class OrdersRepository
a.city AS buyer_city,
o.external_carrier_id,
o.external_payment_type_id,
- (SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
- (SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
- (SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
- (SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count
+ COALESCE(oi_agg.items_count, 0) AS items_count,
+ COALESCE(oi_agg.items_qty, 0) AS items_qty,
+ COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
+ COALESCE(od_agg.documents_count, 0) AS documents_count
FROM orders o
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
- LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code'
+ LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
+ LEFT JOIN (
+ SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty
+ FROM order_items GROUP BY order_id
+ ) oi_agg ON oi_agg.order_id = o.id
+ LEFT JOIN (
+ SELECT order_id, COUNT(*) AS shipments_count
+ FROM order_shipments GROUP BY order_id
+ ) sh_agg ON sh_agg.order_id = o.id
+ LEFT JOIN (
+ SELECT order_id, COUNT(*) AS documents_count
+ FROM order_documents GROUP BY order_id
+ ) od_agg ON od_agg.order_id = o.id'
. $whereSql
. ' ORDER BY ' . $sortColumn . ' ' . $sortDir
. ' LIMIT :limit OFFSET :offset';
@@ -646,8 +658,8 @@ final class OrdersRepository
private function canResolveMappedMedia(): bool
{
- if ($this->supportsMappedMedia !== null) {
- return $this->supportsMappedMedia;
+ if (self::$supportsMappedMedia !== null) {
+ return self::$supportsMappedMedia;
}
try {
@@ -682,12 +694,12 @@ final class OrdersRepository
$stmt->execute($params);
$count = (int) $stmt->fetchColumn();
- $this->supportsMappedMedia = ($count === count($requiredColumns));
+ self::$supportsMappedMedia = ($count === count($requiredColumns));
} catch (Throwable) {
- $this->supportsMappedMedia = false;
+ self::$supportsMappedMedia = false;
}
- return $this->supportsMappedMedia;
+ return self::$supportsMappedMedia;
}
/**
diff --git a/src/Modules/Settings/AllegroApiClient.php b/src/Modules/Settings/AllegroApiClient.php
index 3315bcd..2bf0ccc 100644
--- a/src/Modules/Settings/AllegroApiClient.php
+++ b/src/Modules/Settings/AllegroApiClient.php
@@ -146,6 +146,44 @@ final class AllegroApiClient
return $this->postJson($url, $accessToken, $body);
}
+ private function getCaBundlePath(): ?string
+ {
+ $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
+ if ($envPath !== '' && is_file($envPath)) {
+ return $envPath;
+ }
+ $iniPath = (string) ini_get('curl.cainfo');
+ if ($iniPath !== '' && is_file($iniPath)) {
+ return $iniPath;
+ }
+ $candidates = [
+ 'C:/xampp/apache/bin/curl-ca-bundle.crt',
+ 'C:/xampp/php/extras/ssl/cacert.pem',
+ '/etc/ssl/certs/ca-certificates.crt',
+ ];
+ foreach ($candidates as $path) {
+ if (is_file($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param array $opts
+ * @return array
+ */
+ private function withSslOptions(array $opts): array
+ {
+ $opts[CURLOPT_SSL_VERIFYPEER] = true;
+ $opts[CURLOPT_SSL_VERIFYHOST] = 2;
+ $caPath = $this->getCaBundlePath();
+ if ($caPath !== null) {
+ $opts[CURLOPT_CAINFO] = $caPath;
+ }
+ return $opts;
+ }
+
private function apiBaseUrl(string $environment): string
{
return trim(strtolower($environment)) === 'production'
@@ -166,7 +204,7 @@ final class AllegroApiClient
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.');
}
- curl_setopt_array($ch, [
+ curl_setopt_array($ch, $this->withSslOptions([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonBody,
@@ -177,7 +215,7 @@ final class AllegroApiClient
'Content-Type: application/vnd.allegro.public.v1+json',
'Authorization: Bearer ' . $accessToken,
],
- ]);
+ ]));
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -230,7 +268,7 @@ final class AllegroApiClient
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.');
}
- curl_setopt_array($ch, [
+ curl_setopt_array($ch, $this->withSslOptions([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonBody,
@@ -241,7 +279,7 @@ final class AllegroApiClient
'Content-Type: application/vnd.allegro.public.v1+json',
'Authorization: Bearer ' . $accessToken,
],
- ]);
+ ]));
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -279,7 +317,7 @@ final class AllegroApiClient
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.');
}
- curl_setopt_array($ch, [
+ curl_setopt_array($ch, $this->withSslOptions([
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_TIMEOUT => 30,
@@ -288,7 +326,7 @@ final class AllegroApiClient
'Accept: application/vnd.allegro.public.v1+json',
'Authorization: Bearer ' . $accessToken,
],
- ]);
+ ]));
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
diff --git a/src/Modules/Settings/AllegroOAuthClient.php b/src/Modules/Settings/AllegroOAuthClient.php
index 19d15c6..0a8398b 100644
--- a/src/Modules/Settings/AllegroOAuthClient.php
+++ b/src/Modules/Settings/AllegroOAuthClient.php
@@ -126,6 +126,29 @@ final class AllegroOAuthClient
return trim(strtolower($environment)) === 'production' ? 'production' : 'sandbox';
}
+ private function getCaBundlePath(): ?string
+ {
+ $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
+ if ($envPath !== '' && is_file($envPath)) {
+ return $envPath;
+ }
+ $iniPath = (string) ini_get('curl.cainfo');
+ if ($iniPath !== '' && is_file($iniPath)) {
+ return $iniPath;
+ }
+ $candidates = [
+ 'C:/xampp/apache/bin/curl-ca-bundle.crt',
+ 'C:/xampp/php/extras/ssl/cacert.pem',
+ '/etc/ssl/certs/ca-certificates.crt',
+ ];
+ foreach ($candidates as $path) {
+ if (is_file($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
/**
* @param array $formData
* @return array
@@ -141,18 +164,25 @@ final class AllegroOAuthClient
throw new AllegroOAuthException('Nie udalo sie zainicjowac polaczenia OAuth z Allegro.');
}
- curl_setopt_array($ch, [
+ $sslOpts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . base64_encode($clientId . ':' . $clientSecret),
],
CURLOPT_POSTFIELDS => http_build_query($formData),
- ]);
+ ];
+ $caPath = $this->getCaBundlePath();
+ if ($caPath !== null) {
+ $sslOpts[CURLOPT_CAINFO] = $caPath;
+ }
+ curl_setopt_array($ch, $sslOpts);
$responseBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
diff --git a/src/Modules/Settings/AllegroStatusSyncService.php b/src/Modules/Settings/AllegroStatusSyncService.php
index d993f38..e623097 100644
--- a/src/Modules/Settings/AllegroStatusSyncService.php
+++ b/src/Modules/Settings/AllegroStatusSyncService.php
@@ -38,7 +38,7 @@ final class AllegroStatusSyncService
if ($direction === self::DIRECTION_ORDERPRO_TO_ALLEGRO) {
return [
- 'ok' => true,
+ 'ok' => false,
'direction' => $direction,
'processed' => 0,
'message' => 'Kierunek orderPRO -> Allegro nie jest jeszcze wdrozony.',
diff --git a/src/Modules/Settings/ApaczkaApiClient.php b/src/Modules/Settings/ApaczkaApiClient.php
index d16574f..7977975 100644
--- a/src/Modules/Settings/ApaczkaApiClient.php
+++ b/src/Modules/Settings/ApaczkaApiClient.php
@@ -9,6 +9,29 @@ final class ApaczkaApiClient
{
private const API_BASE_URL = 'https://www.apaczka.pl/api/v2';
+ private function getCaBundlePath(): ?string
+ {
+ $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
+ if ($envPath !== '' && is_file($envPath)) {
+ return $envPath;
+ }
+ $iniPath = (string) ini_get('curl.cainfo');
+ if ($iniPath !== '' && is_file($iniPath)) {
+ return $iniPath;
+ }
+ $candidates = [
+ 'C:/xampp/apache/bin/curl-ca-bundle.crt',
+ 'C:/xampp/php/extras/ssl/cacert.pem',
+ '/etc/ssl/certs/ca-certificates.crt',
+ ];
+ foreach ($candidates as $path) {
+ if (is_file($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
/**
* @return array>
*/
@@ -180,18 +203,25 @@ final class ApaczkaApiClient
throw new ApaczkaApiException('Nie udalo sie zainicjowac polaczenia z API Apaczka.');
}
- curl_setopt_array($ch, [
+ $sslOpts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($payload),
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
'User-Agent: orderPRO/1.0',
],
- ]);
+ ];
+ $caPath = $this->getCaBundlePath();
+ if ($caPath !== null) {
+ $sslOpts[CURLOPT_CAINFO] = $caPath;
+ }
+ curl_setopt_array($ch, $sslOpts);
$rawBody = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
diff --git a/src/Modules/Settings/ShopproApiClient.php b/src/Modules/Settings/ShopproApiClient.php
index 59bda76..0109498 100644
--- a/src/Modules/Settings/ShopproApiClient.php
+++ b/src/Modules/Settings/ShopproApiClient.php
@@ -5,6 +5,29 @@ namespace App\Modules\Settings;
final class ShopproApiClient
{
+ private function getCaBundlePath(): ?string
+ {
+ $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
+ if ($envPath !== '' && is_file($envPath)) {
+ return $envPath;
+ }
+ $iniPath = (string) ini_get('curl.cainfo');
+ if ($iniPath !== '' && is_file($iniPath)) {
+ return $iniPath;
+ }
+ $candidates = [
+ 'C:/xampp/apache/bin/curl-ca-bundle.crt',
+ 'C:/xampp/php/extras/ssl/cacert.pem',
+ '/etc/ssl/certs/ca-certificates.crt',
+ ];
+ foreach ($candidates as $path) {
+ if (is_file($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
/**
* @return array{ok:bool,http_code:int|null,message:string,items:array>,total:int,page:int,per_page:int}
*/
@@ -193,15 +216,22 @@ final class ShopproApiClient
];
}
- curl_setopt_array($curl, [
+ $sslOpts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => max(1, min(120, $timeoutSeconds)),
CURLOPT_CONNECTTIMEOUT => max(1, min(120, $timeoutSeconds)),
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'X-Api-Key: ' . $apiKey,
],
- ]);
+ ];
+ $caPath = $this->getCaBundlePath();
+ if ($caPath !== null) {
+ $sslOpts[CURLOPT_CAINFO] = $caPath;
+ }
+ curl_setopt_array($curl, $sslOpts);
$body = curl_exec($curl);
$httpCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);