update
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<string, mixed> $value
|
||||
* @param array<string, mixed> $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<string, mixed> $value
|
||||
* @param array<string, mixed> $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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = '<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">'
|
||||
. '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
||||
. '</button>';
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'order_ref' => '<div class="orders-ref">'
|
||||
. '<div class="orders-ref__main"><a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. '<div class="orders-ref__main">' . $previewBtn . '<a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||
. '</a></div>'
|
||||
. '<div class="orders-ref__meta">'
|
||||
@@ -371,7 +394,7 @@ final class OrdersController
|
||||
'status_badges' => '<div class="orders-status-wrap" data-order-id="' . (int) ($row['id'] ?? 0) . '" data-current-status="' . htmlspecialchars($status, ENT_QUOTES, 'UTF-8') . '">'
|
||||
. $this->statusBadge($status, $this->statusLabel($status, $statusLabelMap), $statusColorMap[strtolower(trim($status))] ?? '')
|
||||
. '</div>',
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty),
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty, $projectsDone, $projectsTotal),
|
||||
'totals' => '<div class="orders-money">'
|
||||
. '<div class="orders-money__main">' . htmlspecialchars($totalWithTax . ' ' . $currency, ENT_QUOTES, 'UTF-8') . ($isUnpaid ? ' <span class="order-tag is-unpaid">Nieopłacone</span>' : '') . '</div>'
|
||||
. '<div class="orders-money__meta">' . ($isCod ? '<span class="order-tag is-cod">Za pobraniem</span>' : 'oplacono: ' . htmlspecialchars($totalPaid . ' ' . $currency, ENT_QUOTES, 'UTF-8')) . '</div>'
|
||||
@@ -671,7 +694,7 @@ final class OrdersController
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $itemsPreview
|
||||
*/
|
||||
private function productsHtml(array $itemsPreview, int $itemsCount, string $itemsQty): string
|
||||
private function productsHtml(array $itemsPreview, int $itemsCount, string $itemsQty, int $projectsDone = 0, int $projectsTotal = 0): string
|
||||
{
|
||||
if ($itemsPreview === []) {
|
||||
return '<div class="orders-products">'
|
||||
@@ -704,12 +727,37 @@ final class OrdersController
|
||||
if ($itemsCount > count($itemsPreview)) {
|
||||
$html .= '<div class="orders-products__more">+' . ($itemsCount - count($itemsPreview)) . ' pozycji</div>';
|
||||
}
|
||||
$html .= '<div class="orders-products__meta">' . $itemsCount . ' pozycji / ' . htmlspecialchars($itemsQty, ENT_QUOTES, 'UTF-8') . ' szt.</div>';
|
||||
$html .= '<div class="orders-products__meta">' . $itemsCount . ' pozycji / ' . htmlspecialchars($itemsQty, ENT_QUOTES, 'UTF-8') . ' szt.'
|
||||
. $this->projectBadge($projectsDone, $projectsTotal)
|
||||
. '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function projectBadge(int $done, int $total): string
|
||||
{
|
||||
if ($total === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($done === $total) {
|
||||
return ' <span class="project-badge project-badge--done" title="Wszystkie projekty wygenerowane (' . $done . '/' . $total . ')">'
|
||||
. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
if ($done > 0) {
|
||||
return ' <span class="project-badge project-badge--partial" title="Projekty: ' . $done . '/' . $total . '">'
|
||||
. $done . '/' . $total
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
return ' <span class="project-badge project-badge--none" title="Brak wygenerowanych projektow (0/' . $total . ')">'
|
||||
. '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
private function shippingHtml(string $deliveryMethod, int $shipments, int $documents): string
|
||||
{
|
||||
$deliveryMethod = trim(html_entity_decode(strip_tags($deliveryMethod), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
@@ -961,4 +1009,35 @@ final class OrdersController
|
||||
return Response::json(['results' => $results]);
|
||||
}
|
||||
|
||||
public function preview(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
if ($details === null) {
|
||||
return Response::html('<div class="order-preview-error">Zamowienie nie znalezione.</div>', 404);
|
||||
}
|
||||
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||
|
||||
$addressByType = ['customer' => null, 'delivery' => null, 'invoice' => null];
|
||||
foreach ($addresses as $address) {
|
||||
$type = (string) ($address['address_type'] ?? '');
|
||||
if ($type !== '' && array_key_exists($type, $addressByType) && $addressByType[$type] === null) {
|
||||
$addressByType[$type] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
$html = $this->template->render('orders/partials/preview-content', [
|
||||
'order' => $order,
|
||||
'items' => $items,
|
||||
'addressByType' => $addressByType,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class OrdersRepository
|
||||
$sortColumn = match ($sort) {
|
||||
'source_order_id' => 'o.source_order_id',
|
||||
'external_order_id' => 'o.external_order_id',
|
||||
'external_status_id' => 'o.external_status_id',
|
||||
'status_code' => 'o.status_code',
|
||||
'payment_status' => 'o.payment_status',
|
||||
'total_with_tax' => 'o.total_with_tax',
|
||||
'total_paid' => 'o.total_paid',
|
||||
@@ -48,7 +48,7 @@ final class OrdersRepository
|
||||
try {
|
||||
$countSql = 'SELECT 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.status_code) = asm.allegro_status_code'
|
||||
. $whereSql;
|
||||
$countStmt = $this->pdo->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
@@ -158,7 +158,7 @@ final class OrdersRepository
|
||||
o.source,
|
||||
o.source_order_id,
|
||||
o.external_order_id,
|
||||
o.external_status_id,
|
||||
o.status_code,
|
||||
' . $effectiveStatusSql . ' AS effective_status_id,
|
||||
o.payment_status,
|
||||
o.currency,
|
||||
@@ -178,15 +178,19 @@ final class OrdersRepository
|
||||
o.external_payment_type_id,
|
||||
COALESCE(oi_agg.items_count, 0) AS items_count,
|
||||
COALESCE(oi_agg.items_qty, 0) AS items_qty,
|
||||
COALESCE(oi_agg.projects_done, 0) AS projects_done,
|
||||
COALESCE(oi_agg.projects_total, 0) AS projects_total,
|
||||
COALESCE(sh_agg.shipments_count, 0) AS shipments_count,
|
||||
COALESCE(od_agg.documents_count, 0) AS documents_count,
|
||||
ig.name AS integration_name
|
||||
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.status_code) = asm.allegro_status_code
|
||||
LEFT JOIN integrations ig ON ig.id = o.integration_id
|
||||
LEFT JOIN (
|
||||
SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty
|
||||
SELECT order_id, COUNT(*) AS items_count, COALESCE(SUM(quantity), 0) AS items_qty,
|
||||
SUM(CASE WHEN project_generated = 1 THEN 1 ELSE 0 END) AS projects_done,
|
||||
COUNT(*) AS projects_total
|
||||
FROM order_items GROUP BY order_id
|
||||
) oi_agg ON oi_agg.order_id = o.id
|
||||
LEFT JOIN (
|
||||
@@ -217,7 +221,7 @@ final class OrdersRepository
|
||||
'source' => (string) ($row['source'] ?? ''),
|
||||
'source_order_id' => (string) ($row['source_order_id'] ?? ''),
|
||||
'external_order_id' => (string) ($row['external_order_id'] ?? ''),
|
||||
'external_status_id' => (string) ($row['external_status_id'] ?? ''),
|
||||
'status_code' => (string) ($row['status_code'] ?? ''),
|
||||
'effective_status_id' => (string) ($row['effective_status_id'] ?? ''),
|
||||
'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null,
|
||||
'currency' => (string) ($row['currency'] ?? ''),
|
||||
@@ -240,6 +244,8 @@ final class OrdersRepository
|
||||
'documents_count' => (int) ($row['documents_count'] ?? 0),
|
||||
'integration_name' => (string) ($row['integration_name'] ?? ''),
|
||||
'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []),
|
||||
'projects_done' => (int) ($row['projects_done'] ?? 0),
|
||||
'projects_total' => (int) ($row['projects_total'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -253,7 +259,7 @@ final class OrdersRepository
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT DISTINCT ' . $effectiveStatusSql . ' AS effective_status_id
|
||||
FROM orders o
|
||||
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.status_code) = asm.allegro_status_code
|
||||
WHERE ' . $effectiveStatusSql . ' IS NOT NULL
|
||||
AND ' . $effectiveStatusSql . ' <> ""
|
||||
ORDER BY effective_status_id ASC'
|
||||
@@ -317,7 +323,7 @@ final class OrdersRepository
|
||||
SUM(CASE WHEN payment_status = 2 THEN 1 ELSE 0 END) AS paid_count,
|
||||
SUM(CASE WHEN ' . $effectiveStatusSql . ' IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.status_code) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
return [
|
||||
'all' => 0,
|
||||
@@ -351,7 +357,7 @@ final class OrdersRepository
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT ' . $effectiveStatusSql . ' AS effective_status_id, COUNT(*) AS cnt
|
||||
FROM orders o
|
||||
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.status_code) = asm.allegro_status_code
|
||||
GROUP BY effective_status_id'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
@@ -475,7 +481,7 @@ final class OrdersRepository
|
||||
'SELECT o.*, ' . $effectiveStatusSql . ' AS effective_status_id,
|
||||
ig.name AS integration_name
|
||||
FROM orders o
|
||||
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.status_code) = asm.allegro_status_code
|
||||
LEFT JOIN integrations ig ON ig.id = o.integration_id
|
||||
WHERE o.id = :id
|
||||
LIMIT 1'
|
||||
@@ -670,7 +676,7 @@ final class OrdersRepository
|
||||
AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
|
||||
AND ' . $mappingAlias . '.orderpro_status_code <> ""
|
||||
THEN ' . $mappingAlias . '.orderpro_status_code
|
||||
ELSE ' . $orderAlias . '.external_status_id
|
||||
ELSE ' . $orderAlias . '.status_code
|
||||
END';
|
||||
}
|
||||
|
||||
@@ -981,16 +987,16 @@ final class OrdersRepository
|
||||
public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT external_status_id FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt = $this->pdo->prepare('SELECT status_code FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $orderId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!is_array($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldStatus = trim((string) ($row['external_status_id'] ?? ''));
|
||||
$oldStatus = trim((string) ($row['status_code'] ?? ''));
|
||||
|
||||
$update = $this->pdo->prepare('UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id');
|
||||
$update = $this->pdo->prepare('UPDATE orders SET status_code = :status, updated_at = NOW() WHERE id = :id');
|
||||
$update->execute(['status' => $newStatusCode, 'id' => $orderId]);
|
||||
|
||||
$this->recordStatusChange(
|
||||
|
||||
@@ -209,7 +209,7 @@ final class AllegroOrderImportService
|
||||
'external_order_id' => $checkoutFormId,
|
||||
'external_platform_id' => trim((string) ($payload['marketplace']['id'] ?? 'allegro-pl')),
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $externalStatus,
|
||||
'status_code' => $externalStatus,
|
||||
'external_payment_type_id' => trim((string) ($payment['type'] ?? '')),
|
||||
'payment_status' => $mappedPaymentStatus,
|
||||
'external_carrier_id' => $deliveryForm !== '' ? $deliveryForm : null,
|
||||
|
||||
@@ -241,10 +241,10 @@ final class AllegroStatusSyncService
|
||||
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, source_order_id, external_status_id
|
||||
'SELECT id, source_order_id, status_code
|
||||
FROM orders
|
||||
WHERE source = ?
|
||||
AND LOWER(COALESCE(external_status_id, "")) NOT IN (' . $placeholders . ')
|
||||
AND LOWER(COALESCE(status_code, "")) NOT IN (' . $placeholders . ')
|
||||
AND (last_status_checked_at IS NULL OR source_updated_at > last_status_checked_at)
|
||||
ORDER BY source_updated_at DESC
|
||||
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||
@@ -273,7 +273,7 @@ final class AllegroStatusSyncService
|
||||
'SELECT
|
||||
o.id,
|
||||
o.source_order_id,
|
||||
o.external_status_id AS orderpro_status_code,
|
||||
o.status_code AS orderpro_status_code,
|
||||
MAX(h.changed_at) AS latest_change
|
||||
FROM order_status_history h
|
||||
INNER JOIN orders o ON o.id = h.order_id
|
||||
@@ -281,7 +281,7 @@ final class AllegroStatusSyncService
|
||||
AND o.integration_id = :integration_id
|
||||
AND h.change_source = :change_source
|
||||
AND h.changed_at > :since_date
|
||||
GROUP BY o.id, o.source_order_id, o.external_status_id
|
||||
GROUP BY o.id, o.source_order_id, o.status_code
|
||||
ORDER BY latest_change ASC
|
||||
LIMIT ' . self::MAX_ORDERS_PER_RUN
|
||||
);
|
||||
|
||||
155
src/Modules/Settings/ProjectMappingController.php
Normal file
155
src/Modules/Settings/ProjectMappingController.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Modules\Auth\AuthService;
|
||||
|
||||
final class ProjectMappingController
|
||||
{
|
||||
private const SCRIPTS_DIR = 'tools/generowanie';
|
||||
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ProjectMappingRepository $repository,
|
||||
private readonly string $basePath
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$mappings = $this->repository->getAll();
|
||||
$scripts = $this->scanScripts();
|
||||
|
||||
$html = $this->template->render('settings/project-mappings', [
|
||||
'title' => $this->translator->get('settings.project_mapping.title'),
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'project-mappings',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'mappings' => $mappings,
|
||||
'scripts' => $scripts,
|
||||
'errorMessage' => (string) Flash::get('settings_error', ''),
|
||||
'successMessage' => (string) Flash::get('settings_success', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$pattern = trim((string) $request->input('product_name_pattern', ''));
|
||||
$scriptName = trim((string) $request->input('script_name', ''));
|
||||
$outputDir = trim((string) $request->input('output_dir', ''));
|
||||
|
||||
if ($pattern === '' || $scriptName === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.validation_error'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$scriptPath = $this->basePath . '/' . self::SCRIPTS_DIR . '/' . $scriptName;
|
||||
if (!file_exists($scriptPath)) {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.script_not_found'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$this->repository->create([
|
||||
'product_name_pattern' => $pattern,
|
||||
'script_name' => $scriptName,
|
||||
'output_dir' => $outputDir !== '' ? $outputDir : null,
|
||||
]);
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.created'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
$pattern = trim((string) $request->input('product_name_pattern', ''));
|
||||
$scriptName = trim((string) $request->input('script_name', ''));
|
||||
$outputDir = trim((string) $request->input('output_dir', ''));
|
||||
|
||||
if ($id <= 0 || $pattern === '' || $scriptName === '') {
|
||||
Flash::set('settings_error', $this->translator->get('settings.project_mapping.flash.validation_error'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$this->repository->update($id, [
|
||||
'product_name_pattern' => $pattern,
|
||||
'script_name' => $scriptName,
|
||||
'output_dir' => $outputDir !== '' ? $outputDir : null,
|
||||
]);
|
||||
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.updated'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
if ($id > 0) {
|
||||
$this->repository->delete($id);
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.deleted'));
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
public function toggleActive(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
$id = (int) $request->param('id', '0');
|
||||
if ($id > 0) {
|
||||
$this->repository->toggleActive($id);
|
||||
Flash::set('settings_success', $this->translator->get('settings.project_mapping.flash.toggled'));
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/project-mappings');
|
||||
}
|
||||
|
||||
private function scanScripts(): array
|
||||
{
|
||||
$dir = $this->basePath . '/' . self::SCRIPTS_DIR;
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = glob($dir . '/*.py');
|
||||
$scripts = [];
|
||||
foreach ($files as $file) {
|
||||
$scripts[] = basename($file);
|
||||
}
|
||||
sort($scripts);
|
||||
|
||||
return $scripts;
|
||||
}
|
||||
}
|
||||
88
src/Modules/Settings/ProjectMappingRepository.php
Normal file
88
src/Modules/Settings/ProjectMappingRepository.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class ProjectMappingRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT * FROM project_mappings ORDER BY id DESC'
|
||||
);
|
||||
$statement->execute();
|
||||
|
||||
return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
}
|
||||
|
||||
public function getById(int $id): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT * FROM project_mappings WHERE id = :id LIMIT 1'
|
||||
);
|
||||
$statement->execute([':id' => $id]);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO project_mappings (product_name_pattern, script_name, output_dir, is_active)
|
||||
VALUES (:product_name_pattern, :script_name, :output_dir, :is_active)'
|
||||
);
|
||||
$statement->execute([
|
||||
':product_name_pattern' => $data['product_name_pattern'],
|
||||
':script_name' => $data['script_name'],
|
||||
':output_dir' => $data['output_dir'] ?? null,
|
||||
':is_active' => (int) ($data['is_active'] ?? 1),
|
||||
]);
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE project_mappings
|
||||
SET product_name_pattern = :product_name_pattern,
|
||||
script_name = :script_name,
|
||||
output_dir = :output_dir
|
||||
WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([
|
||||
':product_name_pattern' => $data['product_name_pattern'],
|
||||
':script_name' => $data['script_name'],
|
||||
':output_dir' => $data['output_dir'] ?? null,
|
||||
':id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'DELETE FROM project_mappings WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([':id' => $id]);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE project_mappings SET is_active = 1 - is_active WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute([':id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ final class ShopproOrderMapper
|
||||
'external_order_id' => $sourceOrderId,
|
||||
'external_platform_id' => IntegrationSources::SHOPPRO,
|
||||
'external_platform_account_id' => null,
|
||||
'external_status_id' => $effectiveStatus,
|
||||
'status_code' => $effectiveStatus,
|
||||
'external_payment_type_id' => $this->normalizeCodPaymentType((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
||||
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
||||
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
||||
|
||||
@@ -164,9 +164,9 @@ final class ShopproPaymentStatusSyncService
|
||||
}
|
||||
|
||||
if ($watchedStatuses !== []) {
|
||||
$where[] = 'LOWER(COALESCE(external_status_id, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
$where[] = 'LOWER(COALESCE(status_code, "")) IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
} else {
|
||||
$where[] = 'LOWER(COALESCE(external_status_id, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
$where[] = 'LOWER(COALESCE(status_code, "")) NOT IN (' . implode(', ', $statusPlaceholders) . ')';
|
||||
}
|
||||
|
||||
$sql = 'SELECT id, source_order_id, payment_status, total_paid, total_with_tax, currency, external_payment_type_id
|
||||
|
||||
@@ -137,7 +137,7 @@ final class ShopproStatusSyncService
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$sourceOrderId = (int) ($order['source_order_id'] ?? 0);
|
||||
$orderproStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));
|
||||
$orderproStatus = strtolower(trim((string) ($order['status_code'] ?? '')));
|
||||
$changeAt = (string) ($order['latest_change'] ?? '');
|
||||
|
||||
if ($sourceOrderId <= 0 || $orderproStatus === '') {
|
||||
@@ -224,7 +224,7 @@ final class ShopproStatusSyncService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{order_id:int,source_order_id:string,external_status_id:string,latest_change:string}>
|
||||
* @return array<int, array{order_id:int,source_order_id:string,status_code:string,latest_change:string}>
|
||||
*/
|
||||
private function findOrdersWithManualStatusChanges(int $integrationId, ?string $lastPushedAt): array
|
||||
{
|
||||
@@ -233,14 +233,14 @@ final class ShopproStatusSyncService
|
||||
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT o.id AS order_id, o.source_order_id, o.external_status_id,
|
||||
'SELECT o.id AS order_id, o.source_order_id, o.status_code,
|
||||
MAX(h.changed_at) AS latest_change
|
||||
FROM order_status_history h
|
||||
JOIN orders o ON o.id = h.order_id
|
||||
WHERE o.integration_id = :integration_id
|
||||
AND h.change_source = :change_source
|
||||
AND h.changed_at > :since_date
|
||||
GROUP BY o.id, o.source_order_id, o.external_status_id
|
||||
GROUP BY o.id, o.source_order_id, o.status_code
|
||||
ORDER BY latest_change ASC
|
||||
LIMIT 50'
|
||||
);
|
||||
|
||||
@@ -139,6 +139,43 @@ final class UserRepository
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateRememberToken(int $userId, ?string $tokenHash): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE users SET remember_token = :token WHERE id = :id'
|
||||
);
|
||||
$statement->execute([
|
||||
'token' => $tokenHash,
|
||||
'id' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findByRememberToken(string $tokenHash): ?array
|
||||
{
|
||||
if ($tokenHash === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, email FROM users WHERE remember_token = :token LIMIT 1'
|
||||
);
|
||||
$statement->execute(['token' => $tokenHash]);
|
||||
|
||||
$row = $statement->fetch();
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'email' => (string) ($row['email'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
public function create(string $name, string $email, string $passwordHash): array
|
||||
{
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
|
||||
Reference in New Issue
Block a user