diff --git a/resources/views/settings/project-mappings.php b/resources/views/settings/project-mappings.php
new file mode 100644
index 0000000..cf6f046
--- /dev/null
+++ b/resources/views/settings/project-mappings.php
@@ -0,0 +1,178 @@
+
+
+
+ = $e($t('settings.project_mapping.title')) ?>
+ = $e($t('settings.project_mapping.description')) ?>
+
+
+ = $e((string) $errorMessage) ?>
+
+
+ = $e((string) $successMessage) ?>
+
+
+
+
+ = $e($t('settings.project_mapping.add_title')) ?>
+
+
+
+
+ = $e($t('settings.project_mapping.list_title')) ?>
+
+
+ = $e($t('settings.project_mapping.empty')) ?>
+
+
+
+
+ | = $e($t('settings.project_mapping.fields.pattern')) ?> |
+ = $e($t('settings.project_mapping.fields.script')) ?> |
+ = $e($t('settings.project_mapping.fields.output_dir')) ?> |
+ = $e($t('settings.project_mapping.fields.active')) ?> |
+ = $e($t('settings.project_mapping.fields.actions')) ?> |
+
+
+
+
+
+ | = $e((string) $mapping['product_name_pattern']) ?> |
+ = $e((string) $mapping['script_name']) ?> |
+ = $e((string) ($mapping['output_dir'] ?? '-')) ?> |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
= $e($t('settings.project_mapping.edit_title')) ?>
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php
index 72e29e3..d39ae08 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -72,6 +72,8 @@ use App\Modules\Printing\PrintApiController;
use App\Modules\Printing\PrintApiKeyRepository;
use App\Modules\Printing\PrintJobRepository;
use App\Modules\Settings\PrintSettingsController;
+use App\Modules\Settings\ProjectMappingController;
+use App\Modules\Settings\ProjectMappingRepository;
use App\Modules\Info\InfoController;
use App\Modules\Users\UsersController;
@@ -423,6 +425,7 @@ return static function (Application $app): void {
$router->post('/orders/{id}/send-email', [$ordersController, 'sendEmail'], [$authMiddleware]);
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
$router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
+ $router->get('/api/orders/{id}/preview', [$ordersController, 'preview'], [$authMiddleware]);
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
@@ -524,6 +527,14 @@ return static function (Application $app): void {
$shipmentProviderRegistry
);
$printSettingsController = new PrintSettingsController($template, $translator, $auth, $printApiKeyRepository, $printJobRepository);
+ $projectMappingRepository = new ProjectMappingRepository($app->db());
+ $projectMappingController = new ProjectMappingController(
+ $template,
+ $translator,
+ $auth,
+ $projectMappingRepository,
+ $app->basePath()
+ );
// Print API — session auth (from orderPRO UI)
$router->post('/api/print/jobs', [$printApiController, 'createJob'], [$authMiddleware]);
@@ -539,6 +550,13 @@ return static function (Application $app): void {
$router->post('/settings/printing/keys/{id}/delete', [$printSettingsController, 'deleteKey'], [$authMiddleware]);
$router->post('/settings/printing/jobs/delete', [$printSettingsController, 'deleteJob'], [$authMiddleware]);
+ // Project mappings
+ $router->get('/settings/project-mappings', [$projectMappingController, 'index'], [$authMiddleware]);
+ $router->post('/settings/project-mappings', [$projectMappingController, 'store'], [$authMiddleware]);
+ $router->post('/settings/project-mappings/{id}/update', [$projectMappingController, 'update'], [$authMiddleware]);
+ $router->post('/settings/project-mappings/{id}/delete', [$projectMappingController, 'delete'], [$authMiddleware]);
+ $router->post('/settings/project-mappings/{id}/toggle', [$projectMappingController, 'toggleActive'], [$authMiddleware]);
+
// Shipment presets API
$presetRepository = new ShipmentPresetRepository($app->db());
$presetController = new ShipmentPresetController($presetRepository);
diff --git a/src/Modules/Auth/AuthController.php b/src/Modules/Auth/AuthController.php
index 3e291b6..854e4ff 100644
--- a/src/Modules/Auth/AuthController.php
+++ b/src/Modules/Auth/AuthController.php
@@ -29,6 +29,7 @@ final class AuthController
'title' => $this->translator->get('auth.login.title'),
'errorMessage' => Flash::get('error'),
'oldEmail' => (string) Flash::get('old_email', ''),
+ 'oldRemember' => (bool) Flash::get('old_remember', false),
'csrfToken' => Csrf::token(),
], 'layouts/auth');
@@ -38,9 +39,12 @@ final class AuthController
public function login(Request $request): Response
{
$csrfToken = (string) $request->input('_token', '');
+ $remember = (bool) $request->input('remember', false);
+
if (!Csrf::validate($csrfToken)) {
Flash::set('error', $this->translator->get('auth.errors.csrf_expired'));
Flash::set('old_email', (string) $request->input('email', ''));
+ Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
@@ -50,15 +54,24 @@ final class AuthController
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || $password === '') {
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials_format'));
Flash::set('old_email', $email);
+ Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
if (!$this->auth->attempt($email, $password)) {
Flash::set('error', $this->translator->get('auth.errors.invalid_credentials'));
Flash::set('old_email', $email);
+ Flash::set('old_remember', $remember);
return Response::redirect('/login');
}
+ if ($remember) {
+ $user = $this->auth->user();
+ if ($user !== null) {
+ $this->auth->createRememberToken((int) $user['id']);
+ }
+ }
+
return Response::redirect('/settings/users');
}
diff --git a/src/Modules/Auth/AuthMiddleware.php b/src/Modules/Auth/AuthMiddleware.php
index b976b33..a4f74c2 100644
--- a/src/Modules/Auth/AuthMiddleware.php
+++ b/src/Modules/Auth/AuthMiddleware.php
@@ -14,7 +14,7 @@ final class AuthMiddleware
public function __invoke(Request $request, callable $next): Response
{
- if (!$this->auth->check()) {
+ if (!$this->auth->check() && !$this->auth->loginFromRememberToken()) {
return Response::redirect('/login');
}
diff --git a/src/Modules/Auth/AuthService.php b/src/Modules/Auth/AuthService.php
index 242644b..d636690 100644
--- a/src/Modules/Auth/AuthService.php
+++ b/src/Modules/Auth/AuthService.php
@@ -9,6 +9,8 @@ use App\Modules\Users\UserRepository;
final class AuthService
{
private const SESSION_USER_KEY = 'auth_user';
+ private const REMEMBER_COOKIE = 'remember_token';
+ private const REMEMBER_DAYS = 30;
public function __construct(private readonly UserRepository $users)
{
@@ -57,9 +59,69 @@ final class AuthService
return $user;
}
+ public function createRememberToken(int $userId): void
+ {
+ $token = bin2hex(random_bytes(32));
+ $this->users->updateRememberToken($userId, hash('sha256', $token));
+
+ $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ setcookie(self::REMEMBER_COOKIE, $token, [
+ 'expires' => time() + (self::REMEMBER_DAYS * 86400),
+ 'path' => '/',
+ 'httponly' => true,
+ 'secure' => $secure,
+ 'samesite' => 'Lax',
+ ]);
+ }
+
+ public function loginFromRememberToken(): bool
+ {
+ $cookieToken = $_COOKIE[self::REMEMBER_COOKIE] ?? '';
+ if ($cookieToken === '') {
+ return false;
+ }
+
+ $tokenHash = hash('sha256', $cookieToken);
+ $user = $this->users->findByRememberToken($tokenHash);
+ if ($user === null) {
+ $this->clearRememberCookie();
+ return false;
+ }
+
+ Session::regenerate();
+
+ $_SESSION[self::SESSION_USER_KEY] = [
+ 'id' => (int) $user['id'],
+ 'name' => (string) $user['name'],
+ 'email' => (string) $user['email'],
+ 'login_at' => date(DATE_ATOM),
+ ];
+
+ return true;
+ }
+
public function logout(): void
{
+ $user = $this->user();
+ if ($user !== null) {
+ $this->users->updateRememberToken((int) $user['id'], null);
+ }
+ $this->clearRememberCookie();
+
unset($_SESSION[self::SESSION_USER_KEY]);
Session::regenerate();
}
+
+ private function clearRememberCookie(): void
+ {
+ $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ setcookie(self::REMEMBER_COOKIE, '', [
+ 'expires' => time() - 3600,
+ 'path' => '/',
+ 'httponly' => true,
+ 'secure' => $secure,
+ 'samesite' => 'Lax',
+ ]);
+ unset($_COOKIE[self::REMEMBER_COOKIE]);
+ }
}
diff --git a/src/Modules/Automation/AutomationController.php b/src/Modules/Automation/AutomationController.php
index 2506b77..5c7c63f 100644
--- a/src/Modules/Automation/AutomationController.php
+++ b/src/Modules/Automation/AutomationController.php
@@ -17,7 +17,7 @@ final class AutomationController
{
private const HISTORY_PER_PAGE = 25;
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.created', 'shipment.status_changed', 'payment.status_changed', 'order.status_changed', 'order.status_aged', 'order.imported'];
- private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'order_status', 'days_in_status'];
+ private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status', 'payment_status', 'payment_method', 'order_status', 'days_in_status'];
private const PAYMENT_STATUS_OPTIONS = [
'0' => 'Nieopłacone',
'1' => 'Częściowo opłacone',
@@ -27,6 +27,12 @@ final class AutomationController
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
+ private const PAYMENT_METHOD_OPTIONS = [
+ 'cod' => 'Platnosc przy odbiorze (COD)',
+ 'transfer' => 'Przelew bankowy',
+ 'online' => 'Karta / platnosc online',
+ 'other' => 'Inna',
+ ];
private const SHIPMENT_STATUS_OPTIONS = [
'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']],
'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']],
@@ -251,6 +257,7 @@ final class AutomationController
'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES,
'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS,
'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS,
+ 'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS,
'orderStatusOptions' => $this->repository->listActiveOrderStatuses(),
'errorMessage' => $errorMessage !== '' ? $errorMessage : Flash::get('settings.automation.error', ''),
], 'layouts/app');
@@ -275,6 +282,8 @@ final class AutomationController
$value = ['status_keys' => is_array($cond['shipment_status_keys'] ?? null) ? $cond['shipment_status_keys'] : []];
} elseif ($type === 'payment_status') {
$value = ['status_keys' => is_array($cond['payment_status_keys'] ?? null) ? $cond['payment_status_keys'] : []];
+ } elseif ($type === 'payment_method') {
+ $value = ['method_keys' => is_array($cond['payment_method_keys'] ?? null) ? $cond['payment_method_keys'] : []];
} elseif ($type === 'order_status') {
$value = ['order_status_codes' => is_array($cond['order_status_codes'] ?? null) ? $cond['order_status_codes'] : []];
} elseif ($type === 'days_in_status') {
@@ -441,6 +450,21 @@ final class AutomationController
return count($statusKeys) > 0 ? ['status_keys' => array_values(array_unique($statusKeys))] : null;
}
+ if ($type === 'payment_method') {
+ $keys = $condition['payment_method_keys'] ?? [];
+ if (!is_array($keys)) {
+ $keys = [];
+ }
+
+ $allowedKeys = array_keys(self::PAYMENT_METHOD_OPTIONS);
+ $methodKeys = array_values(array_filter(
+ array_map(static fn (mixed $key): string => trim((string) $key), $keys),
+ static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true)
+ ));
+
+ return count($methodKeys) > 0 ? ['method_keys' => array_values(array_unique($methodKeys))] : null;
+ }
+
if ($type === 'order_status') {
$codes = $condition['order_status_codes'] ?? [];
if (!is_array($codes)) {
diff --git a/src/Modules/Automation/AutomationService.php b/src/Modules/Automation/AutomationService.php
index cd90d72..a2fafa5 100644
--- a/src/Modules/Automation/AutomationService.php
+++ b/src/Modules/Automation/AutomationService.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace App\Modules\Automation;
+use App\Core\Support\StringHelper;
use App\Modules\Accounting\ReceiptIssueException;
use App\Modules\Accounting\ReceiptRepository;
use App\Modules\Accounting\ReceiptService;
@@ -134,6 +135,9 @@ final class AutomationService
if ($type === 'payment_status') {
return $this->evaluatePaymentStatusCondition($value, $context);
}
+ if ($type === 'payment_method') {
+ return $this->evaluatePaymentMethodCondition($value, $order);
+ }
if ($type === 'order_status') {
return $this->evaluateOrderStatusCondition($value, $context);
}
@@ -216,6 +220,57 @@ final class AutomationService
return in_array($newPaymentStatus, array_map(static fn (mixed $k): string => trim((string) $k), $statusKeys), true);
}
+ /**
+ * @param array
$value
+ * @param array $order
+ */
+ private function evaluatePaymentMethodCondition(array $value, array $order): bool
+ {
+ $methodKeys = is_array($value['method_keys'] ?? null) ? $value['method_keys'] : [];
+ if ($methodKeys === []) {
+ return false;
+ }
+
+ $paymentType = trim((string) ($order['external_payment_type_id'] ?? ''));
+ if ($paymentType === '') {
+ return false;
+ }
+
+ $upperType = strtoupper($paymentType);
+
+ foreach ($methodKeys as $key) {
+ $match = match ((string) $key) {
+ 'cod' => StringHelper::isCodPayment($paymentType),
+ 'transfer' => str_contains($upperType, 'PRZELEW')
+ || str_contains($upperType, 'TRANSFER')
+ || str_contains($upperType, 'WIRE'),
+ 'online' => str_contains($upperType, 'CARD')
+ || str_contains($upperType, 'ONLINE')
+ || str_contains($upperType, 'PAYU')
+ || str_contains($upperType, 'PRZELEWY24')
+ || str_contains($upperType, 'BLIK')
+ || str_contains($upperType, 'TPAY'),
+ 'other' => !StringHelper::isCodPayment($paymentType)
+ && !str_contains($upperType, 'PRZELEW')
+ && !str_contains($upperType, 'TRANSFER')
+ && !str_contains($upperType, 'WIRE')
+ && !str_contains($upperType, 'CARD')
+ && !str_contains($upperType, 'ONLINE')
+ && !str_contains($upperType, 'PAYU')
+ && !str_contains($upperType, 'PRZELEWY24')
+ && !str_contains($upperType, 'BLIK')
+ && !str_contains($upperType, 'TPAY'),
+ default => false,
+ };
+
+ if ($match) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* @param array $value
* @param array $context
@@ -491,7 +546,7 @@ final class AutomationService
$details = $this->orders->findDetails($orderId);
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
- $oldStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
+ $oldStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
$actorName = 'Automatyzacja: ' . $ruleName;
$updated = $this->orders->updateOrderStatus($orderId, $statusCode, 'system', $actorName);
diff --git a/src/Modules/Automation/OrderStatusAgedService.php b/src/Modules/Automation/OrderStatusAgedService.php
index 6a556ce..be14618 100644
--- a/src/Modules/Automation/OrderStatusAgedService.php
+++ b/src/Modules/Automation/OrderStatusAgedService.php
@@ -60,7 +60,7 @@ final class OrderStatusAgedService
}
try {
- $currentStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
+ $currentStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
$lastChanged = (string) ($order['last_changed'] ?? '');
$actualDays = $lastChanged !== '' ? $this->daysSince($lastChanged) : $days;
@@ -129,12 +129,12 @@ final class OrderStatusAgedService
$placeholders = implode(', ', array_fill(0, count($statusCodes), '?'));
- $sql = "SELECT o.id, o.external_status_id, MAX(h.changed_at) AS last_changed
+ $sql = "SELECT o.id, o.status_code, MAX(h.changed_at) AS last_changed
FROM orders o
INNER JOIN order_status_history h ON h.order_id = o.id
- AND LOWER(h.to_status_id) = LOWER(o.external_status_id)
- WHERE LOWER(COALESCE(o.external_status_id, '')) IN ({$placeholders})
- GROUP BY o.id, o.external_status_id
+ AND LOWER(h.to_status_id) = LOWER(o.status_code)
+ WHERE LOWER(COALESCE(o.status_code, '')) IN ({$placeholders})
+ GROUP BY o.id, o.status_code
HAVING MAX(h.changed_at) <= DATE_SUB(NOW(), INTERVAL ? DAY)
LIMIT " . self::MAX_ORDERS_PER_RULE;
diff --git a/src/Modules/Orders/OrderImportRepository.php b/src/Modules/Orders/OrderImportRepository.php
index 96d57c9..62c2e68 100644
--- a/src/Modules/Orders/OrderImportRepository.php
+++ b/src/Modules/Orders/OrderImportRepository.php
@@ -45,7 +45,7 @@ final class OrderImportRepository
$newPaymentStatus = (int) ($orderData['payment_status'] ?? 0);
$paymentTransition = $currentStatus === 'nieoplacone' && $newPaymentStatus === 2;
if (!$paymentTransition) {
- $orderData['external_status_id'] = $currentStatus;
+ $orderData['status_code'] = $currentStatus;
}
}
@@ -104,7 +104,7 @@ final class OrderImportRepository
private function getCurrentStatus(int $orderId): string
{
$statement = $this->pdo->prepare(
- 'SELECT external_status_id FROM orders WHERE id = :id LIMIT 1'
+ 'SELECT status_code FROM orders WHERE id = :id LIMIT 1'
);
$statement->execute(['id' => $orderId]);
$value = $statement->fetchColumn();
@@ -120,13 +120,13 @@ final class OrderImportRepository
$statement = $this->pdo->prepare(
'INSERT INTO orders (
integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id,
- external_status_id, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
+ status_code, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency,
total_without_tax, total_with_tax, total_paid, delivery_price, send_date_min, send_date_max, ordered_at,
source_created_at, source_updated_at, preferences_json, payload_json, fetched_at
) VALUES (
:integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id,
- :external_status_id, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
+ :status_code, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
:customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency,
:total_without_tax, :total_with_tax, :total_paid, :delivery_price, :send_date_min, :send_date_max, :ordered_at,
:source_created_at, :source_updated_at, :preferences_json, :payload_json, :fetched_at
@@ -155,7 +155,7 @@ final class OrderImportRepository
external_order_id = :external_order_id,
external_platform_id = :external_platform_id,
external_platform_account_id = :external_platform_account_id,
- external_status_id = :external_status_id,
+ status_code = :status_code,
external_payment_type_id = :external_payment_type_id,
payment_status = :payment_status,
external_carrier_id = :external_carrier_id,
@@ -201,7 +201,7 @@ final class OrderImportRepository
'external_order_id' => $orderData['external_order_id'] ?? null,
'external_platform_id' => $orderData['external_platform_id'] ?? null,
'external_platform_account_id' => $orderData['external_platform_account_id'] ?? null,
- 'external_status_id' => $orderData['external_status_id'] ?? null,
+ 'status_code' => $orderData['status_code'] ?? null,
'external_payment_type_id' => $orderData['external_payment_type_id'] ?? null,
'payment_status' => $orderData['payment_status'] ?? null,
'external_carrier_id' => $orderData['external_carrier_id'] ?? null,
diff --git a/src/Modules/Orders/OrdersController.php b/src/Modules/Orders/OrdersController.php
index 29c28fb..06176c5 100644
--- a/src/Modules/Orders/OrdersController.php
+++ b/src/Modules/Orders/OrdersController.php
@@ -71,81 +71,98 @@ final class OrdersController
$tableRows = array_map(fn (array $row): array => $this->toTableRow($row, $statusLabelMap, $statusColorMap), (array) ($result['items'] ?? []));
+ $tableListData = [
+ 'list_key' => 'orders',
+ 'base_path' => '/orders/list',
+ 'query' => $filters,
+ 'filters' => [
+ [
+ 'key' => 'search',
+ 'label' => $this->translator->get('orders.filters.search'),
+ 'type' => 'text',
+ 'value' => $filters['search'],
+ ],
+ [
+ 'key' => 'source',
+ 'label' => $this->translator->get('orders.filters.source'),
+ 'type' => 'select',
+ 'value' => $filters['source'],
+ 'options' => ['' => $this->translator->get('orders.filters.any')] + $sourceOptions,
+ ],
+ [
+ 'key' => 'status',
+ 'label' => $this->translator->get('orders.filters.status'),
+ 'type' => 'select',
+ 'value' => $filters['status'],
+ 'options' => ['' => $this->translator->get('orders.filters.any')] + $statusOptions,
+ ],
+ [
+ 'key' => 'payment_status',
+ 'label' => $this->translator->get('orders.filters.payment_status'),
+ 'type' => 'select',
+ 'value' => $filters['payment_status'],
+ 'options' => $this->paymentStatusFilterOptions(),
+ ],
+ [
+ 'key' => 'date_from',
+ 'label' => $this->translator->get('orders.filters.date_from'),
+ 'type' => 'date',
+ 'value' => $filters['date_from'],
+ ],
+ [
+ 'key' => 'date_to',
+ 'label' => $this->translator->get('orders.filters.date_to'),
+ 'type' => 'date',
+ 'value' => $filters['date_to'],
+ ],
+ ],
+ 'columns' => [
+ ['key' => 'order_ref', 'label' => $this->translator->get('orders.fields.order_ref'), 'sortable' => true, 'sort_key' => 'source_order_id', 'raw' => true],
+ ['key' => 'buyer', 'label' => $this->translator->get('orders.fields.buyer'), 'raw' => true],
+ ['key' => 'status_badges', 'label' => $this->translator->get('orders.fields.status'), 'sortable' => true, 'sort_key' => 'status_code', 'raw' => true],
+ ['key' => 'products', 'label' => $this->translator->get('orders.fields.products'), 'raw' => true],
+ ['key' => 'totals', 'label' => $this->translator->get('orders.fields.totals'), 'sortable' => true, 'sort_key' => 'total_with_tax', 'raw' => true],
+ ['key' => 'shipping', 'label' => $this->translator->get('orders.fields.shipping'), 'raw' => true],
+ ['key' => 'ordered_at', 'label' => $this->translator->get('orders.fields.ordered_at'), 'sortable' => true, 'sort_key' => 'ordered_at'],
+ ],
+ 'rows' => $tableRows,
+ 'pagination' => [
+ 'page' => (int) ($result['page'] ?? 1),
+ 'total_pages' => $totalPages,
+ 'total' => (int) ($result['total'] ?? 0),
+ 'per_page' => (int) ($result['per_page'] ?? 20),
+ ],
+ 'per_page_options' => [20, 50, 100],
+ 'selectable' => true,
+ 'select_name' => 'selected_ids[]',
+ 'select_value_key' => 'id',
+ 'header_actions' => [],
+ 'empty_message' => $this->translator->get('orders.empty'),
+ 'show_actions' => false,
+ ];
+
+ if ($request->header('X-Requested-With') === 'XMLHttpRequest') {
+ $tableHtml = $this->template->render('components/table-list', [
+ 'tableList' => $tableListData,
+ ]);
+ $panelHtml = $this->template->render('components/order-status-panel', [
+ 'statusPanelList' => $statusPanel,
+ 'statusPanelTitle' => 'Statusy',
+ ]);
+
+ return Response::json([
+ 'tableHtml' => $tableHtml,
+ 'panelHtml' => $panelHtml,
+ ]);
+ }
+
$html = $this->template->render('orders/list', [
'title' => $this->translator->get('orders.title'),
'activeMenu' => 'orders',
'activeOrders' => 'list',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
- 'tableList' => [
- 'list_key' => 'orders',
- 'base_path' => '/orders/list',
- 'query' => $filters,
- 'filters' => [
- [
- 'key' => 'search',
- 'label' => $this->translator->get('orders.filters.search'),
- 'type' => 'text',
- 'value' => $filters['search'],
- ],
- [
- 'key' => 'source',
- 'label' => $this->translator->get('orders.filters.source'),
- 'type' => 'select',
- 'value' => $filters['source'],
- 'options' => ['' => $this->translator->get('orders.filters.any')] + $sourceOptions,
- ],
- [
- 'key' => 'status',
- 'label' => $this->translator->get('orders.filters.status'),
- 'type' => 'select',
- 'value' => $filters['status'],
- 'options' => ['' => $this->translator->get('orders.filters.any')] + $statusOptions,
- ],
- [
- 'key' => 'payment_status',
- 'label' => $this->translator->get('orders.filters.payment_status'),
- 'type' => 'select',
- 'value' => $filters['payment_status'],
- 'options' => $this->paymentStatusFilterOptions(),
- ],
- [
- 'key' => 'date_from',
- 'label' => $this->translator->get('orders.filters.date_from'),
- 'type' => 'date',
- 'value' => $filters['date_from'],
- ],
- [
- 'key' => 'date_to',
- 'label' => $this->translator->get('orders.filters.date_to'),
- 'type' => 'date',
- 'value' => $filters['date_to'],
- ],
- ],
- 'columns' => [
- ['key' => 'order_ref', 'label' => $this->translator->get('orders.fields.order_ref'), 'sortable' => true, 'sort_key' => 'source_order_id', 'raw' => true],
- ['key' => 'buyer', 'label' => $this->translator->get('orders.fields.buyer'), 'raw' => true],
- ['key' => 'status_badges', 'label' => $this->translator->get('orders.fields.status'), 'sortable' => true, 'sort_key' => 'external_status_id', 'raw' => true],
- ['key' => 'products', 'label' => $this->translator->get('orders.fields.products'), 'raw' => true],
- ['key' => 'totals', 'label' => $this->translator->get('orders.fields.totals'), 'sortable' => true, 'sort_key' => 'total_with_tax', 'raw' => true],
- ['key' => 'shipping', 'label' => $this->translator->get('orders.fields.shipping'), 'raw' => true],
- ['key' => 'ordered_at', 'label' => $this->translator->get('orders.fields.ordered_at'), 'sortable' => true, 'sort_key' => 'ordered_at'],
- ],
- 'rows' => $tableRows,
- 'pagination' => [
- 'page' => (int) ($result['page'] ?? 1),
- 'total_pages' => $totalPages,
- 'total' => (int) ($result['total'] ?? 0),
- 'per_page' => (int) ($result['per_page'] ?? 20),
- ],
- 'per_page_options' => [20, 50, 100],
- 'selectable' => true,
- 'select_name' => 'selected_ids[]',
- 'select_value_key' => 'id',
- 'header_actions' => [],
- 'empty_message' => $this->translator->get('orders.empty'),
- 'show_actions' => false,
- ],
+ 'tableList' => $tableListData,
'stats' => $stats,
'statusPanel' => $statusPanel,
'allStatuses' => $this->buildAllStatusOptions($statusConfig),
@@ -173,7 +190,7 @@ final class OrdersController
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
- $statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['external_status_id'] ?? ''));
+ $statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['status_code'] ?? ''));
$statusCounts = $this->orders->statusCounts();
$statusConfig = $this->orders->statusPanelConfig();
$statusLabelMap = $this->statusLabelMap($statusConfig);
@@ -280,7 +297,7 @@ final class OrdersController
$oldDetails = $this->orders->findDetails($orderId);
$oldOrder = is_array($oldDetails['order'] ?? null) ? $oldDetails['order'] : [];
- $oldStatus = strtolower(trim((string) ($oldOrder['external_status_id'] ?? '')));
+ $oldStatus = strtolower(trim((string) ($oldOrder['status_code'] ?? '')));
$success = $this->orders->updateOrderStatus($orderId, $newStatus, 'user', $actorName !== '' ? $actorName : null);
@@ -336,7 +353,7 @@ final class OrdersController
$buyerName = trim((string) ($row['buyer_name'] ?? ''));
$buyerEmail = trim((string) ($row['buyer_email'] ?? ''));
$buyerCity = trim((string) ($row['buyer_city'] ?? ''));
- $status = trim((string) (($row['effective_status_id'] ?? '') !== '' ? $row['effective_status_id'] : ($row['external_status_id'] ?? '')));
+ $status = trim((string) (($row['effective_status_id'] ?? '') !== '' ? $row['effective_status_id'] : ($row['status_code'] ?? '')));
$currency = trim((string) ($row['currency'] ?? ''));
$totalWithTax = $row['total_with_tax'] !== null ? number_format((float) $row['total_with_tax'], 2, '.', ' ') : '-';
$totalPaid = $row['total_paid'] !== null ? number_format((float) $row['total_paid'], 2, '.', ' ') : '-';
@@ -349,11 +366,17 @@ final class OrdersController
$shipments = max(0, (int) ($row['shipments_count'] ?? 0));
$documents = max(0, (int) ($row['documents_count'] ?? 0));
$itemsPreview = is_array($row['items_preview'] ?? null) ? $row['items_preview'] : [];
+ $projectsDone = max(0, (int) ($row['projects_done'] ?? 0));
+ $projectsTotal = max(0, (int) ($row['projects_total'] ?? 0));
+
+ $previewBtn = '';
return [
'id' => (int) ($row['id'] ?? 0),
'order_ref' => '