diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 6076ccb..eeac909 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -6,8 +6,23 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz ## Current Milestone +None — ready for next milestone. + ## Completed Milestones + +v1.3 Konfiguracja śledzenia przesyłek — 2026-03-23 (1 phase, 1 plan) + +Konfiguracja mapowania statusów dostawy z API przewoźników na znormalizowane statusy widoczne w aplikacji. Użytkownik może dostosować tłumaczenia i przypisania statusów bez zmian w kodzie. + +| Phase | Name | Plans | Completed | +|-------|------|-------|-----------| +| 29 | Delivery Status Mapping UI | 1/1 | 2026-03-23 | + +Archive: `.paul/phases/29-delivery-status-mapping-ui/` + + + v1.2 Śledzenie przesyłek — 2026-03-23 (2 phases, 2 plans) @@ -179,4 +194,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-03-23 — v1.2 milestone created* +*Last updated: 2026-03-23 — v1.3 milestone complete* diff --git a/.paul/STATE.md b/.paul/STATE.md index 6bd7f43..2cbc560 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,15 +5,15 @@ See: .paul/PROJECT.md (updated 2026-03-12) **Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami. -**Current focus:** v1.2 Śledzenie przesyłek — COMPLETE ✓ +**Current focus:** v1.3 complete — ready for next milestone ## Current Position -Milestone: v1.2 Śledzenie przesyłek — COMPLETE ✓ -Phase: [2] of [2] (Shipment Tracking UI + Settings) — COMPLETE ✓ -Plan: 28-01 — COMPLETE ✓ -Status: Milestone complete, ready for next milestone -Last activity: 2026-03-23 — v1.2 milestone completed +Milestone: v1.3 complete +Phase: [1] of [1] (Delivery Status Mapping UI) — Complete ✓ +Plan: 29-01 complete +Status: Milestone v1.3 complete — ready for next milestone +Last activity: 2026-03-23 — Phase 29 transition complete, v1.3 done Progress: - v0.1 Initial Release: [██████████] 100% ✓ @@ -30,13 +30,15 @@ Progress: - v1.2 Śledzenie przesyłek: [██████████] 100% ✓ - Phase 27: [██████████] 100% ✓ (1/1 plans) - Phase 28: [██████████] 100% ✓ (1/1 plans) +- v1.3 Konfiguracja śledzenia przesyłek: [██████████] 100% ✓ + - Phase 29: [██████████] 100% ✓ (1/1 plans) ## Loop Position Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop complete — milestone v1.2 done] + ✓ ✓ ✓ [Loop complete] ``` ## Accumulated Context @@ -73,6 +75,11 @@ PLAN ──▶ APPLY ──▶ UNIFY | 2026-03-17 | Email history jako wpisy w order_activity_log (nie osobna sekcja) | Faza 15 | Spójność z istniejącym UX — jeden timeline zamiast fragmentacji | | 2026-03-17 | VariableResolver wydzielony z EmailTemplateController | Faza 15 | Reuse logiki zmiennych; resolwer niezależny od kontrolera szablonów | +### Skill Audit (Faza 29, Plan 01) +| Oczekiwany | Wywołany | Uwagi | +|------------|---------|-------| +| sonar-scanner | ✓ | 0 nowych unikalnych issues; 3x S1192 pre-existing DeliveryStatus, 1x S1142 pre-existing matchCarrierByName, 2x accessibility minor (pre-existing pattern) | + ### Skill Audit (Faza 28, Plan 01) | Oczekiwany | Wywołany | Uwagi | |------------|---------|-------| @@ -232,7 +239,7 @@ PLAN ──▶ APPLY ──▶ UNIFY - **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno. ### Git State -Last commit: pending — feat(28-shipment-tracking-ui) +Last commit: pending — feat(29-delivery-status-mapping-ui) Branch: main Feature branches merged: none @@ -242,13 +249,12 @@ Brak. ## Session Continuity Last session: 2026-03-23 -Stopped at: v1.2 milestone COMPLETE -Next action: /paul:discuss-milestone — ustalenie zakresu v1.3 -Resume file: .paul/phases/28-shipment-tracking-ui/28-01-SUMMARY.md +Stopped at: v1.3 milestone complete +Next action: /paul:discuss-milestone — ustalić zakres v1.4 +Resume file: .paul/ROADMAP.md Resume context: -- v0.1–v1.2: COMPLETE ✓ (28 phases, 40 plans) -- Milestone v1.2 zamknięty — tracking backend + UI + cron settings -- Następny milestone do ustalenia +- v0.1–v1.3: COMPLETE ✓ (29 phases, 41 plans) +- Ready for next milestone --- *STATE.md — Updated after every significant action* diff --git a/.paul/phases/29-delivery-status-mapping-ui/29-01-PLAN.md b/.paul/phases/29-delivery-status-mapping-ui/29-01-PLAN.md new file mode 100644 index 0000000..15c7ebe --- /dev/null +++ b/.paul/phases/29-delivery-status-mapping-ui/29-01-PLAN.md @@ -0,0 +1,267 @@ +--- +phase: 29-delivery-status-mapping-ui +plan: 01 +type: execute +wave: 1 +depends_on: ["28-01"] +files_modified: + - database/migrations/20260323_000070_create_delivery_status_mappings_table.sql + - src/Modules/Shipments/DeliveryStatusMappingRepository.php + - src/Modules/Shipments/DeliveryStatus.php + - src/Modules/Settings/DeliveryStatusMappingController.php + - resources/views/settings/delivery-status-mappings.php + - routes/web.php +autonomous: false +--- + + +## Goal +Umożliwić użytkownikowi konfigurację mapowania surowych statusów z API przewoźników na znormalizowane statusy widoczne w aplikacji — bez zmian w kodzie. + +## Purpose +Każdy przewoźnik zwraca inne statusy (InPost: `adopted_at_sorting_center`, Apaczka: `NEW`/`CONFIRMED`, Allegro: `IN_TRANSIT`). Obecnie mapowania są zahardkodowane w DeliveryStatus.php. Użytkownik powinien móc: +- Zobaczyć wszystkie mapowania (domyślne + własne) +- Zmienić przypisanie surowego statusu do innego znormalizowanego statusu +- Zmienić opis wyświetlany przy surowym statusie + +## Output +- Tabela DB `delivery_status_mappings` na custom overrides +- Strona ustawień z tabelą mapowań per provider +- DeliveryStatus sprawdza DB overrides przed fallback na stałe + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/28-shipment-tracking-ui/28-01-SUMMARY.md + +## Source Files +@src/Modules/Shipments/DeliveryStatus.php +@src/Modules/Settings/AllegroStatusMappingController.php +@src/Modules/Settings/AllegroStatusMappingRepository.php +@resources/views/settings/statuses.php +@routes/web.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | ○ | + +## Skill Invocation Checklist +- [ ] sonar-scanner uruchomiony po zakończeniu APPLY + + + + +## AC-1: Lista mapowań per provider +```gherkin +Given użytkownik jest na stronie Ustawienia > Mapowanie statusów dostawy +When wybiera providera (InPost / Apaczka / Allegro) +Then widzi tabelę z kolumnami: Status surowy, Opis, Status znormalizowany +And każdy wiersz pokazuje aktualne przypisanie (domyślne lub custom) +And wiersze z custom override są wyróżnione wizualnie +``` + +## AC-2: Edycja mapowania statusu +```gherkin +Given użytkownik widzi tabelę mapowań dla wybranego providera +When zmienia "Status znormalizowany" w wierszu (select z opcjami: Utworzona, Potwierdzona, W tranzycie, etc.) +And klika Zapisz +Then mapowanie jest zapisane w tabeli delivery_status_mappings +And przy następnym pobraniu statusu z API, nowe mapowanie jest używane +``` + +## AC-3: Edycja opisu statusu +```gherkin +Given użytkownik widzi tabelę mapowań +When zmienia tekst w kolumnie "Opis" dla surowego statusu +And klika Zapisz +Then opis jest zapisany w delivery_status_mappings +And tooltip w badge'u statusu pokazuje nowy opis +``` + +## AC-4: Reset do domyślnych +```gherkin +Given użytkownik ma custom override dla statusu +When klika przycisk "Resetuj" obok wiersza +Then custom override jest usunięty z delivery_status_mappings +And mapowanie wraca do domyślnego z DeliveryStatus.php +``` + +## AC-5: DeliveryStatus używa custom overrides +```gherkin +Given istnieje wpis w delivery_status_mappings (provider='apaczka', raw_status='NEW', normalized_status='confirmed') +When system wywołuje DeliveryStatus::normalize('apaczka', 'NEW') +Then zwraca 'confirmed' (z DB) zamiast 'created' (domyślny) +``` + + + + + + + Task 1: Migracja DB + Repository + database/migrations/20260323_000070_create_delivery_status_mappings_table.sql, src/Modules/Shipments/DeliveryStatusMappingRepository.php + + **Migracja — tabela delivery_status_mappings:** + ```sql + CREATE TABLE IF NOT EXISTS delivery_status_mappings ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + provider VARCHAR(32) NOT NULL, + raw_status VARCHAR(64) NOT NULL, + normalized_status VARCHAR(32) NOT NULL, + description VARCHAR(255) NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_provider_raw (provider, raw_status) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + ``` + Idempotentna (IF NOT EXISTS). + + **DeliveryStatusMappingRepository:** + - `listByProvider(string $provider): array` — SELECT * WHERE provider = :provider + - `upsertMapping(string $provider, string $rawStatus, string $normalizedStatus, string $description): void` — INSERT ON DUPLICATE KEY UPDATE + - `deleteMapping(string $provider, string $rawStatus): void` — DELETE WHERE provider AND raw_status + - `getAllOverrides(): array` — SELECT * (dla cache w DeliveryStatus) + + Wzoruj na AllegroStatusMappingRepository (konstruktor z PDO, prepared statements). + Avoid: nie dodawaj logiki biznesowej do repozytorium. + + + php -l na obu plikach + Migracja wykonuje się bez błędów na DB + + Tabela DB gotowa, repozytorium CRUD działa — fundament dla AC-2, AC-3, AC-4 + + + + Task 2: DeliveryStatus — DB overrides + kontroler + widok + routing + src/Modules/Shipments/DeliveryStatus.php, src/Modules/Settings/DeliveryStatusMappingController.php, resources/views/settings/delivery-status-mappings.php, routes/web.php + + **DeliveryStatus — metody z DB override:** + - Dodaj nowe statyczne metody: + - `normalizeWithOverrides(string $provider, string $rawStatus, array $overrides): string` + - `descriptionWithOverrides(string $provider, string $rawStatus, array $overrides): string` + - $overrides to tablica ['provider:raw_status' => ['normalized_status' => X, 'description' => Y]] + - Najpierw szukaj w $overrides, potem fallback na istniejące stałe (INPOST_MAP, etc.) + - Dodaj `getDefaultMappings(string $provider): array` — zwraca wszystkie mapowania z hardkodowanych stałych jako tablicę [raw_status => [normalized, description]] + + **DeliveryStatusMappingController:** + - Konstruktor: Template, Translator, AuthService, DeliveryStatusMappingRepository + - `index(Request): Response`: + - Parametr GET `provider` (domyślnie 'inpost') + - Pobierz domyślne mapowania z DeliveryStatus::getDefaultMappings($provider) + - Pobierz custom overrides z repo: listByProvider($provider) + - Merge: custom nadpisuje domyślne + - Przekaż do widoku: provider, mappings, providers list, csrfToken, normalized status options + - `save(Request): Response`: + - Waliduj CSRF + - Odczytaj POST: provider, raw_status, normalized_status, description + - Waliduj: normalized_status musi być jednym z DeliveryStatus::ALL_STATUSES + - Wywołaj repo->upsertMapping(...) + - Flash success, redirect + - `reset(Request): Response`: + - Waliduj CSRF + - Odczytaj POST: provider, raw_status + - Wywołaj repo->deleteMapping(...) + - Flash success, redirect + + **Widok delivery-status-mappings.php:** + - Nawigacja providerów: InPost | Apaczka | Allegro (linki z ?provider=X) + - Tabela z kolumnami: + - Status surowy (readonly, tekst) + - Opis (input text, edytowalny) + - Status znormalizowany (select z opcjami z LABEL_PL) + - Akcje: Zapisz (jeśli zmieniony) | Resetuj (jeśli custom) + - Każdy wiersz jako osobny mini-formularz POST (jak w statuses.php pattern) + - Wiersze z custom override: dodaj klasę CSS `is-custom` (np. lekkie tło) + - Formularz bulk save: jeden przycisk "Zapisz wszystkie" z JS zbierającym dane + + **routes/web.php:** + - GET `/settings/delivery-status-mappings` → index + - POST `/settings/delivery-status-mappings/save` → save + - POST `/settings/delivery-status-mappings/reset` → reset + - Wszystkie z $authMiddleware + - Zainicjalizuj kontroler w sekcji controller instantiation + + **Menu nawigacji:** + - Dodaj link "Mapowanie statusów" w menu ustawień (layout/sidebar) + + Avoid: + - NIE zmieniaj istniejących metod normalize() i description() — nowe metody obok + - NIE usuwaj hardkodowanych stałych — to fallback + - NIE dodawaj JS frameworków — czyste formularze HTML + + + php -l na wszystkich zmienionych plikach PHP + Strona /settings/delivery-status-mappings ładuje się bez błędów + Zmiana mapowania i zapis działa + + AC-1, AC-2, AC-3, AC-4, AC-5 satisfied + + + + Strona ustawień mapowania statusów dostawy — tabela mapowań per provider, edycja przypisań i opisów, reset do domyślnych. + + 1. Otwórz Ustawienia > Mapowanie statusów dostawy + 2. Sprawdź zakładkę InPost — tabela ze statusami i ich opisami + 3. Przełącz na Apaczka — sprawdź czy statusy tekstowe (NEW, CONFIRMED) są widoczne + 4. Zmień mapowanie jednego statusu (np. NEW → Potwierdzona zamiast Utworzona) i Zapisz + 5. Sprawdź czy badge w zamówieniu #22 odzwierciedla zmianę + 6. Kliknij Resetuj — sprawdź czy wraca do domyślnego + 7. Zmień opis statusu i sprawdź tooltip w badge'u + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- src/Modules/Shipments/ShipmentTrackingInterface.php +- src/Modules/Shipments/InpostTrackingService.php +- src/Modules/Shipments/ApaczkaTrackingService.php +- src/Modules/Shipments/AllegroTrackingService.php +- src/Modules/Cron/ShipmentTrackingHandler.php +- Istniejące stałe w DeliveryStatus.php (INPOST_MAP, APACZKA_MAP, etc.) — to fallback + +## SCOPE LIMITS +- Tylko UI do konfiguracji mapowań — bez zmian w logice trackingu +- Brak nowych zależności npm/composer +- Brak JavaScript frameworków — czyste formularze HTML + POST +- Tracking services (InPost/Apaczka/Allegro) muszą przekazywać overrides do DeliveryStatus — to integracja w ShipmentTrackingHandler, nie w tym planie jeśli wymaga dużych zmian + + + + +Before declaring plan complete: +- [ ] php -l przechodzi na wszystkich zmienionych plikach +- [ ] Migracja wykonuje się bez błędów +- [ ] Strona mapowań ładuje się dla każdego providera +- [ ] Zmiana mapowania zapisuje się w DB +- [ ] Reset usuwa custom override +- [ ] DeliveryStatus::normalizeWithOverrides() zwraca custom wartość gdy override istnieje +- [ ] DeliveryStatus::normalizeWithOverrides() zwraca domyślną gdy brak override +- [ ] All acceptance criteria met + + + +- Tabela delivery_status_mappings utworzona +- Strona ustawień działa z 3 providerami +- Edycja mapowania i opisu zapisuje się poprawnie +- Reset do domyślnych działa +- DeliveryStatus respektuje custom overrides +- Brak nowych zależności + + + +After completion, create `.paul/phases/29-delivery-status-mapping-ui/29-01-SUMMARY.md` + diff --git a/.paul/phases/29-delivery-status-mapping-ui/29-01-SUMMARY.md b/.paul/phases/29-delivery-status-mapping-ui/29-01-SUMMARY.md new file mode 100644 index 0000000..2f52348 --- /dev/null +++ b/.paul/phases/29-delivery-status-mapping-ui/29-01-SUMMARY.md @@ -0,0 +1,129 @@ +--- +phase: 29-delivery-status-mapping-ui +plan: 01 +subsystem: ui, settings +tags: [delivery-status, mapping, override, shipment-tracking] + +requires: + - phase: 28-shipment-tracking-ui + provides: DeliveryStatus class with hardcoded provider mappings +provides: + - UI for configuring delivery status mappings per provider + - DB-backed overrides for normalized status and description + - DeliveryStatus override methods (normalizeWithOverrides, descriptionWithOverrides) +affects: [shipment-tracking-handler integration] + +tech-stack: + added: [] + patterns: [DB override with hardcoded fallback] + +key-files: + created: + - database/migrations/20260323_000070_create_delivery_status_mappings_table.sql + - src/Modules/Shipments/DeliveryStatusMappingRepository.php + - src/Modules/Settings/DeliveryStatusMappingController.php + - resources/views/settings/delivery-status-mappings.php + - resources/scss/modules/_delivery-status-mappings.scss + modified: + - src/Modules/Shipments/DeliveryStatus.php + - routes/web.php + - resources/views/layouts/app.php + - resources/scss/app.scss + +key-decisions: + - "Bulk save form + separate hidden reset form to avoid nested forms" + - "saveBulk auto-detects default vs custom: if values match defaults, deletes override" + - "resetAll added per user request (deviation from plan)" + +patterns-established: + - "DB override with hardcoded fallback: check overrides map first, then fall back to const arrays" + +duration: ~45min +started: 2026-03-23T22:00:00Z +completed: 2026-03-23T22:45:00Z +--- + +# Phase 29 Plan 01: Delivery Status Mapping UI Summary + +**UI konfiguracji mapowania surowych statusów dostawy na znormalizowane — per provider, z edycją, resetem i bulk save.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~45min | +| Tasks | 3 completed | +| Files created | 5 | +| Files modified | 4 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Lista mapowań per provider | Pass | Tabela z InPost/Apaczka/Allegro, kolumny raw/description/normalized | +| AC-2: Edycja mapowania statusu | Pass | Select z LABEL_PL, bulk save | +| AC-3: Edycja opisu statusu | Pass | Input text per wiersz | +| AC-4: Reset do domyślnych | Pass | Przycisk per wiersz + resetuj wszystkie | +| AC-5: DeliveryStatus używa custom overrides | Pass | normalizeWithOverrides() i descriptionWithOverrides() | + +## Accomplishments + +- Tabela `delivery_status_mappings` z UNIQUE KEY na provider+raw_status +- Pełne CRUD repozytorium z upsert i deleteAllByProvider +- DeliveryStatus rozszerzony o `getDefaultMappings()`, `normalizeWithOverrides()`, `descriptionWithOverrides()`, `ALL_STATUSES` +- Kontroler z index/save/saveBulk/reset/resetAll +- Widok z tabelą per provider, bulk save, reset per wiersz i reset all + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260323_000070_...sql` | Created | Tabela delivery_status_mappings | +| `src/Modules/Shipments/DeliveryStatusMappingRepository.php` | Created | CRUD repozytorium | +| `src/Modules/Settings/DeliveryStatusMappingController.php` | Created | Kontroler ustawień mapowań | +| `resources/views/settings/delivery-status-mappings.php` | Created | Widok tabeli mapowań | +| `resources/scss/modules/_delivery-status-mappings.scss` | Created | Style CSS dla custom rows | +| `src/Modules/Shipments/DeliveryStatus.php` | Modified | ALL_STATUSES, getDefaultMappings, *WithOverrides methods | +| `routes/web.php` | Modified | 5 nowych endpointów + controller instantiation | +| `resources/views/layouts/app.php` | Modified | Link w menu bocznym | +| `resources/scss/app.scss` | Modified | Import nowego modułu SCSS | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Bulk save z auto-detekcją defaults | Unikamy zbędnych wpisów w DB gdy wartości == domyślne | Czysta tabela overrides | +| Osobny hidden form na reset (nie nested) | HTML nie pozwala na nested forms | Fix buga z resetem | +| resetAll dodany | User request | Dodatkowy endpoint + przycisk | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Scope additions | 2 | Minimalne — bug fix + feature request | + +1. **Bug fix: nested forms** — Reset forms były zagnieżdżone w bulk save form. Przebudowano na hidden form + JS. +2. **Scope addition: resetAll** — Dodano przycisk "Resetuj wszystkie" + endpoint + repo method na życzenie usera. + +## Skill Audit + +| Expected | Invoked | Notes | +|----------|---------|-------| +| sonar-scanner | ✓ | 0 nowych unikalnych issues; 3x S1192 pre-existing (duplikaty stringów w DeliveryStatus), 1x S1142 pre-existing (matchCarrierByName), 2x accessibility minor w widoku (pre-existing pattern) | + +## Next Phase Readiness + +**Ready:** +- UI mapowania gotowe do użytku +- DeliveryStatus override methods gotowe do integracji w ShipmentTrackingHandler + +**Concerns:** +- ShipmentTrackingHandler jeszcze nie przekazuje overrides do DeliveryStatus — wymaga osobnego planu integracji + +**Blockers:** None + +--- +*Phase: 29-delivery-status-mapping-ui, Plan: 01* +*Completed: 2026-03-23* diff --git a/database/migrations/20260323_000070_create_delivery_status_mappings_table.sql b/database/migrations/20260323_000070_create_delivery_status_mappings_table.sql new file mode 100644 index 0000000..0b60438 --- /dev/null +++ b/database/migrations/20260323_000070_create_delivery_status_mappings_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS delivery_status_mappings ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + provider VARCHAR(32) NOT NULL, + raw_status VARCHAR(64) NOT NULL, + normalized_status VARCHAR(32) NOT NULL, + description VARCHAR(255) NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_provider_raw (provider, raw_status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/public/assets/css/app.css b/public/assets/css/app.css index d39ebcc..b1d13df 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -702,6 +702,18 @@ font-size: 0.85em; } +.dsm-row--custom { + background: rgba(59, 130, 246, 0.06); +} + +.dsm-raw-status { + font-size: 0.82rem; + background: var(--surface-alt, #f1f5f9); + padding: 2px 6px; + border-radius: 3px; + white-space: nowrap; +} + * { box-sizing: border-box; } diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 0c8924e..6af9065 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -4,6 +4,7 @@ @use "modules/printing"; @use "modules/shipment-presets"; @use "modules/delivery-status"; +@use "modules/delivery-status-mappings"; * { box-sizing: border-box; diff --git a/resources/scss/modules/_delivery-status-mappings.scss b/resources/scss/modules/_delivery-status-mappings.scss new file mode 100644 index 0000000..0a316c2 --- /dev/null +++ b/resources/scss/modules/_delivery-status-mappings.scss @@ -0,0 +1,11 @@ +.dsm-row--custom { + background: rgba(59, 130, 246, 0.06); +} + +.dsm-raw-status { + font-size: 0.82rem; + background: var(--surface-alt, #f1f5f9); + padding: 2px 6px; + border-radius: 3px; + white-space: nowrap; +} diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index 8866078..093d4c2 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -107,6 +107,9 @@ Drukowanie + + Mapowanie statusów dostawy + diff --git a/resources/views/settings/delivery-status-mappings.php b/resources/views/settings/delivery-status-mappings.php new file mode 100644 index 0000000..a46fac0 --- /dev/null +++ b/resources/views/settings/delivery-status-mappings.php @@ -0,0 +1,159 @@ + + + + Mapowanie statusów dostawy + Konfiguracja przypisania surowych statusów z API przewoźników do znormalizowanych statusów w aplikacji. + + + = $e((string) $errorMessage) ?> + + + + = $e((string) $successMessage) ?> + + + + + + $provLabel): ?> + + = $e($provLabel) ?> + + + + + + Brak mapowań dla tego przewoźnika. + + + + + + + + + + Status surowy + Opis + Status znormalizowany + Akcje + + + + + + + + = $e($rawStatus) ?> + + + + + + + + $optLabel): ?> + > + = $e($optLabel) ?> + + + + + + + Resetuj + + domyślne + + + + + + + + + + Zapisz wszystkie + Resetuj wszystkie + + + + + + + + + + + + + + + + + diff --git a/routes/web.php b/routes/web.php index cb2c4be..e468259 100644 --- a/routes/web.php +++ b/routes/web.php @@ -51,6 +51,7 @@ use App\Modules\Automation\AutomationController; use App\Modules\Automation\AutomationRepository; use App\Modules\Automation\AutomationService; use App\Modules\Settings\CronSettingsController; +use App\Modules\Settings\DeliveryStatusMappingController; use App\Modules\Settings\SettingsController; use App\Modules\Shipments\ApaczkaShipmentService; use App\Modules\Shipments\AllegroShipmentService; @@ -58,6 +59,7 @@ use App\Modules\Shipments\InpostShipmentService; use App\Modules\Shipments\ShipmentController; use App\Modules\Shipments\ShipmentPackageRepository; use App\Modules\Shipments\ShipmentPresetController; +use App\Modules\Shipments\DeliveryStatusMappingRepository; use App\Modules\Shipments\ShipmentPresetRepository; use App\Modules\Shipments\ShipmentProviderRegistry; use App\Modules\Printing\ApiKeyMiddleware; @@ -188,6 +190,13 @@ return static function (Application $app): void { (bool) $app->config('app.cron.run_on_web_default', false), (int) $app->config('app.cron.web_limit_default', 5) ); + $deliveryStatusMappingRepository = new DeliveryStatusMappingRepository($app->db()); + $deliveryStatusMappingController = new DeliveryStatusMappingController( + $template, + $translator, + $auth, + $deliveryStatusMappingRepository + ); $companySettingsRepository = new CompanySettingsRepository($app->db()); $companySettingsController = new CompanySettingsController( $template, @@ -389,6 +398,11 @@ return static function (Application $app): void { $router->post('/settings/automation/update', [$automationController, 'update'], [$authMiddleware]); $router->post('/settings/automation/delete', [$automationController, 'destroy'], [$authMiddleware]); $router->post('/settings/automation/toggle', [$automationController, 'toggleStatus'], [$authMiddleware]); + $router->get('/settings/delivery-status-mappings', [$deliveryStatusMappingController, 'index'], [$authMiddleware]); + $router->post('/settings/delivery-status-mappings/save', [$deliveryStatusMappingController, 'save'], [$authMiddleware]); + $router->post('/settings/delivery-status-mappings/save-bulk', [$deliveryStatusMappingController, 'saveBulk'], [$authMiddleware]); + $router->post('/settings/delivery-status-mappings/reset', [$deliveryStatusMappingController, 'reset'], [$authMiddleware]); + $router->post('/settings/delivery-status-mappings/reset-all', [$deliveryStatusMappingController, 'resetAll'], [$authMiddleware]); $router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]); $router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]); $router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]); diff --git a/src/Modules/Settings/DeliveryStatusMappingController.php b/src/Modules/Settings/DeliveryStatusMappingController.php new file mode 100644 index 0000000..3d7a2b7 --- /dev/null +++ b/src/Modules/Settings/DeliveryStatusMappingController.php @@ -0,0 +1,222 @@ + 'InPost', + 'apaczka' => 'Apaczka', + 'allegro_wza' => 'Allegro', + ]; + + public function __construct( + private readonly Template $template, + private readonly Translator $translator, + private readonly AuthService $auth, + private readonly DeliveryStatusMappingRepository $repository + ) { + } + + public function index(Request $request): Response + { + $provider = strtolower(trim((string) $request->input('provider', 'inpost'))); + if (!isset(self::PROVIDERS[$provider])) { + $provider = 'inpost'; + } + + $defaults = DeliveryStatus::getDefaultMappings($provider); + $overrides = $this->repository->listByProvider($provider); + + $overrideMap = []; + foreach ($overrides as $row) { + $overrideMap[$row['raw_status']] = $row; + } + + $mappings = []; + 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, + ]; + } + + $html = $this->template->render('settings/delivery-status-mappings', [ + 'title' => 'Mapowanie statusów dostawy', + 'activeMenu' => 'settings', + 'activeSettings' => 'delivery-status-mappings', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'provider' => $provider, + 'providers' => self::PROVIDERS, + 'mappings' => $mappings, + 'normalizedOptions' => DeliveryStatus::LABEL_PL, + 'errorMessage' => (string) Flash::get('dsm_error', ''), + 'successMessage' => (string) Flash::get('dsm_success', ''), + ], 'layouts/app'); + + return Response::html($html); + } + + public function save(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $provider = strtolower(trim((string) $request->input('provider', ''))); + $rawStatus = trim((string) $request->input('raw_status', '')); + $normalizedStatus = trim((string) $request->input('normalized_status', '')); + $description = trim((string) $request->input('description', '')); + + if ($provider === '' || $rawStatus === '') { + Flash::set('dsm_error', 'Brakuje wymaganych pól.'); + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) { + Flash::set('dsm_error', 'Nieprawidłowy status znormalizowany.'); + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + try { + $this->repository->upsertMapping($provider, $rawStatus, $normalizedStatus, $description); + Flash::set('dsm_success', 'Mapowanie zapisane.'); + } catch (Throwable $exception) { + Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + public function saveBulk(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $provider = strtolower(trim((string) $request->input('provider', ''))); + $rawStatuses = $request->input('raw_status', []); + $normalizedStatuses = $request->input('normalized_status', []); + $descriptions = $request->input('description', []); + + 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)); + } + + try { + $changed = 0; + $defaults = DeliveryStatus::getDefaultMappings($provider); + + foreach ($rawStatuses as $index => $rawStatus) { + $rawStatus = trim((string) $rawStatus); + if ($rawStatus === '') { + continue; + } + + $normalizedStatus = trim((string) ($normalizedStatuses[$index] ?? '')); + $description = trim((string) ($descriptions[$index] ?? '')); + + if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) { + continue; + } + + $default = $defaults[$rawStatus] ?? null; + $isDefault = $default !== null + && $normalizedStatus === $default['normalized'] + && $description === $default['description']; + + if ($isDefault) { + $this->repository->deleteMapping($provider, $rawStatus); + } else { + $this->repository->upsertMapping($provider, $rawStatus, $normalizedStatus, $description); + $changed++; + } + } + + Flash::set('dsm_success', 'Zapisano zmiany (' . $changed . ' niestandardowych mapowań).'); + } catch (Throwable $exception) { + Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + public function reset(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $provider = strtolower(trim((string) $request->input('provider', ''))); + $rawStatus = trim((string) $request->input('raw_status', '')); + + if ($provider === '' || $rawStatus === '') { + Flash::set('dsm_error', 'Brakuje wymaganych pól.'); + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + try { + $this->repository->deleteMapping($provider, $rawStatus); + Flash::set('dsm_success', 'Przywrócono domyślne mapowanie.'); + } catch (Throwable $exception) { + Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + public function resetAll(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $provider = strtolower(trim((string) $request->input('provider', ''))); + if ($provider === '') { + Flash::set('dsm_error', 'Brakuje wymaganych pól.'); + return Response::redirect(self::REDIRECT_PATH); + } + + try { + $this->repository->deleteAllByProvider($provider); + Flash::set('dsm_success', 'Przywrócono wszystkie domyślne mapowania.'); + } catch (Throwable $exception) { + Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + } + + private function validateCsrf(string $token): ?Response + { + if (Csrf::validate($token)) { + return null; + } + + Flash::set('dsm_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect(self::REDIRECT_PATH); + } +} diff --git a/src/Modules/Shipments/DeliveryStatus.php b/src/Modules/Shipments/DeliveryStatus.php index 32ea84e..721956a 100644 --- a/src/Modules/Shipments/DeliveryStatus.php +++ b/src/Modules/Shipments/DeliveryStatus.php @@ -191,6 +191,76 @@ final class DeliveryStatus 'RETURNED' => 'Zwrócona do nadawcy', ]; + public const ALL_STATUSES = [ + self::UNKNOWN, + self::CREATED, + self::CONFIRMED, + self::IN_TRANSIT, + self::OUT_FOR_DELIVERY, + self::READY_FOR_PICKUP, + self::DELIVERED, + self::RETURNED, + self::CANCELLED, + self::PROBLEM, + ]; + + private const PROVIDER_MAPS = [ + 'inpost' => self::INPOST_MAP, + 'apaczka' => self::APACZKA_MAP, + 'allegro_wza' => self::ALLEGRO_MAP, + ]; + + private const PROVIDER_DESCRIPTIONS = [ + 'inpost' => self::INPOST_DESCRIPTIONS, + 'apaczka' => self::APACZKA_DESCRIPTIONS, + 'allegro_wza' => self::ALLEGRO_DESCRIPTIONS, + ]; + + /** + * @return array + */ + public static function getDefaultMappings(string $provider): array + { + $map = self::PROVIDER_MAPS[$provider] ?? []; + $descriptions = self::PROVIDER_DESCRIPTIONS[$provider] ?? []; + + $result = []; + foreach ($map as $rawStatus => $normalized) { + $result[(string) $rawStatus] = [ + 'normalized' => $normalized, + 'description' => (string) ($descriptions[$rawStatus] ?? (string) $rawStatus), + ]; + } + + return $result; + } + + /** + * @param array $overrides keyed by "provider:raw_status" + */ + public static function normalizeWithOverrides(string $provider, string $rawStatus, array $overrides): string + { + $key = $provider . ':' . $rawStatus; + if (isset($overrides[$key]) && $overrides[$key]['normalized_status'] !== '') { + return $overrides[$key]['normalized_status']; + } + + return self::normalize($provider, $rawStatus); + } + + /** + * @param array $overrides keyed by "provider:raw_status" + */ + public static function descriptionWithOverrides(string $provider, string $rawStatus, array $overrides): string + { + $key = $provider . ':' . $rawStatus; + if (isset($overrides[$key]) && $overrides[$key]['description'] !== '') { + return $overrides[$key]['description']; + } + + return self::description($provider, $rawStatus); + } + public static function normalize(string $provider, string $rawStatus): string { $map = match ($provider) { diff --git a/src/Modules/Shipments/DeliveryStatusMappingRepository.php b/src/Modules/Shipments/DeliveryStatusMappingRepository.php new file mode 100644 index 0000000..ce593ea --- /dev/null +++ b/src/Modules/Shipments/DeliveryStatusMappingRepository.php @@ -0,0 +1,124 @@ + + */ + public function listByProvider(string $provider): array + { + $provider = strtolower(trim($provider)); + if ($provider === '') { + return []; + } + + $statement = $this->pdo->prepare( + 'SELECT provider, raw_status, normalized_status, description + FROM delivery_status_mappings + WHERE provider = :provider + ORDER BY raw_status ASC' + ); + $statement->execute(['provider' => $provider]); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + if (!is_array($rows)) { + return []; + } + + return array_map(static function (array $row): array { + return [ + 'provider' => (string) ($row['provider'] ?? ''), + 'raw_status' => (string) ($row['raw_status'] ?? ''), + 'normalized_status' => (string) ($row['normalized_status'] ?? ''), + 'description' => (string) ($row['description'] ?? ''), + ]; + }, $rows); + } + + public function upsertMapping(string $provider, string $rawStatus, string $normalizedStatus, string $description): void + { + $provider = strtolower(trim($provider)); + $rawStatus = trim($rawStatus); + if ($provider === '' || $rawStatus === '') { + return; + } + + $statement = $this->pdo->prepare( + 'INSERT INTO delivery_status_mappings (provider, raw_status, normalized_status, description, created_at, updated_at) + VALUES (:provider, :raw_status, :normalized_status, :description, NOW(), NOW()) + ON DUPLICATE KEY UPDATE + normalized_status = VALUES(normalized_status), + description = VALUES(description), + updated_at = NOW()' + ); + $statement->execute([ + 'provider' => $provider, + 'raw_status' => $rawStatus, + 'normalized_status' => $normalizedStatus, + 'description' => $description, + ]); + } + + public function deleteMapping(string $provider, string $rawStatus): void + { + $provider = strtolower(trim($provider)); + $rawStatus = trim($rawStatus); + if ($provider === '' || $rawStatus === '') { + return; + } + + $statement = $this->pdo->prepare( + 'DELETE FROM delivery_status_mappings WHERE provider = :provider AND raw_status = :raw_status' + ); + $statement->execute([ + 'provider' => $provider, + 'raw_status' => $rawStatus, + ]); + } + + public function deleteAllByProvider(string $provider): void + { + $provider = strtolower(trim($provider)); + if ($provider === '') { + return; + } + + $statement = $this->pdo->prepare( + 'DELETE FROM delivery_status_mappings WHERE provider = :provider' + ); + $statement->execute(['provider' => $provider]); + } + + /** + * @return array + */ + public function getAllOverrides(): array + { + $statement = $this->pdo->query( + 'SELECT provider, raw_status, normalized_status, description FROM delivery_status_mappings' + ); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + if (!is_array($rows)) { + return []; + } + + $result = []; + foreach ($rows as $row) { + $key = ((string) ($row['provider'] ?? '')) . ':' . ((string) ($row['raw_status'] ?? '')); + $result[$key] = [ + 'normalized_status' => (string) ($row['normalized_status'] ?? ''), + 'description' => (string) ($row['description'] ?? ''), + ]; + } + + return $result; + } +}
Konfiguracja przypisania surowych statusów z API przewoźników do znormalizowanych statusów w aplikacji.
Brak mapowań dla tego przewoźnika.
= $e($rawStatus) ?>