feat(108): delivery status management
Phase 108 complete (v3.2 milestone):
Plan 108-01 — Delivery Status DB & CRUD:
- Tabela delivery_statuses z seedem 11 statusow systemowych
- DeliveryStatusRepository (CRUD + per-request static cache)
- DeliveryStatus::setRepository() — DB fallback dla static final class
- Panel /settings/delivery-statuses (zakladki Statusy + Mapowanie)
- Sidebar przebudowany: Statusy zamowien + Statusy przesylek
Plan 108-02 — Automation Dropdowns z DB + UI Refactor:
- Dropdowny automatyzacji ladowane z DB (warunek shipment_status + akcja update_shipment_status)
- Walidacja przez DeliveryStatus::getAllStatuses()
- Osobna podstrona formularza CRUD (delivery-status-form.php)
- Lista uproszczona: rename Terminal -> Koncowy, usunieta kolumna Typ
- BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji
- Bug fix: path params w DeliveryStatusesController via \$request->input('id')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use App\Modules\Shipments\DeliveryStatus;
|
||||
use Throwable;
|
||||
|
||||
final class AutomationController
|
||||
@@ -33,17 +34,6 @@ final class AutomationController
|
||||
'online' => 'Karta / platnosc online',
|
||||
'other' => 'Inna',
|
||||
];
|
||||
private const SHIPMENT_STATUS_OPTIONS = [
|
||||
'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']],
|
||||
'courier_pickup' => ['label' => 'Odebrana przez kuriera / nadana w paczkomacie', 'statuses' => ['picked_up']],
|
||||
'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']],
|
||||
'dropped_at_point' => ['label' => 'Przesylka nadana w punkcie', 'statuses' => ['confirmed', 'in_transit']],
|
||||
'picked_up' => ['label' => 'Przesylka odebrana', 'statuses' => ['delivered']],
|
||||
'cancelled' => ['label' => 'Przesylka anulowana', 'statuses' => ['cancelled']],
|
||||
'unclaimed' => ['label' => 'Przesylka nieodebrana', 'statuses' => ['problem']],
|
||||
'picked_up_return' => ['label' => 'Przesylka odebrana (zwrot)', 'statuses' => ['returned']],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
@@ -256,7 +246,7 @@ final class AutomationController
|
||||
'receiptConfigs' => $this->listActiveReceiptConfigs(),
|
||||
'receiptIssueDateModes' => self::ALLOWED_RECEIPT_ISSUE_DATE_MODES,
|
||||
'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES,
|
||||
'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS,
|
||||
'shipmentStatusOptions' => $this->buildShipmentStatusOptions(),
|
||||
'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS,
|
||||
'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS,
|
||||
'orderStatusOptions' => $this->repository->listActiveOrderStatuses(),
|
||||
@@ -427,7 +417,7 @@ final class AutomationController
|
||||
$keys = [];
|
||||
}
|
||||
|
||||
$allowedKeys = array_keys(self::SHIPMENT_STATUS_OPTIONS);
|
||||
$allowedKeys = DeliveryStatus::getAllStatuses();
|
||||
$statusKeys = 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)
|
||||
@@ -570,7 +560,7 @@ final class AutomationController
|
||||
|
||||
if ($type === 'update_shipment_status') {
|
||||
$statusKey = trim((string) ($action['shipment_status_key'] ?? ''));
|
||||
if (!array_key_exists($statusKey, self::SHIPMENT_STATUS_OPTIONS)) {
|
||||
if ($statusKey === '' || !in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -598,6 +588,19 @@ final class AutomationController
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{label:string}>
|
||||
*/
|
||||
private function buildShipmentStatusOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (DeliveryStatus::getAllOptions() as $key => $label) {
|
||||
$options[(string) $key] = ['label' => (string) $label];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id:int,name:string,number_format:string}>
|
||||
*/
|
||||
|
||||
@@ -21,17 +21,6 @@ final class AutomationService
|
||||
private const MAX_CHAIN_DEPTH = 8;
|
||||
private const MAX_CHAIN_EXECUTIONS = 200;
|
||||
|
||||
private const SHIPMENT_STATUS_OPTION_MAP = [
|
||||
'registered' => ['created', 'confirmed'],
|
||||
'courier_pickup' => ['picked_up'],
|
||||
'ready_for_pickup' => ['ready_for_pickup'],
|
||||
'dropped_at_point' => ['confirmed', 'in_transit'],
|
||||
'picked_up' => ['delivered'],
|
||||
'cancelled' => ['cancelled'],
|
||||
'unclaimed' => ['problem'],
|
||||
'picked_up_return' => ['returned'],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly AutomationRepository $repository,
|
||||
private readonly AutomationExecutionLogRepository $executionLogs,
|
||||
@@ -189,12 +178,10 @@ final class AutomationService
|
||||
$allowedStatuses = [];
|
||||
foreach ($statusKeys as $statusKeyRaw) {
|
||||
$statusKey = trim((string) $statusKeyRaw);
|
||||
if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) {
|
||||
if ($statusKey === '') {
|
||||
continue;
|
||||
}
|
||||
foreach (self::SHIPMENT_STATUS_OPTION_MAP[$statusKey] as $mappedStatus) {
|
||||
$allowedStatuses[$mappedStatus] = true;
|
||||
}
|
||||
$allowedStatuses[$statusKey] = true;
|
||||
}
|
||||
|
||||
if ($allowedStatuses === []) {
|
||||
@@ -609,21 +596,15 @@ final class AutomationService
|
||||
|
||||
private function resolveStatusFromActionKey(string $statusKey): ?string
|
||||
{
|
||||
if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) {
|
||||
if ($statusKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mappedStatuses = self::SHIPMENT_STATUS_OPTION_MAP[$statusKey];
|
||||
if ($mappedStatuses === []) {
|
||||
if (!in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidate = trim((string) $mappedStatuses[0]);
|
||||
if ($candidate === '' || !in_array($candidate, DeliveryStatus::ALL_STATUSES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
return $statusKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,11 +12,12 @@ use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Shipments\DeliveryStatus;
|
||||
use App\Modules\Shipments\DeliveryStatusMappingRepository;
|
||||
use App\Modules\Shipments\DeliveryStatusRepository;
|
||||
use Throwable;
|
||||
|
||||
final class DeliveryStatusMappingController
|
||||
{
|
||||
private const REDIRECT_PATH = '/settings/delivery-status-mappings';
|
||||
private const REDIRECT_PATH = '/settings/delivery-statuses?tab=mapping';
|
||||
|
||||
private const PROVIDERS = [
|
||||
'inpost' => 'InPost',
|
||||
@@ -28,7 +29,8 @@ final class DeliveryStatusMappingController
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly DeliveryStatusMappingRepository $repository
|
||||
private readonly DeliveryStatusMappingRepository $repository,
|
||||
private readonly DeliveryStatusRepository $deliveryStatusRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ final class DeliveryStatusMappingController
|
||||
'providers' => self::PROVIDERS,
|
||||
'mappings' => $mappings,
|
||||
'unmappedRawStatuses' => $unmappedRawStatuses,
|
||||
'normalizedOptions' => DeliveryStatus::LABEL_PL,
|
||||
'normalizedOptions' => $this->deliveryStatusRepository->getAllAsOptions(),
|
||||
'errorMessage' => (string) Flash::get('dsm_error', ''),
|
||||
'successMessage' => (string) Flash::get('dsm_success', ''),
|
||||
], 'layouts/app');
|
||||
@@ -108,12 +110,12 @@ final class DeliveryStatusMappingController
|
||||
|
||||
if ($provider === '' || $rawStatus === '') {
|
||||
Flash::set('dsm_error', 'Brakuje wymaganych pól.');
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) {
|
||||
if ($this->deliveryStatusRepository->getByKey($normalizedStatus) === null) {
|
||||
Flash::set('dsm_error', 'Nieprawidłowy status znormalizowany.');
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -123,7 +125,7 @@ final class DeliveryStatusMappingController
|
||||
Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
public function saveBulk(Request $request): Response
|
||||
@@ -140,7 +142,7 @@ final class DeliveryStatusMappingController
|
||||
|
||||
if (!is_array($rawStatuses) || !is_array($normalizedStatuses) || !is_array($descriptions)) {
|
||||
Flash::set('dsm_error', 'Nieprawidłowe dane formularza.');
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -156,7 +158,7 @@ final class DeliveryStatusMappingController
|
||||
$normalizedStatus = trim((string) ($normalizedStatuses[$index] ?? ''));
|
||||
$description = trim((string) ($descriptions[$index] ?? ''));
|
||||
|
||||
if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) {
|
||||
if ($this->deliveryStatusRepository->getByKey($normalizedStatus) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -178,7 +180,7 @@ final class DeliveryStatusMappingController
|
||||
Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
public function reset(Request $request): Response
|
||||
@@ -193,7 +195,7 @@ final class DeliveryStatusMappingController
|
||||
|
||||
if ($provider === '' || $rawStatus === '') {
|
||||
Flash::set('dsm_error', 'Brakuje wymaganych pól.');
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -203,7 +205,7 @@ final class DeliveryStatusMappingController
|
||||
Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
public function resetAll(Request $request): Response
|
||||
@@ -226,7 +228,7 @@ final class DeliveryStatusMappingController
|
||||
Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider));
|
||||
return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider));
|
||||
}
|
||||
|
||||
private function validateCsrf(string $token): ?Response
|
||||
|
||||
286
src/Modules/Settings/DeliveryStatusesController.php
Normal file
286
src/Modules/Settings/DeliveryStatusesController.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Shipments\DeliveryStatus;
|
||||
use App\Modules\Shipments\DeliveryStatusMappingRepository;
|
||||
use App\Modules\Shipments\DeliveryStatusRepository;
|
||||
use Throwable;
|
||||
|
||||
final class DeliveryStatusesController
|
||||
{
|
||||
private const REDIRECT_STATUSES = '/settings/delivery-statuses?tab=statuses';
|
||||
|
||||
private const PROVIDERS = [
|
||||
'inpost' => 'InPost',
|
||||
'apaczka' => 'Apaczka',
|
||||
'allegro_wza' => 'Allegro',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly DeliveryStatusRepository $deliveryStatusRepository,
|
||||
private readonly DeliveryStatusMappingRepository $deliveryStatusMappingRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tab = (string) ($request->input('tab', 'statuses') ?? 'statuses');
|
||||
if (!in_array($tab, ['statuses', 'mapping'], true)) {
|
||||
$tab = 'statuses';
|
||||
}
|
||||
|
||||
$data = [
|
||||
'title' => 'Statusy przesyłek',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'delivery-statuses',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'tab' => $tab,
|
||||
'statuses' => $this->deliveryStatusRepository->getAll(),
|
||||
'errorMessage' => (string) Flash::get('ds_error', ''),
|
||||
'successMessage' => (string) Flash::get('ds_success', ''),
|
||||
];
|
||||
|
||||
if ($tab === 'mapping') {
|
||||
$data = array_merge($data, $this->buildMappingData($request));
|
||||
}
|
||||
|
||||
$html = $this->template->render('settings/delivery-statuses', $data, 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
return $this->renderForm(null);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$id = max(0, (int) $request->input('id', 0));
|
||||
$row = null;
|
||||
foreach ($this->deliveryStatusRepository->getAll() as $candidate) {
|
||||
if ((int) ($candidate['id'] ?? 0) === $id) {
|
||||
$row = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($row === null) {
|
||||
Flash::set('ds_error', 'Status nie istnieje.');
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
if ((int) ($row['is_system'] ?? 0) === 1) {
|
||||
Flash::set('ds_error', 'Statusów systemowych nie można edytować.');
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
return $this->renderForm($row);
|
||||
}
|
||||
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$key = trim((string) $request->input('key', ''));
|
||||
$labelPl = trim((string) $request->input('label_pl', ''));
|
||||
$color = trim((string) $request->input('color', '#6c757d'));
|
||||
$sortOrder = (int) $request->input('sort_order', 0);
|
||||
$isTerminal = $request->input('is_terminal', '') !== '' ? 1 : 0;
|
||||
|
||||
$validationError = $this->validateFields($key, $labelPl, $color, true);
|
||||
if ($validationError !== null) {
|
||||
Flash::set('ds_error', $validationError);
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryStatusRepository->create([
|
||||
'key' => $key,
|
||||
'label_pl' => $labelPl,
|
||||
'color' => $color,
|
||||
'sort_order' => $sortOrder,
|
||||
'is_terminal' => $isTerminal,
|
||||
]);
|
||||
Flash::set('ds_success', 'Status dodany.');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('ds_error', 'Błąd: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$id = max(0, (int) $request->input('id', 0));
|
||||
$labelPl = trim((string) $request->input('label_pl', ''));
|
||||
$color = trim((string) $request->input('color', '#6c757d'));
|
||||
$sortOrder = (int) $request->input('sort_order', 0);
|
||||
$isTerminal = $request->input('is_terminal', '') !== '' ? 1 : 0;
|
||||
|
||||
$validationError = $this->validateFields('valid_key', $labelPl, $color, false);
|
||||
if ($validationError !== null) {
|
||||
Flash::set('ds_error', $validationError);
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->deliveryStatusRepository->update($id, [
|
||||
'label_pl' => $labelPl,
|
||||
'color' => $color,
|
||||
'sort_order' => $sortOrder,
|
||||
'is_terminal' => $isTerminal,
|
||||
]);
|
||||
Flash::set('ds_success', 'Status zaktualizowany.');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('ds_error', 'Błąd: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
$csrfError = $this->validateCsrf((string) $request->input('_token', ''));
|
||||
if ($csrfError !== null) {
|
||||
return $csrfError;
|
||||
}
|
||||
|
||||
$id = max(0, (int) $request->input('id', 0));
|
||||
|
||||
try {
|
||||
$this->deliveryStatusRepository->delete($id);
|
||||
Flash::set('ds_success', 'Status usunięty.');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('ds_error', 'Błąd: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $row
|
||||
*/
|
||||
private function renderForm(?array $row): Response
|
||||
{
|
||||
$isEdit = $row !== null && isset($row['id']);
|
||||
|
||||
$html = $this->template->render('settings/delivery-status-form', [
|
||||
'title' => $isEdit ? 'Edytuj status przesyłki' : 'Nowy status przesyłki',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'delivery-statuses',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'row' => $row,
|
||||
'isEdit' => $isEdit,
|
||||
'errorMessage' => (string) Flash::get('ds_error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
private function validateCsrf(string $token): ?Response
|
||||
{
|
||||
if (Csrf::validate($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Flash::set('ds_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect(self::REDIRECT_STATUSES);
|
||||
}
|
||||
|
||||
private function validateFields(string $key, string $labelPl, string $color, bool $validateKey): ?string
|
||||
{
|
||||
if ($validateKey) {
|
||||
if ($key === '' || !preg_match('/^[a-z][a-z0-9_]{0,49}$/', $key)) {
|
||||
return 'Klucz statusu jest nieprawidłowy (tylko małe litery, cyfry, _, max 50 znaków, zaczyna się literą).';
|
||||
}
|
||||
}
|
||||
|
||||
if ($labelPl === '' || mb_strlen($labelPl) > 100) {
|
||||
return 'Etykieta jest wymagana i nie może przekraczać 100 znaków.';
|
||||
}
|
||||
|
||||
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
|
||||
return 'Kolor musi być w formacie #RRGGBB.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildMappingData(Request $request): array
|
||||
{
|
||||
$provider = strtolower(trim((string) $request->input('provider', 'inpost')));
|
||||
if (!isset(self::PROVIDERS[$provider])) {
|
||||
$provider = 'inpost';
|
||||
}
|
||||
|
||||
$defaults = DeliveryStatus::getDefaultMappings($provider);
|
||||
$overrides = $this->deliveryStatusMappingRepository->listByProvider($provider);
|
||||
|
||||
$overrideMap = [];
|
||||
foreach ($overrides as $row) {
|
||||
$overrideMap[$row['raw_status']] = $row;
|
||||
}
|
||||
|
||||
$mappings = [];
|
||||
$knownRawStatuses = [];
|
||||
foreach ($defaults as $rawStatus => $default) {
|
||||
$isCustom = isset($overrideMap[$rawStatus]);
|
||||
$mappings[] = [
|
||||
'raw_status' => $rawStatus,
|
||||
'description' => $isCustom ? $overrideMap[$rawStatus]['description'] : $default['description'],
|
||||
'normalized_status' => $isCustom ? $overrideMap[$rawStatus]['normalized_status'] : $default['normalized'],
|
||||
'is_custom' => $isCustom,
|
||||
];
|
||||
$knownRawStatuses[$rawStatus] = true;
|
||||
}
|
||||
|
||||
foreach ($overrideMap as $rawStatus => $row) {
|
||||
if (isset($knownRawStatuses[$rawStatus])) {
|
||||
continue;
|
||||
}
|
||||
$mappings[] = [
|
||||
'raw_status' => $rawStatus,
|
||||
'description' => $row['description'],
|
||||
'normalized_status' => $row['normalized_status'],
|
||||
'is_custom' => true,
|
||||
];
|
||||
$knownRawStatuses[$rawStatus] = true;
|
||||
}
|
||||
|
||||
$unmappedRawStatuses = $this->deliveryStatusMappingRepository->listUnmappedRawStatuses($provider, $knownRawStatuses);
|
||||
|
||||
return [
|
||||
'provider' => $provider,
|
||||
'providers' => self::PROVIDERS,
|
||||
'mappings' => $mappings,
|
||||
'unmappedRawStatuses' => $unmappedRawStatuses,
|
||||
'normalizedOptions' => $this->deliveryStatusRepository->getAllAsOptions(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ namespace App\Modules\Shipments;
|
||||
|
||||
final class DeliveryStatus
|
||||
{
|
||||
private static ?DeliveryStatusRepository $repository = null;
|
||||
|
||||
public const UNKNOWN = 'unknown';
|
||||
public const CREATED = 'created';
|
||||
public const CONFIRMED = 'confirmed';
|
||||
@@ -379,8 +381,46 @@ final class DeliveryStatus
|
||||
return $map[$rawStatus] ?? $rawStatus;
|
||||
}
|
||||
|
||||
public static function setRepository(DeliveryStatusRepository $repo): void
|
||||
{
|
||||
self::$repository = $repo;
|
||||
}
|
||||
|
||||
public static function getAllStatuses(): array
|
||||
{
|
||||
if (self::$repository !== null) {
|
||||
return array_column(self::$repository->getAll(), 'key');
|
||||
}
|
||||
return self::ALL_STATUSES;
|
||||
}
|
||||
|
||||
public static function getAllOptions(): array
|
||||
{
|
||||
if (self::$repository !== null) {
|
||||
return self::$repository->getAllAsOptions();
|
||||
}
|
||||
return self::LABEL_PL;
|
||||
}
|
||||
|
||||
public static function getColor(string $key): string
|
||||
{
|
||||
if (self::$repository !== null) {
|
||||
$row = self::$repository->getByKey($key);
|
||||
if ($row !== null) {
|
||||
return (string) $row['color'];
|
||||
}
|
||||
}
|
||||
return '#6c757d';
|
||||
}
|
||||
|
||||
public static function label(string $status): string
|
||||
{
|
||||
if (self::$repository !== null) {
|
||||
$row = self::$repository->getByKey($status);
|
||||
if ($row !== null) {
|
||||
return (string) $row['label_pl'];
|
||||
}
|
||||
}
|
||||
return self::LABEL_PL[$status] ?? 'Nieznany';
|
||||
}
|
||||
|
||||
|
||||
157
src/Modules/Shipments/DeliveryStatusRepository.php
Normal file
157
src/Modules/Shipments/DeliveryStatusRepository.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class DeliveryStatusRepository
|
||||
{
|
||||
private static ?array $cache = null;
|
||||
|
||||
public function __construct(private readonly PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
if (self::$cache !== null) {
|
||||
return self::$cache;
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
'SELECT * FROM delivery_statuses ORDER BY sort_order ASC'
|
||||
);
|
||||
$statement->execute();
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
self::$cache = is_array($rows) ? $rows : [];
|
||||
|
||||
return self::$cache;
|
||||
}
|
||||
|
||||
public function getByKey(string $key): ?array
|
||||
{
|
||||
foreach ($this->getAll() as $row) {
|
||||
if ($row['key'] === $key) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAllAsOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ($this->getAll() as $row) {
|
||||
$options[$row['key']] = $row['label_pl'];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
if ($this->getByKey($data['key']) !== null) {
|
||||
throw new \RuntimeException('Status o tym kluczu już istnieje.');
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
'INSERT INTO delivery_statuses (`key`, label_pl, color, sort_order, is_terminal)
|
||||
VALUES (:key, :label_pl, :color, :sort_order, :is_terminal)'
|
||||
);
|
||||
$statement->bindValue(':key', $data['key']);
|
||||
$statement->bindValue(':label_pl', $data['label_pl']);
|
||||
$statement->bindValue(':color', $data['color']);
|
||||
$statement->bindValue(':sort_order', $data['sort_order'], PDO::PARAM_INT);
|
||||
$statement->bindValue(':is_terminal', $data['is_terminal'], PDO::PARAM_INT);
|
||||
$statement->execute();
|
||||
|
||||
$this->clearCache();
|
||||
|
||||
return (int) $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): void
|
||||
{
|
||||
$row = $this->findById($id);
|
||||
if ($row === null) {
|
||||
throw new \RuntimeException('Status nie istnieje.');
|
||||
}
|
||||
|
||||
if ((int) $row['is_system'] === 1) {
|
||||
throw new \RuntimeException('Statusów systemowych nie można edytować.');
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
'UPDATE delivery_statuses
|
||||
SET label_pl = :label_pl, color = :color, sort_order = :sort_order, is_terminal = :is_terminal
|
||||
WHERE id = :id'
|
||||
);
|
||||
$statement->bindValue(':label_pl', $data['label_pl']);
|
||||
$statement->bindValue(':color', $data['color']);
|
||||
$statement->bindValue(':sort_order', $data['sort_order'], PDO::PARAM_INT);
|
||||
$statement->bindValue(':is_terminal', $data['is_terminal'], PDO::PARAM_INT);
|
||||
$statement->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$statement->execute();
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$row = $this->findById($id);
|
||||
if ($row === null) {
|
||||
throw new \RuntimeException('Status nie istnieje.');
|
||||
}
|
||||
|
||||
if ((int) $row['is_system'] === 1) {
|
||||
throw new \RuntimeException('Statusów systemowych nie można usunąć.');
|
||||
}
|
||||
|
||||
$key = (string) $row['key'];
|
||||
|
||||
$stmtMappings = $this->db->prepare(
|
||||
'SELECT COUNT(*) FROM delivery_status_mappings WHERE normalized_status = :key'
|
||||
);
|
||||
$stmtMappings->bindValue(':key', $key);
|
||||
$stmtMappings->execute();
|
||||
$countMappings = (int) $stmtMappings->fetchColumn();
|
||||
|
||||
$stmtPackages = $this->db->prepare(
|
||||
'SELECT COUNT(*) FROM shipment_packages WHERE delivery_status = :key'
|
||||
);
|
||||
$stmtPackages->bindValue(':key', $key);
|
||||
$stmtPackages->execute();
|
||||
$countPackages = (int) $stmtPackages->fetchColumn();
|
||||
|
||||
if ($countMappings > 0 || $countPackages > 0) {
|
||||
throw new \RuntimeException('Status jest używany i nie może być usunięty.');
|
||||
}
|
||||
|
||||
$statement = $this->db->prepare(
|
||||
'DELETE FROM delivery_statuses WHERE id = :id'
|
||||
);
|
||||
$statement->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$statement->execute();
|
||||
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
self::$cache = null;
|
||||
}
|
||||
|
||||
private function findById(int $id): ?array
|
||||
{
|
||||
foreach ($this->getAll() as $row) {
|
||||
if ((int) $row['id'] === $id) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user