This commit is contained in:
2026-05-19 00:40:34 +02:00
parent 9ea26ad610
commit cff0635aff
141 changed files with 90329 additions and 5633 deletions

View File

@@ -133,7 +133,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
- [x] Tracking i automatyzacje Erli: lokalny provider tracking jak w Allegro, retry niekrytycznej rejestracji paczki zewnetrznej Erli z `shipment_tracking_sync`, wspolny kontekst `shipment.created`/`shipment.status_changed` dla regul e-mail/SMS/statystyk — Phase 131 - [x] Tracking i automatyzacje Erli: lokalny provider tracking jak w Allegro, retry niekrytycznej rejestracji paczki zewnetrznej Erli z `shipment_tracking_sync`, wspolny kontekst `shipment.created`/`shipment.status_changed` dla regul e-mail/SMS/statystyk — Phase 131
- [x] Hardening Erli: spojna diagnostyka importu/ACK w `integration_order_sync_state.last_error`, brak ACK po blednym batchu, testy jednostkowe import/status sync i dokumentacja obserwowalnosci bez nowej migracji — Phase 132 - [x] Hardening Erli: spojna diagnostyka importu/ACK w `integration_order_sync_state.last_error`, brak ACK po blednym batchu, testy jednostkowe import/status sync i dokumentacja obserwowalnosci bez nowej migracji — Phase 132
- [x] Parytet Erli w powierzchniach wspolnych: filtr zrodla zamowien, kanaly statystyk dziennych/podsumowania, warunek integracji automatyzacji, menu integracji i etykiety `zrodlo` uzywaja wspolnego rejestru zrodel — Phase 133 - [x] Parytet Erli w powierzchniach wspolnych: filtr zrodla zamowien, kanaly statystyk dziennych/podsumowania, warunek integracji automatyzacji, menu integracji i etykiety `zrodlo` uzywaja wspolnego rejestru zrodel — Phase 133
- [x] Backlog Reality Check: `.paul/codebase/todo.md` i `.paul/codebase/concerns.md` sklasyfikowane przeciw aktualnemu kodowi/docs, z dowodami w `BACKLOG-AUDIT.md` i pierwotnym routingiem do kolejnych faz dlugu; nieaktualne fazy 140+ usuniete 2026-05-18 decyzja operatora — Phase 134 - [x] Backlog Reality Check: reczne wpisy z `.paul/codebase/todo.md` i dawny raport ryzyk sklasyfikowane przeciw aktualnemu kodowi/docs, z dowodami w `BACKLOG-AUDIT.md` i pierwotnym routingiem do kolejnych faz dlugu; nieaktualne fazy 140+ usuniete 2026-05-18 decyzja operatora — Phase 134
- [x] Accounting Net Correctness: nowe paragony zapisuja VAT-aware `receipts.total_net`, a statystyki dzienne preferuja source-level net, potem `order_items` VAT fallback, z gross `/1.23` tylko jako legacy fallback — Phase 135 - [x] Accounting Net Correctness: nowe paragony zapisuja VAT-aware `receipts.total_net`, a statystyki dzienne preferuja source-level net, potem `order_items` VAT fallback, z gross `/1.23` tylko jako legacy fallback — Phase 135
- [x] Fakturownia Invoice Idempotency: delegowane faktury uzywaja stabilnego `oid=orders.internal_order_number`, lookup-first `GET /invoices.json?oid=...`, lokalnego stanu `pending_external`/`failed_retryable` i auto-attach po timeoutach — Phase 136 - [x] Fakturownia Invoice Idempotency: delegowane faktury uzywaja stabilnego `oid=orders.internal_order_number`, lookup-first `GET /invoices.json?oid=...`, lokalnego stanu `pending_external`/`failed_retryable` i auto-attach po timeoutach — Phase 136
- [x] Delivery Status Backlog Verification: `DELIVERY-STATUS-MGMT` zamkniete jako wdrozone; runtime korzysta z DB-driven statusow, a read-only DB check nie wykazal starych ani niepoprawnych kluczy automatyzacji — Phase 137 - [x] Delivery Status Backlog Verification: `DELIVERY-STATUS-MGMT` zamkniete jako wdrozone; runtime korzysta z DB-driven statusow, a read-only DB check nie wykazal starych ani niepoprawnych kluczy automatyzacji — Phase 137
@@ -325,9 +325,10 @@ Quick Reference:
- /code-review → Przegląd kodu przed UNIFY (optional) - /code-review → Przegląd kodu przed UNIFY (optional)
- /frontend-design → Komponenty UI i widoki (optional) - /frontend-design → Komponenty UI i widoki (optional)
- /simplify → Refaktoryzacja po implementacji (optional) - /simplify → Refaktoryzacja po implementacji (optional)
- SonarQube / `sonar-scanner` → reczny skan na zadanie operatora; nie jest wymagany w PLAN/APPLY/UNIFY
--- ---
*PROJECT.md — Updated when requirements or context change* *PROJECT.md — Updated when requirements or context change*
*Last updated: 2026-05-18 after Phase 145 closure* *Last updated: 2026-05-18 after Sonar workflow policy update*

View File

@@ -1,707 +0,0 @@
# Roadmap: orderPRO
## Overview
orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechodzi od podstawowych integracji z marketplace'ami i generowania etykiet, przez rozbudowe o nowe zrodla zamowien i przewoznikow, az do pelnego zarzadzania produktami i stanami magazynowymi.
## Current Milestone
v3.14 Polkurier COD Return Time Hotfix - Complete
Pilny hotfix tworzenia przesylek pobraniowych Polkurier: API odrzuca `create_order`, bo payload wysyla bledna wartosc czasu zwrotu pobrania. `codtype` ma uzywac kodu terminu (`S`, `1D`, `4D`, `16D`), a `return_cod` kodu sposobu zwrotu (`BA`, `PO`, `MB`).
Progress: 1 of 1 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 145 | Polkurier COD Return Time Hotfix | 1/1 | Complete (2026-05-18; PHPUnit/Sonar/live smoke follow-up pending) |
### Phase 145: Polkurier COD Return Time Hotfix
Focus: Naprawic payload COD w `PolkurierShipmentService`, aby przesylki pobraniowe wysylaly `codtype='S'` jako standardowy termin zwrotu pobrania oraz `return_cod='BA'` jako przelew na konto bankowe. Brak zmian DB i brak UI konfiguracji terminow w tym hotfixie.
Plans: 145-01 (complete; `.paul/phases/145-polkurier-cod-return-time-hotfix/145-01-SUMMARY.md`)
## Previous Milestone
v3.13 Imported Notes Badge Count Hotfix - Complete
Pilny hotfix dla listy zamowien: notatki zaimportowane ze zrodla, np. shopPRO, maja byc zliczane razem z notatkami autorskimi w badge `[N]` przy numerze zamowienia.
Progress: 1 of 1 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 144 | Imported Notes Badge Count Hotfix | 1/1 | Complete (2026-05-18; PHPUnit/Sonar env gaps documented) |
### Phase 144: Imported Notes Badge Count Hotfix
Focus: Zmienic licznik badge notatek na `/orders/list`, aby uzywal wszystkich rekordow `order_notes` dla zamowienia, a nie tylko `note_type='user'`. Zamowienie `1034` z importowana notatka shopPRO powinno pokazac cyfre na liscie.
Plans: 144-01 (complete; `.paul/phases/144-imported-notes-badge-count/144-01-SUMMARY.md`)
## Earlier Milestone
v3.12 Orders List Sidebar UI Hotfix - Complete
Maly hotfix UX dla ekranu operacyjnego: usuniecie opisowego boksu z listy zamowien oraz likwidacja widocznego "pokaz i schowaj" przy odswiezaniu strony ze zwinietym sidebarem.
Progress: 1 of 1 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 143 | Orders List Sidebar UI Hotfix | 1/1 | Complete (2026-05-18; manual UI/Sonar follow-up pending) |
### Phase 143: Orders List Sidebar UI Hotfix
Focus: Usunac boks "Zamowienia / Kompaktowa lista zamowien oparta o lokalna baze orderPRO." z `/orders/list` i zastosowac zapisany stan zwinietego sidebaru przed pierwszym renderem strony.
Plans: 143-01 (complete; `.paul/phases/143-orders-list-sidebar-ui-hotfix/143-01-SUMMARY.md`)
## Earlier Milestone
v3.11 Polkurier Shipment Prepare Hotfix - Complete
Pilny hotfix po Phase 140: mapowanie shopPRO -> Polkurier zapisuje sie poprawnie, ale formularz `/orders/{id}/shipment/prepare` nie podstawia providera i uslugi Polkuriera.
Progress: 1 of 1 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 142 | Polkurier Shipment Prepare Prefill | 1/1 | Complete (2026-05-18; PHPUnit/Sonar/manual live smoke follow-up pending) |
### Phase 142: Polkurier Shipment Prepare Prefill
Focus: Naprawic preselect w formularzu przygotowania przesylki, aby `carrier_delivery_method_mappings.provider='polkurier'` wybieral przewoznika Polkurier, zaznaczal zapisana usluge i ustawial hidden fields wymagane przez `PolkurierShipmentService`.
Plans: 142-01 (complete; `.paul/phases/142-polkurier-shipment-prepare-prefill/142-01-SUMMARY.md`)
## Earlier Milestone
v3.10 Integrations UI Polish - Complete
Maly milestone porzadkujacy ekran `/settings/integrations`, aby rosnaca liczba integracji byla latwiejsza do skanowania bez zmiany backendowych kontraktow providerow.
Progress: 1 of 1 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 141 | Integrations Hub Grouped Sections | 1/1 | Complete (2026-05-18; manual UI/Sonar follow-up pending) |
### Phase 141: Integrations Hub Grouped Sections
Focus: Podzielic `/settings/integrations` na lekkie sekcje/kafelki: marketplace (Allegro Sandbox, Allegro Production, shopPRO, Erli), kurierzy (InPost, Apaczka, polkurier.pl) i pozostale (Fakturownia, HostedSMS, SMSPLANET), usuwajac niepotrzebny naglowek/opis "Wspolny panel konfiguracji wszystkich providerow."
Plans: 141-01 (complete; `.paul/phases/141-integrations-hub-grouped-sections/141-01-SUMMARY.md`)
## Earlier Milestone
v3.9 Stabilizacja i splata dlugu technicznego - Complete
Milestone porzadkujacy zbudowany z `.paul/codebase/todo.md` i `.paul/codebase/concerns.md`: poprawa znanych bugow, weryfikacja stalych ryzyk, domkniecie security/performance oraz ograniczenie dlugu technicznego, ktory utrudnia kolejne wdrozenia.
Progress: 7 of 7 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 134 | Backlog Reality Check | 1/1 | Complete (2026-05-16; documentation-only audit, Sonar CLI gap documented) |
| 135 | Accounting Net Correctness | 1/1 | Complete (2026-05-16; VAT-aware receipt/stat net, PHPUnit/Sonar env gaps documented) |
| 136 | Fakturownia Invoice Idempotency | 1/1 | Complete (2026-05-17; Fakturownia oid idempotency, migration/PHPUnit/Sonar env gaps documented) |
| 137 | Delivery Status Backlog Verification | 1/1 | Complete (2026-05-17; verification-only closure, no stale automation keys found) |
| 138 | Security and Legacy Hardening | 1/1 | Complete (2026-05-17; SMTP TLS/template/session/view hardening, PHPUnit/Sonar env gaps documented) |
| 139 | Sonar Critical/Major Cleanup | 2/2 | Complete (2026-05-17; Sonar BLOCKER/CRITICAL/MAJOR reduced 648 -> 495 across two cleanup slices) |
| 140 | shopPRO Polkurier Delivery Mapping | 1/1 | Complete (2026-05-18; shopPRO delivery mapping supports Polkurier, manual UI/Sonar follow-up pending) |
### Phase 134: Backlog Reality Check
Focus: Zweryfikowac wszystkie wpisy z `.paul/codebase/todo.md` i `.paul/codebase/concerns.md` przeciw aktualnemu kodowi oraz dokumentacji. Dla kazdego wpisu oznaczyc: nadal aktywne, juz wdrozone, nieaktualne albo wymaga decyzji operatora. Wynik ma stac sie wejsciem do planow kolejnych faz.
Plans: 134-01 (complete; `.paul/phases/134-backlog-reality-check/134-01-SUMMARY.md`)
### Phase 135: Accounting Net Correctness
Focus: Poprawic znane rozbieznosci kwot netto: `RECEIPT-NET-FIX` dla `receipts.total_net` oraz `STAT-NET` dla statystyk zamowien bez stalego zalozenia 23% VAT. Zakres obejmuje ustalenie zrodla prawdy, ewentualny backfill i testy eksportow/statystyk.
Plans: 135-01 (complete; `.paul/phases/135-accounting-net-correctness/135-01-SUMMARY.md`)
### Phase 136: Fakturownia Invoice Idempotency
Focus: Domknac `INVOICE-IDEMP-115`: zabezpieczyc delegowane wystawianie faktur przed podwojnym POST do Fakturowni po timeoutach lub utracie odpowiedzi, z weryfikacja mozliwosci `Idempotency-Key` albo deduplikacji po referencji.
Plans: 136-01 (complete; `.paul/phases/136-fakturownia-invoice-idempotency/136-01-SUMMARY.md`)
### Phase 137: Delivery Status Backlog Verification
Focus: Zweryfikowac wpis `DELIVERY-STATUS-MGMT` z todo oraz breaking changes po Phase 108: statusy DB-driven, stare klucze grup statusow, usuniecie `SHIPMENT_STATUS_OPTION_MAP` i realny wplyw na reguly automatyzacji. Jezeli funkcjonalnosc jest juz wdrozona, zamknac/oczyscic backlog i zostawic tylko potwierdzone luki.
Plans: 137-01 (complete; `.paul/phases/137-delivery-status-backlog-verification/137-01-SUMMARY.md`)
### Phase 138: Security and Legacy Hardening
Focus: Sprawdzic i naprawic po potwierdzeniu: szyfrowanie `print_api_keys.api_key`, `fsockopen('ssl://...')` w tescie skrzynki e-mail, injection przez zmienne szablonow, brakujacy import `RuntimeException`, stare `require` w widokach, raw `$_SESSION` i pozostale legacy patterns wskazane w concerns.
Plans: 138-01 (complete; `.paul/phases/138-security-and-legacy-hardening/138-01-SUMMARY.md`)
### Phase 139: Sonar Critical/Major Cleanup
Focus: Zmniejszyc potwierdzone problemy SonarQube z `concerns.md`: generic exceptions, zbyt wiele returnow, powtarzajace sie literaly, cognitive complexity, unused parameters, use-namespace-import oraz accessibility (`aria-label`, `<output>`). Przed kazda grupa zmian odswiezyc stan skanu albo lokalnie potwierdzic wystepowanie problemu.
Plans: 139-01 (complete; `.paul/phases/139-sonar-critical-major-cleanup/139-01-SUMMARY.md`); 139-02 (complete; `.paul/phases/139-sonar-critical-major-cleanup/139-02-SUMMARY.md`)
### Phase 140: shopPRO Polkurier Delivery Mapping
Focus: Dodac Polkurier do zakladki Dostawy w ustawieniach integracji shopPRO, aby formy dostawy z zamowien shopPRO mogly byc mapowane na lokalna usluge Polkurier i pozniej automatycznie preselectowane przy przygotowaniu przesylki.
Plans: 140-01 (complete; `.paul/phases/140-shoppro-polkurier-delivery-mapping/140-01-SUMMARY.md`)
## Earlier Milestone
v3.8 Erli Marketplace Integration - Complete in code
Pelna integracja z erli.pl wzorowana na istniejacej integracji Allegro: konfiguracja konta/API, pobieranie zamowien, mapowanie i synchronizacja statusow, generowanie etykiet, tracking oraz wlaczenie Erli w istniejace przeplywy automatyzacji, statystyk i obslugi zamowien.
Progress: 7 of 7 phases complete (100%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 127 | Erli Integration Foundation | 1/1 | Complete (2026-05-15; migration/manual Erli API smoke pending operator) |
| 128 | Erli Orders Import | 1/1 | Complete (2026-05-15; migration/manual Erli import smoke pending operator) |
| 129 | Erli Status Mapping + Sync | 1/1 | Complete (2026-05-16; migration/manual Erli status smoke pending operator) |
| 130 | Erli Shipments + Labels | 1/1 | Complete (2026-05-16; migration/manual Erli shipping smoke pending operator) |
| 131 | Erli Tracking + Automation Hooks | 1/1 | Complete (2026-05-16; manual Erli tracking/automation smoke pending operator) |
| 132 | Erli Hardening, Observability + Docs | 1/1 | Complete (2026-05-16; PHPUnit/Sonar env gaps documented) |
| 133 | Erli Cross-Surface Parity | 1/1 | Complete (2026-05-16; PHPUnit/Sonar env gaps documented) |
### Phase 127: Erli Integration Foundation
Focus: Dodac podstawowy typ integracji Erli: migracje konfiguracji, szyfrowanie sekretow, klient API, test polaczenia, karta w hubie integracji i routing/settings zgodne z wzorcami Allegro/shopPRO.
Plans: 127-01 (complete)
### Phase 128: Erli Orders Import
Focus: Pobieranie nowych zamowien Erli przez cron i import reczny, mapper do wspolnego modelu orderPRO, state cursor, delta-only re-import, adresy/pozycje/platnosci/notatki oraz flaga faktury/NIP tam, gdzie API Erli daje dane firmowe.
Plans: 128-01 (complete)
### Phase 129: Erli Status Mapping + Sync
Focus: Osobne mapowanie pull/push statusow Erli, auto-discovery nieznanych statusow, cron synchronizacji orderPRO -> Erli i ochrona lokalnych statusow przy re-imporcie analogicznie do Allegro/shopPRO.
Plans: 129-01 (complete)
### Phase 130: Erli Shipments + Labels
Focus: Generowanie etykiet dla zamowien Erli, mapowanie metod dostawy Erli na dostepne providery, zapis paczek w `shipment_packages`, pobieranie labeli i integracja z kolejka zdalnego druku.
Plans: 130-01 (complete)
### Phase 131: Erli Tracking + Automation Hooks
Focus: Tracking przesylek Erli, aktualizacja delivery statusow, zdarzenia automatyzacji (`order.imported`, `shipment.created`, `shipment.status_changed`) i zachowanie kompatybilnosci z szablonami e-mail/SMS oraz statystykami.
Plans: 131-01 (complete)
### Phase 132: Erli Hardening, Observability + Docs
Focus: Testy jednostkowe mapperow/klientow, logi integracji i bledow API, retry/idempotencja, manual smoke checklist na zywej konfiguracji oraz aktualizacja `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md`.
Plans: 132-01 (complete)
### Phase 133: Erli Cross-Surface Parity
Focus: Domknac Erli jako pelnoprawny kanal w istniejacych wspolnych powierzchniach: filtr `Zrodlo` na liscie zamowien, kanaly sprzedazy w `/statistics/orders` i `/statistics/summary`, warunki integracji w automatyzacjach oraz aktywne menu integracji. Wprowadzic maly wspolny rejestr zrodel, zeby ograniczyc kolejne lokalne pominiecia Erli.
Plans: 133-01 (complete)
## Earlier Recent Milestone (transition pending)
v3.7 Invoices (Fakturownia integration) — Complete in code, transition/follow-ups pending
Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakturownia.pl). Numeracja lokalna z opcja delegacji do Fakturowni, rozdzielenie przyciskow "Wystaw paragon" / "Wystaw fakture", osobne podstrony edycji konfiguracji paragonow i faktur.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 113 | Fakturownia Integration Foundation | 1/1 | Complete (2026-05-10) |
| 114 | Accounting Configs Refactor (hub + osobne podstrony receipts/invoices) | 1/1 | Complete (2026-05-10) |
| 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia + NIP lookup MF Biala Lista) | 1/1 | Complete (2026-05-10) |
| 116 | HostedSMS Integration Settings + Test SMS | 1/1 | Complete (2026-05-12) |
| 117 | SMSPLANET Integration Settings + Test SMS | 1/1 | Complete (2026-05-12; migration/manual SMS verification pending) |
| 118 | Fakturownia Single Instance | 1/1 | Complete (2026-05-12; migration/manual Fakturownia verification pending) |
| 119 | Re-import total_paid Protection | 1/1 | Complete (2026-05-12; phpunit run + manual shoppro smoke pending env) |
| 120 | Alert Component Unification | 1/1 | Complete (2026-05-12; CSS rebuilt; smoke tests pending operator) |
| 121 | SMSPLANET Conversation + Notifications | 1/1 | Complete (2026-05-12; live SMS/browser smoke pending operator) |
| 122 | SMSPLANET Default SMS Footer | 1/1 | Complete (2026-05-12; live SMS smoke + over-limit UI test pending operator) |
| 123 | Receipts Export VAT Breakdown | 1/1 | Complete (2026-05-12; manual XLSX smoke pending operator) |
| 124 | SMS Templates | 1/1 | Complete (2026-05-13; migration + manual SMS smoke pending operator) |
| 125 | invoice_requested Import Fix (shopPRO+Allegro NIP detection, drop legacy is_invoice column) | 1/1 | Complete (2026-05-13; migration + manual smoke pending operator) |
| 126 | Invoice GUS Field Mapping Fix (KRS-based heuristic: JDG → name do "Imię i nazwisko", spółka → "Nazwa firmy") | 1/1 | Complete (2026-05-13; manual smoke pending operator) |
| 127 | polkurier Integration Foundation (single-instance settings + Token API + realny test polaczenia; obok Apaczki) | 1/1 | Complete (2026-05-14; live API verified — `Autoryzacja: 1`) |
| 128 | polkurier ShipmentService + TrackingService + UI prepare panel + delivery_status_mappings seed (live test na #114/#115) | 1/1 | Complete (2026-05-14; live test passed po 4 iteracjach; migracja + cron tracking weryfikacja pending) |
| 129 | Order User Notes module (extend `order_notes` o user_id/author_name/note_type='user' + pelen CRUD restricted to author + badge `[N]` na liscie zamowien) | 1/1 | Complete (2026-05-14; migracja + manualny smoke pending operator) |
| 130 | polkurier delivery status mappings UI (hardcoded POLKURIER_MAP/DESCRIPTIONS + dropdown w `/settings/delivery-statuses?tab=mapping` + badge counter) | 1/1 | Complete (2026-05-14; manualny smoke pending operator) |
Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania):
- polkurier TrackingService + `delivery_status_mappings` (provider='polkurier')
- polkurier paczkomaty (`InpostParcelMachines` / `PocztexPostOffices` / `Kurier48PostOffices` z SDK polkuriera)
- Eksport XLSX listy wystawionych faktur (analogicznie do paragonow)
- Event automatyzacji `invoice.created` (jezeli operator chce wysylac faktury mailem)
- Automatyzacje SMS / odbior odpowiedzi SMS po aktywacji HostedSMS
- SMSPLANET conversation mode: wybor nadpis/numer 2WAY, odbior odpowiedzi, historia SMS w zamowieniu i notification center - Phase 121 planning
- Manualne potwierdzenie SMSPLANET na zywej bazie i danych produkcyjnych
- Backfill `curl_close()` w `ShopproIntegrationsRepository` (PHP 8.5 compat, poza zakresem 115)
## Next Milestone
Kandydaci w kolejce (po v3.8):
- Mobile Orders List / Mobile Order Details / Mobile Settings
- Zarzadzanie produktami
- Zarzadzanie stanami magazynowymi
- Historical receipt net backfill, only if operator later wants old `receipts.total_net` corrected
- Phase 68 — Code Deduplication Refactor
## Completed Milestones
<details>
<summary>v3.6 Re-import Data Protection - 2026-05-07 (1 phase, 1 plan)</summary>
Re-import istniejacego zamowienia (Allegro + shopPRO) jest delta-only: `replaceAddresses/Items/Notes` wywolywane wylacznie przy pierwszym imporcie, nowy `updateOrderDelta()` aktualizuje tylko pola realnie zmieniajace sie ze zrodla. Stabilne `order_items.id` chronia `project_generated` (Phase 97) i flow generowania PSD. Dodatkowo: propagacja anulowania ze zrodla (override niezalezny od statusOverwriteAllowed) i identical-payload no-op guard. Naprawa case #882 (znikajaca flaga "Projekt" po re-imporcie wymuszanym przez `payment.status_changed` z Phase 111).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 112 | Re-import Data Protection | 1/1 | Complete |
Archive: `.paul/phases/112-reimport-data-protection/`
</details>
<details>
<summary>v3.5 Payment Transition Event - 2026-05-05 (1 phase, 1 plan)</summary>
Naprawa luki w re-imporcie zamowien Allegro/shopPRO: po potwierdzeniu platnosci re-import emituje `payment.status_changed`, co przez chain reguly #7 zmienia status na `w_realizacji`. Eliminuje przypadki zamowien zaimportowanych przed potwierdzeniem platnosci utykajacych w `nieoplacone` (case #864).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 111 | Payment Transition Event | 1/1 | Complete |
Archive: `.paul/phases/111-payment-transition-event/`
</details>
<details>
<summary>v3.4 Statistics Summary - 2026-04-28 (1 phase, 1 plan)</summary>
Dodano pierwsza pozycje `Statystyki -> Podsumowanie` z miesiecznymi wykresami liczby i wartosci zamowien. Kazda integracja ma osobna serie, a dodatkowa seria `Razem` sumuje miesiac. Domyslny start historii to `04-2026`.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 110 | Statistics Summary | 1/1 | Complete |
Archive: `.paul/phases/110-statistics-summary/`
</details>
<details>
<summary>v3.3 UI Filters - 2026-04-28 (1 phase, 1 plan)</summary>
Usprawnienie wielokrotnego wyboru w filtrach: natywne selecty multiple na `/statistics/orders` zostaly zastapione kompaktowym dropdownem z checkboxami, bez zmiany kontraktu GET i backendu statystyk.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 109 | Checkbox Multiselect Filters | 1/1 | Complete |
Archive: `.paul/phases/109-checkbox-multiselect-filters/`
</details>
<details>
<summary>v3.2 Delivery Status Management - 2026-04-27 (1 phase, 2 plans)</summary>
Wyniesienie znormalizowanych statusow przesylek do tabeli DB z CRUD panelem oraz pelna integracja DB-driven w dropdownach automatyzacji.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 108 | Delivery Status Management | 2/2 | Complete |
Archive: `.paul/phases/108-delivery-status-management/`
</details>
<details>
<summary>v3.1 Operational Enhancements - 2026-04-27 (2 phases, 2 plans)</summary>
Usprawnienia operacyjne: alert o kliencie z historia zwrotow oraz idempotentna jednorazowa wysylka e-mail per zamowienie.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 106 | Customer Return Alert | 1/1 | Complete |
| 107 | Automation Email Send Once | 1/1 | Complete |
Archive: `.paul/phases/106-customer-return-alert/`, `.paul/phases/107-automation-email-send-once/`
</details>
<details>
<summary>v3.0 Mobile Responsive - 2026-04-19 (52 phases shipped, 55 plans)</summary>
Wersja mobilna aplikacji plus pelny zestaw usprawnien operacyjnych: automation events, tracking push/pull, personalizacja pozycji, resilient print pipeline, project generation PSD oraz raport statystyk zamowien.
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 52 | Mobile Main Menu | 1/1 | Complete |
| 53 | Mobile Status Panel Toggle | 1/1 | Complete |
| 54 | Order Detail Image Hover | 1/1 | Complete |
| 55 | Desktop Collapsed Sidebar Fix | 1/1 | Complete |
| 56 | Order Payments | 1/1 | Complete |
| 57 | Payment Automation Event | 1/1 | Complete |
| 58 | Automation Form Preserve | 1/1 | Complete |
| 59 | Order Status Automation Event | 1/1 | Complete |
| 60 | Order Status Aged Event | 1/1 | Complete |
| 61 | Payment Button Activation | 1/1 | Complete |
| 62 | Import Re-import Safety | 1/1 | Complete |
| 63 | Order Item Personalization | 1/1 | Complete |
| 64 | Receipt Datetime Precision | 1/1 | Complete |
| 65 | PAUL Delegated Apply | 1/1 | Complete |
| 66 | Allegro Delivery Tracking | 2/2 | Complete |
| 67 | PAUL Codex Executor | 1/1 | Complete |
| 68 | Code Deduplication Refactor | 0/2 | Deferred |
| 69 | Allegro Tracking English Statuses | 1/1 | Complete |
| 70 | Receipt Shipping Cost | 1/1 | Complete |
| 71 | Attributes Import | 1/1 | Complete |
| 72 | Per Page Persistence | 1/1 | Complete |
| 73 | Search by Product | 1/1 | Complete |
| 74 | Reverse Status Mapping | 1/1 | Complete |
| 75 | Pull Status Mapping | 1/1 | Complete |
| 76 | Shipment Receiver Fallback | 1/1 | Complete |
| 77 | COD Amount Fix | 1/1 | Complete |
| 78 | Preset Auto Submit | 1/1 | Complete |
| 79 | Personalization Message Field | 1/1 | Complete |
| 80 | Status Change Reload | 1/1 | Complete |
| 81 | Global Search Bar | 1/1 | Complete |
| 82 | Product Title Tooltip | 1/1 | Complete |
| 83 | Allegro Pull Status Mapping | 1/1 | Complete |
| 84 | Order Imported Automation Event | 1/1 | Complete |
| 85 | Status Group Filter | 1/1 | Complete |
| 86 | Apaczka COD Bank Account | 1/1 | Complete |
| 87 | Shipment Delete | 1/1 | Complete |
| 88 | Allegro User-Agent | 1/1 | Complete |
| 89 | Allegro Info Page | 1/1 | Complete |
| 90 | Delivery Price Import Fix | 1/1 | Complete |
| 91 | Print Client Timeout Resilience | 1/1 | Complete |
| 92 | Buyer Name Copy | 1/1 | Complete |
| 93 | Remember Me Login | 1/1 | Complete |
| 94 | Order Preview Popup | 1/1 | Complete |
| 95 | AJAX Table Refresh | 1/1 | Complete |
| 96 | Automation Payment Method Condition | 1/1 | Complete |
| 97 | Project Generation | 1/1 | Complete |
| 98 | Order Imported First Only | 1/1 | Complete |
| 99 | Order Delivery & Payment Edit | 0/1 | Cancelled |
| 100 | Preset Scope & Print UX | 1/1 | Complete |
| 101 | Aged Orders Row Highlight | 1/1 | Complete |
| 102 | Apaczka Receiver Street Length | 1/1 | Complete |
| 103 | Print Autoclick Fix | 1/1 | Complete |
| 104 | Apaczka Weekend Delivery | 1/1 | Complete |
| 105 | Orders Statistics | 1/1 | Complete |
Archive: `.paul/milestones/v3.0-ROADMAP.md`
</details>
<details>
<summary>v2.3 Email HTML Layout - 2026-03-28 (1 phase, 1 plan)</summary>
HTML header/footer per skrzynka pocztowa z dual-mode edytorem (Quill WYSIWYG + HTML source) i kompozycja email header+body+footer.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 51 | Email HTML Layout | 1/1 | 2026-03-28 |
Archive: `.paul/phases/51-email-html-layout/`
</details>
<details>
<summary>v2.2 Allegro Shipment Waybill Push - 2026-03-28 (1 phase, 1 plan)</summary>
Automatyczne przekazywanie waybilla do Allegro checkout forms przy tworzeniu przesylki, ograniczone do zamowien `source=allegro` i odporne na bledy API Allegro.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 50 | Allegro Shipment Waybill Push | 1/1 | 2026-03-28 |
Archive: `.paul/phases/50-allegro-shipment-waybill-push/`
</details>
<details>
<summary>v2.1 Automation History & Observability - 2026-03-28 (1 phase, 1 plan)</summary>
Rozdzielenie Ustawienia > Zadania automatyczne na taby Ustawienia i Historia, wdrozenie audytu wykonan regul (filtry + paginacja), retencja 30 dni oraz akcja update_order_status.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 49 | Automation History Tab | 1/1 | 2026-03-28 |
Archive: .paul/phases/49-automation-history-tab/
</details>
<details>
<summary>v2.0 Email Template Shipment Variables - 2026-03-28 (1 phase, 1 plan)</summary>
Rozszerzenie szablonow e-mail o zmienne przesylki (`przesylka.numer`, `przesylka.link_sledzenia`) oraz provider-aware budowanie linku sledzenia.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 48 | Email Template Shipment Variables | 1/1 | 2026-03-28 |
Archive: `.paul/phases/48-email-template-shipment-variables/`
</details>
<details>
<summary>v1.9 Shipment Automation Immediate Trigger - 2026-03-28 (1 phase, 1 plan)</summary>
Wdrozenie natychmiastowego eventu automatyzacji po utworzeniu przesylki oraz nowej akcji automatyzacji do zmiany statusu przesylki.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 47 | Shipment Creation Automation | 1/1 | 2026-03-28 |
Archive: `.paul/phases/47-shipment-created-automation/`
</details>
<details>
<summary>v1.8 Allegro Status Push - 2026-03-28 (1 phase, 1 plan)</summary>
Wdrozenie synchronizacji statusow zamowien w kierunku orderPRO -> Allegro oraz aktywacja opcji kierunku w ustawieniach integracji Allegro.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 46 | Allegro Status Push | 1/1 | 2026-03-28 |
Archive: `.paul/phases/46-allegro-status-push/`
</details>
<details>
<summary>v1.7 ShopPRO Status Push - 2026-03-27 (1 phase, 1 plan)</summary>
Implementacja synchronizacji statusow zamowien w kierunku orderPRO -> shopPRO. Cron pushuje zmiany statusow do shopPRO API (PUT /api.php?endpoint=orders&action=change_status).
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 45 | ShopPRO Status Push | 1/1 | 2026-03-27 |
Archive: `.paul/phases/45-shoppro-status-push/`
</details>
<details>
<summary>v1.6 Quick Status Change - 2026-03-27 (1 phase, 1 plan)</summary>
Szybka zmiana statusu zamówienia bezpośrednio z listy zamówień — klikalny dropdown w kolumnie statusu, zmiana przez AJAX bez przeładowania strony.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 44 | Inline Status Change | 1/1 | 2026-03-27 |
Archive: `.paul/phases/44-inline-status-change/`
</details>
<details>
<summary>v1.5 Operational Workflow Cleanup - 2026-03-25 (4 phases, 4 plans)</summary>
Usprawnienia operacyjne: usunięcie bulk print, ograniczenie szumu logów importu Allegro, automatyzacja shipment.status_changed, usuwanie wpisów z kolejki druku.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 40 | Remove Order List Bulk Print | 1/1 | 2026-03-25 |
| 41 | Allegro Import Log Rationalization | 1/1 | 2026-03-25 |
| 42 | Automation Shipment Status Event | 1/1 | 2026-03-25 |
| 43 | Print Queue Entry Removal | 1/1 | 2026-03-25 |
Archive: `.paul/phases/40-*`, `.paul/phases/41-*`, `.paul/phases/42-*`, `.paul/phases/43-*`
</details>
<details>
<summary>v1.4 UI Readability Tweaks - 2026-03-25 (1 phase, 1 plan)</summary>
Rozdzielenie semantyki kolorow UI: glowny kolor przyciskow akcji zostal oddzielony od koloru naglowkow sekcji, aby poprawic czytelnosc i szybkosc skanowania interfejsu.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 30 | Button Primary Color Distinction | 1/1 | 2026-03-25 |
Archive: `.paul/phases/30-button-primary-color/`
</details>
<details>
<summary>v1.3 Konfiguracja Ĺledzenia przesyĹek — 2026-03-23 (1 phase, 1 plan)</summary>
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/`
</details>
<details>
<summary>v1.2 Ĺšledzenie przesyĹek — 2026-03-23 (2 phases, 2 plans)</summary>
Automatyczne Ĺledzenie statusu dostawy przesyĹek przez API przewoĹşnikĂłw (InPost ShipX, Apaczka, Allegro WZA). Cykliczne odpytywanie przez cron z konfigurowalnym interwaĹem. Dwupoziomowy system statusĂłw: znormalizowany + surowy z API. Badge'e w UI, linki Ĺledzenia, ustawienia interwaĹu.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 27 | Shipment Tracking Backend | 1/1 | 2026-03-23 |
| 28 | Shipment Tracking UI + Settings | 1/1 | 2026-03-23 |
Archive: `.paul/phases/27-shipment-tracking-backend/`, `.paul/phases/28-shipment-tracking-ui/`
</details>
<details>
<summary>v1.1 RÄ™czny numer przesyĹki — 2026-03-23 (1 phase, 1 plan)</summary>
MoĹĽliwoĹć rÄ™cznego dodania numeru Ĺledzenia przesyĹki do zamĂłwienia (bez tworzenia przesyĹki przez API przewoĹşnika).
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 26 | Manual Tracking Number | 1/1 | 2026-03-23 |
Archive: `.paul/phases/26-manual-tracking-number/`
</details>
<details>
<summary>v1.0 Presety przesyĹek — 2026-03-22 (3 phases, 3 plans)</summary>
Customowe przyciski szybkiego wypeĹniania formularza przygotowania przesyĹki. Presety globalne z nazwÄ… i kolorem — tworzenie, autofill, edycja, usuwanie.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 23 | Shipment Presets Backend | 1/1 | 2026-03-22 |
| 24 | Shipment Presets UI | 1/1 | 2026-03-22 |
| 25 | Shipment Presets Management | 1/1 | 2026-03-22 |
Archive: `.paul/phases/23-shipment-presets-backend/`, `.paul/phases/24-shipment-presets-ui/`, `.paul/phases/25-shipment-presets-management/`
</details>
<details>
<summary>v0.9 Poprawki ustawień firmy — 2026-03-22 (1 phase, 1 plan)</summary>
Naprawa buga: pola REGON, BDO, KRS i logo nie zapisywaĹy siÄ™ w ustawieniach firmy (kontroler nie przekazywaĹ ich do repozytorium).
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 22 | REGON Save Fix | 1/1 | 2026-03-22 |
Archive: `.paul/phases/22-regon-save-fix/`
</details>
<details>
<summary>v0.8 Poprawki wyĹwietlania ĹşrĂłdĹa zamĂłwieĹ„ — 2026-03-22 (1 phase, 1 plan)</summary>
Na liĹcie zamĂłwieĹ„ i stronie szczegĂłĹĂłw: wyĹwietlanie nazwy konkretnej integracji (z tabeli `integrations`) zamiast generycznego "shopPRO". Korekta kolejnoĹci source/ID.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 21 | Order Source Display | 1/1 | 2026-03-22 |
Archive: `.paul/phases/21-order-source-display/`
</details>
<details>
<summary>v0.7 Zdalne drukowanie etykiet — 2026-03-22 (3 phases, 3 plans)</summary>
System zdalnego drukowania etykiet przesyĹek na drukarce termicznej. Aplikacja Windows w system tray odpytuje API orderPRO, pobiera zlecenia i drukuje etykiety A6.
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 18 | Print Queue Backend | 1/1 | 2026-03-22 |
| 19 | UI Integration | 1/1 | 2026-03-22 |
| 20 | Windows Client (C# WinForms) | 1/1 | 2026-03-22 |
Archive: `.paul/phases/18-print-queue-backend/`, `.paul/phases/19-ui-integration/`, `.paul/phases/20-windows-client/`
</details>
<details>
<summary>v0.6 Poprawki UX — 2026-03-22 (1 phase, 1 plan)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 17 | Receipt duplicate guard | 1/1 | 2026-03-22 |
Archive: `.paul/phases/17-receipt-duplicate-guard/`
</details>
<details>
<summary>v0.5 ModuŠAutomatyzacji — 2026-03-18 (1 phase, 2 plans)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 16 | Zadania automatyczne | 2/2 | 2026-03-18 |
Archive: `.paul/phases/16-automated-tasks/`
</details>
<details>
<summary>v0.4 ModuŠE-mail — 2026-03-17 (3 phases, 4 plans)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 13 | DB + Skrzynki pocztowe | 1/1 | 2026-03-17 |
| 14 | Szablony wiadomoĹci | 2/2 | 2026-03-17 |
| 15 | WysyĹka e-mail z zamĂłwieĹ„ | 1/1 | 2026-03-17 |
Archive: `.paul/milestones/v0.4-ROADMAP.md`
</details>
<details>
<summary>v0.3 ModuŠParagonów — 2026-03-15 (5 phases, 5 plans)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 8 | DB Foundation + Company Settings | 1/1 | 2026-03-15 |
| 9 | Konfiguracja paragonĂłw (Ustawienia) | 1/1 | 2026-03-15 |
| 10 | Wystawianie paragonĂłw z zamĂłwienia | 1/1 | 2026-03-15 |
| 11 | PodglÄ…d i wydruk paragonu (HTML+PDF) | 1/1 | 2026-03-15 |
| 12 | Sekcja KsiÄ™gowoĹć — lista + eksport XLSX | 1/1 | 2026-03-15 |
Archive: `.paul/milestones/v0.3-ROADMAP.md`
</details>
<details>
<summary>v0.2 Pre-Expansion Fixes — 2026-03-15 (1 phase, 5 plans)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 7 | Pre-Expansion Fixes | 5/5 | 2026-03-15 |
Plans:
- 07-01: Performance (N+1 subqueries, DB indexes, information_schema cache)
- 07-02: Stability (SSL verification, cron throttle DB, migration 000014b)
- 07-03: UX (orderpro_to_allegro disable, lista zamówień fixes)
- 07-04: Tests (AllegroTokenManager + AllegroOrderImportService — 12 testów)
- 07-05: InPost ShipmentProviderInterface (natywne ShipX API)
Archive: `.paul/phases/07-pre-expansion-fixes/`
</details>
<details>
<summary>v0.1 Initial Release — 2026-03-13 (6 phases, 15 plans)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 1 | Tech Debt | 2/2 | 2026-03-12 |
| 2 | Bug Fixes | 4/4 | 2026-03-13 |
| 3 | Tech Debt 2 | 1/1 | 2026-03-13 |
| 4 | Schema Docs | 1/1 | 2026-03-13 |
| 5 | Tech Debt 3 | 1/1 | 2026-03-13 |
| 6 | SonarQube Quality | 6/6 | 2026-03-13 |
Archive: `.paul/milestones/v0.1-ROADMAP.md`
</details>
---
*Roadmap created: 2026-03-12*
*Last updated: 2026-05-18 - Phase 145 complete; v3.14 Polkurier COD Return Time Hotfix complete*

View File

@@ -4,28 +4,28 @@
| Typ pracy | Skill/Komenda | Priorytet | Kiedy | | Typ pracy | Skill/Komenda | Priorytet | Kiedy |
|-----------|---------------|-----------|-------| |-----------|---------------|-----------|-------|
| Nowe funkcjonalności (integracje marketplace, przewoźnicy, moduły) | /feature-dev | optional | Przed implementacją każdej nowej funkcji lub integracji | | Nowe funkcjonalnosci (integracje marketplace, przewoznicy, moduly) | /feature-dev | optional | Przed implementacja kazdej nowej funkcji lub integracji |
| Przegląd kodu przed zamknięciem planu (bezpieczeństwo, jakość, SQL) | /code-review | optional | Po implementacji, przed UNIFY | | Przeglad kodu przed zamknieciem planu (bezpieczenstwo, jakosc, SQL) | /code-review | optional | Po implementacji, przed UNIFY |
| Skanowanie jakości kodu po każdym zakończonym planie | `sonar-scanner` (CLI w katalogu projektu) | required | Po APPLY, przed UNIFY — wyniki na https://sonar.project-pro.pl/dashboard?id=orderPRO | | Komponenty UI (listy zamowien, dashboard, formularze, modale) | /frontend-design | optional | Przy tworzeniu nowych widokow lub redesignie istniejacych |
| Refaktoryzacja i upraszczanie kodu po implementacji | /simplify | optional | Po zakonczeniu APPLY, gdy kod wymaga porzadkowania |
| Skanowanie jakosci SonarQube | `sonar-scanner` / `$paul-quality-gate` | manual | Tylko na wyrazne zadanie operatora |
## SonarQube — procedura po skanowaniu ## SonarQube - tryb reczny
Po każdym uruchomieniu `sonar-scanner`: SonarQube nie jest czescia automatycznego workflow PAUL dla orderPRO.
1. Odpytaj nowe issues przez MCP (`mcp__sonarqube__issues`, project_key: `orderPRO`, tylko `issueStatuses: OPEN`)
2. Porównaj z tym co już jest w `DOCS/todo.md` — dopisz tylko **nowe** issues (których jeszcze nie ma na liście) - Nie uruchamiaj `sonar-scanner` automatycznie po APPLY ani przed UNIFY.
3. Format wpisu w `DOCS/todo.md`: `[] [Sonar {data}] {rule} — {opis problemu} ({liczba wystąpień}x)` - Nie oznaczaj braku `sonar-scanner` jako gap w SUMMARY/STATE.
4. Grupuj pod nagłówkiem `## SonarQube — {data skanu}` - Uruchamiaj SonarQube tylko po wyraznym poleceniu operatora, np. `uruchom Sonar`, `$paul-quality-gate` albo recznie wskazane `sonar-scanner`.
| Komponenty UI (listy zamówień, dashboard, formularze, modale) | /frontend-design | optional | Przy tworzeniu nowych widoków lub redesignie istniejących | - Jezeli Sonar zostanie uruchomiony recznie, wyniki mozna dopisac do `DOCS/todo.md` albo `.paul/codebase/quality_risks.md` zgodnie z celem danego skanu.
| Refaktoryzacja i upraszczanie kodu po implementacji | /simplify | optional | Po zakończeniu APPLY, gdy kod wymaga porządkowania |
## Phase Overrides ## Phase Overrides
Brak domyślne skille wystarczają dla wszystkich faz. Brak - domyslne skille wystarczaja dla wszystkich nowych planow.
## Templates & Assets ## Templates & Assets
Brak cały projekt dostępny bezpośrednio w środowisku. Brak - caly projekt dostepny bezposrednio w srodowisku.
--- ---
*SPECIAL-FLOWS.md — Created: 2026-03-12* *SPECIAL-FLOWS.md - Updated: 2026-05-18*
*Updated: 2026-03-12*

View File

@@ -5,15 +5,17 @@
See: .paul/PROJECT.md (updated 2026-05-18) See: .paul/PROJECT.md (updated 2026-05-18)
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
**Current focus:** v3.14 Polkurier COD Return Time Hotfix complete; Phase 145 unified. **Current focus:** Polish UI Copy plan unified; ready for next PLAN.
## Current Position ## Current Position
Milestone: v3.14 Polkurier COD Return Time Hotfix Current plan: 20260518-2305-polskie-tlumaczenia - Poprawne polskie tlumaczenia UI - complete
Phase: 145 of 145 (Polkurier COD Return Time Hotfix) - Complete Status: Ready for next PLAN
Plan: 145-01 complete Last activity: 2026-05-18 23:40 - Unified .paul/plans/20260518-2305-polskie-tlumaczenia/PLAN.md
Status: Milestone complete, ready for next milestone or release decision
Last activity: 2026-05-18 13:28 - Unified .paul/phases/145-polkurier-cod-return-time-hotfix/145-01-PLAN.md Previous milestone: v3.14 Polkurier COD Return Time Hotfix
Previous phase: 145 of 145 (Polkurier COD Return Time Hotfix) - Complete
Previous plan: 145-01 complete
Progress: Progress:
- Milestone v3.14: [##########] 100% (1 of 1 phases complete) - Milestone v3.14: [##########] 100% (1 of 1 phases complete)
@@ -23,16 +25,16 @@ Progress:
Current loop state: Current loop state:
``` ```
PLAN -> APPLY -> UNIFY PLAN --> APPLY --> UNIFY
done done done [Loop complete - milestone complete] x x x [Loop complete - ready for next PLAN]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-05-18 13:28 Last session: 2026-05-18 23:40
Stopped at: Phase 145 complete; v3.14 milestone complete Stopped at: Plan 20260518-2305-polskie-tlumaczenia UNIFY complete
Next action: Run $paul-complete-milestone or start next milestone planning Next action: `$paul-plan [next work]` or `$paul-verify .paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md`
Resume file: .paul/phases/145-polkurier-cod-return-time-hotfix/145-01-SUMMARY.md Resume file: .paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md
## Pending parallel work ## Pending parallel work
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1). - None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
@@ -87,6 +89,13 @@ Branch: main
|----------|---------|-------| |----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY with `sonar-scanner --version`; CLI is not available in PATH. | | `sonar-scanner` | gap documented | Attempted after APPLY with `sonar-scanner --version`; CLI is not available in PATH. |
### Skill Audit (20260518-2305 Polish UI Copy)
| Expected | Invoked | Notes |
|----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY with `sonar-scanner --version`; CLI is not available in PATH. |
| `jscpd` | invoked | `npx --yes jscpd` succeeded for the targeted UI-copy scope and wrote `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`. |
### Skill Audit (Phase 129) ### Skill Audit (Phase 129)
| Expected | Invoked | Notes | | Expected | Invoked | Notes |
@@ -166,6 +175,7 @@ Branch: main
### Blockers / Concerns ### Blockers / Concerns
- Polish UI Copy APPLY: `sonar-scanner` is unavailable in PATH and manual browser smoke was not run in this session. PHP lint, translation array load check, residual placeholder/mojibake scans, targeted `jscpd`, and `git diff --check` passed.
- Phase 145 APPLY: `vendor/bin/phpunit` is missing, so `tests/Unit/PolkurierShipmentServiceTest.php` was linted and covered by an ad-hoc runtime smoke instead of PHPUnit; `sonar-scanner` is unavailable in PATH. - Phase 145 APPLY: `vendor/bin/phpunit` is missing, so `tests/Unit/PolkurierShipmentServiceTest.php` was linted and covered by an ad-hoc runtime smoke instead of PHPUnit; `sonar-scanner` is unavailable in PATH.
- Phase 144 APPLY: `vendor/bin/phpunit` is missing, so `tests/Unit/OrdersRepositoryNotesCountTest.php` was linted and covered by an ad-hoc SQLite runtime smoke instead of PHPUnit; `sonar-scanner` is unavailable in PATH. - Phase 144 APPLY: `vendor/bin/phpunit` is missing, so `tests/Unit/OrdersRepositoryNotesCountTest.php` was linted and covered by an ad-hoc SQLite runtime smoke instead of PHPUnit; `sonar-scanner` is unavailable in PATH.
- Phase 134: `sonar-scanner` is still unavailable in PATH. - Phase 134: `sonar-scanner` is still unavailable in PATH.
@@ -185,22 +195,18 @@ Branch: main
## Pending Actions ## Pending Actions
- Polish UI Copy follow-up: manual smoke main pages with changed copy (`/orders/list`, order detail, settings integrations/templates, accounting, shipments prepare). Sonar is now manual on-demand only.
- Phase 145 follow-up: after restoring `vendor/`, run `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php`. - Phase 145 follow-up: after restoring `vendor/`, run `vendor/bin/phpunit tests/Unit/PolkurierShipmentServiceTest.php`.
- Phase 145 follow-up: run SonarQube scan after restoring `sonar-scanner` in PATH.
- Phase 145 follow-up: with explicit operator intent, create one live Polkurier COD shipment and confirm API accepts `codtype='S'` / `return_cod='BA'` without the previous return-time error. - Phase 145 follow-up: with explicit operator intent, create one live Polkurier COD shipment and confirm API accepts `codtype='S'` / `return_cod='BA'` without the previous return-time error.
- Phase 138 follow-up: run `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php tests/Unit/TemplateVariableCatalogTest.php` after dependencies are installed. - Phase 138 follow-up: run `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php tests/Unit/TemplateVariableCatalogTest.php` after dependencies are installed.
- Phase 139 follow-up: split `OrdersStatisticsRepository` (`php:S1448`, 43 methods) in a future god-class refactor if still relevant. - Phase 139 follow-up: split `OrdersStatisticsRepository` (`php:S1448`, 43 methods) in a future god-class refactor if still relevant.
- Phase 139 follow-up: continue with confirmed groups `php:S1142`, `php:S3776`, `php:S1172`, `php:S1192`, `php:S112`, plus Web table/accessibility issues. `php:S4833` is now only 3 core framework require issues. - Phase 139 follow-up: continue with confirmed groups `php:S1142`, `php:S3776`, `php:S1172`, `php:S1192`, `php:S112`, plus Web table/accessibility issues. `php:S4833` is now only 3 core framework require issues.
- Phase 140 follow-up: manual smoke `/settings/integrations/shoppro?tab=delivery` -> wybierz Polkurier -> zapisz -> odswiez -> mapowanie pozostaje; potem przygotuj przesylke shopPRO i potwierdz preselect `provider='polkurier'`. - Phase 140 follow-up: manual smoke `/settings/integrations/shoppro?tab=delivery` -> wybierz Polkurier -> zapisz -> odswiez -> mapowanie pozostaje; potem przygotuj przesylke shopPRO i potwierdz preselect `provider='polkurier'`.
- Phase 140 follow-up: uruchom SonarQube scan po przywroceniu `sonar-scanner` w PATH albo ponownym pobraniu oficjalnego scanner fallback.
- Phase 141 follow-up: manual smoke `/settings/integrations` -> potwierdz sekcje marketplace/kurierzy/pozostale, osobne wiersze Allegro Sandbox/Production i poprawne linki Ustawienia. - Phase 141 follow-up: manual smoke `/settings/integrations` -> potwierdz sekcje marketplace/kurierzy/pozostale, osobne wiersze Allegro Sandbox/Production i poprawne linki Ustawienia.
- Phase 141 follow-up: uruchom SonarQube scan po przywroceniu `sonar-scanner` w PATH albo ponownym pobraniu oficjalnego scanner fallback.
- Phase 142 follow-up: manual smoke `/orders/1164/shipment/prepare` -> potwierdz, ze mapowanie shopPRO -> Polkurier preselectuje przewoznika i usluge. - Phase 142 follow-up: manual smoke `/orders/1164/shipment/prepare` -> potwierdz, ze mapowanie shopPRO -> Polkurier preselectuje przewoznika i usluge.
- Phase 142 follow-up: `composer install` / przywroc `vendor/`, potem uruchom `vendor/bin/phpunit tests/Unit/ShipmentPreparePolkurierMappingTest.php`. - Phase 142 follow-up: `composer install` / przywroc `vendor/`, potem uruchom `vendor/bin/phpunit tests/Unit/ShipmentPreparePolkurierMappingTest.php`.
- Phase 142 follow-up: uruchom SonarQube scan po przywroceniu `sonar-scanner` w PATH albo ponownym pobraniu oficjalnego scanner fallback.
- Phase 143 follow-up: manual smoke `/orders/list` -> potwierdz brak boksu "Zamowienia / Kompaktowa lista..." oraz status panel/table layout. - Phase 143 follow-up: manual smoke `/orders/list` -> potwierdz brak boksu "Zamowienia / Kompaktowa lista..." oraz status panel/table layout.
- Phase 143 follow-up: manual smoke desktop sidebar -> ustaw `localStorage.sidebarCollapsed='1'`, odswiez strone i potwierdz brak widocznego expanded-to-collapsed flash. - Phase 143 follow-up: manual smoke desktop sidebar -> ustaw `localStorage.sidebarCollapsed='1'`, odswiez strone i potwierdz brak widocznego expanded-to-collapsed flash.
- Phase 143 follow-up: uruchom SonarQube scan po przywroceniu `sonar-scanner` w PATH albo ponownym pobraniu oficjalnego scanner fallback.
- Phase 138 manual smoke: test a real SMTP SSL/STARTTLS mailbox in strict mode; test invalid and valid e-mail/SMS template saves in UI. - Phase 138 manual smoke: test a real SMTP SSL/STARTTLS mailbox in strict mode; test invalid and valid e-mail/SMS template saves in UI.
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online). - Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online).
- Backfill zamowienia #882 - operator robi recznie po wdrozeniu (poza zakresem planu). - Backfill zamowienia #882 - operator robi recznie po wdrozeniu (poza zakresem planu).
@@ -260,4 +266,11 @@ Branch: main
## Skill Requirements ## Skill Requirements
- `sonar-scanner` required after APPLY; Phase 139 used the official Windows x64 scanner from `%TEMP%` because the CLI is still not available in PATH. Earlier Phase 116, 117, 121, 122, 128, 129, 130, 131, 132, 133, 134, 135, 136 and 138 gaps remain historical. - SonarQube / `sonar-scanner` is no longer part of the automatic PAUL PLAN/APPLY/UNIFY workflow for orderPRO. Use it only on explicit operator request via `$paul-quality-gate`, `uruchom Sonar`, or a manually provided `sonar-scanner` command.
- Historical Sonar gaps in older phase summaries remain archival context and should not be carried into new plans as required checks.
### Codebase Mapped
Date: 2026-05-18
Documents: `.paul/codebase/`
Quality Radar: partial (`jscpd` ok through `npx`; `codebase-memory-mcp` installed globally at 0.6.1, enabled in Codex MCP config, and initial index completed in a fresh Codex process: 8165 nodes / 13610 edges; `ast-grep` installed globally and working after Windows wrapper repair)

View File

@@ -2,6 +2,16 @@
## Co zrobiono ## Co zrobiono
- [Policy] Dodano opcje autokompresji `.paul/STATE.md` dla PAUL w Codex i Claude Code.
- Nowa preferencja `.paul/config.md`: `state_autocompress: true` oraz `state_autocompress_max_lines: 500`.
- Workflowy PAUL po aktualizacji `STATE.md` maja teraz sprawdzac liczbe linii i kompresowac tylko historyczne/narastajace sekcje, zachowujac biezaca pozycje, `Next action`, `Resume file`, aktywne blokery i aktywne pending actions.
- [Policy] Oznaczono `.paul/ROADMAP.md` jako opcjonalny legacy/release context; `map-codebase` w Codex i Claude Code nie ma go tworzyc ani odswiezac.
- [Plan 20260518-2305-polskie-tlumaczenia] Znormalizowano polskie teksty UI do UTF-8 z polskimi znakami.
- Poprawiono centralny słownik `resources/lang/pl.php`, widoczne hardcoded teksty w widokach, modułach JS i wybranych komunikatach backendu.
- Zachowano techniczne kontrakty: placeholdery `{{zamowienie.*}}` / `{{przesylka.*}}`, routy, form field names, CSS/JS selectors, provider/status codes i API payload keys.
- Quality Radar: `codebase-memory-mcp` i `npx jscpd` działały; globalne `jscpd`, `ast-grep`/`sg` oraz `sonar-scanner` są niedostępne w PATH. jscpd targeted: 226 plików, 397 klonów, 4754 zduplikowane linie.
- Gap: manualny smoke UI Polish UI Copy pozostaje do wykonania po uruchomieniu app/browser session.
- [Policy] Usunieto SonarQube z automatycznego workflow PAUL dla orderPRO. `sonar-scanner` / `$paul-quality-gate` sa teraz reczne, tylko na wyrazne zadanie operatora; brak Sonara nie ma byc raportowany jako gap w nowych PLAN/APPLY/UNIFY.
- [Phase 140, Plan 01] Dodano mapowanie form dostawy shopPRO na uslugi Polkurier. - [Phase 140, Plan 01] Dodano mapowanie form dostawy shopPRO na uslugi Polkurier.
- Rozszerzono zakladke `Dostawy` integracji shopPRO o przewoznika Polkurier i wyszukiwalna liste uslug. - Rozszerzono zakladke `Dostawy` integracji shopPRO o przewoznika Polkurier i wyszukiwalna liste uslug.
- Zapis mapowan shopPRO obsluguje `provider='polkurier'` oraz zapis service code i nazwy uslugi w `carrier_delivery_method_mappings`. - Zapis mapowan shopPRO obsluguje `provider='polkurier'` oraz zapis service code i nazwy uslugi w `carrier_delivery_method_mappings`.
@@ -34,7 +44,15 @@
- `.paul/PROJECT.md` - `.paul/PROJECT.md`
- `.paul/ROADMAP.md` - `.paul/ROADMAP.md`
- `.paul/STATE.md` - `.paul/STATE.md`
- `.paul/config.md`
- `.paul/changelog/2026-05-18.md` - `.paul/changelog/2026-05-18.md`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/PLAN.md`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/translation-audit.txt`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/hardcoded-ui-files.txt`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/residual-ui-scan.txt`
- `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`
- `.paul/codebase/radar/codebase-memory-post-apply-polish-ui-copy.txt`
- `.paul/phases/140-shoppro-polkurier-delivery-mapping/140-01-PLAN.md` - `.paul/phases/140-shoppro-polkurier-delivery-mapping/140-01-PLAN.md`
- `.paul/phases/140-shoppro-polkurier-delivery-mapping/140-01-SUMMARY.md` - `.paul/phases/140-shoppro-polkurier-delivery-mapping/140-01-SUMMARY.md`
- `DOCS/ARCHITECTURE.md` - `DOCS/ARCHITECTURE.md`

View File

@@ -0,0 +1,14 @@
# 2026-05-19
## Co zrobiono
- [Policy] Dodano cleanup handoffow po `$paul-unify` w PAUL dla Codex i Claude Code.
- Po udanym UNIFY usuwane sa tylko aktywne pliki `.paul/handoffs/*-handoff.md` / `*-pause.md` zwiazane z wlasnie zamknietym planem albo wskazane w pre-UNIFY `Resume file`.
- `.paul/handoffs/archive/` pozostaje nietkniety.
- [Policy] Przestawiono PAUL Quality Radar na lekki tryb automatyczny: `codebase-memory-mcp` zostaje wlaczony, a `jscpd` i `ast-grep` sa manual/on-demand.
- Zaktualizowano konfiguracje orderPRO oraz frameworki PAUL dla Codex i Claude Code, aby `jscpd`/`ast-grep` nie byly sprawdzane, instalowane ani uruchamiane automatycznie.
- [Policy] Odchudzono generowany zestaw `.paul/codebase/*.md`: usunieto `index.md`, `structure.md`, `concerns.md`, `domain_duplicates.md` i `tech_changelog.md`.
- `todo.md` pozostaje recznym plikiem operatora i nie bedzie nadpisywany przez `$paul-init` ani `$paul-map-codebase`.
- Zaktualizowano workflowy PAUL dla Codex i Claude Code, zeby nowe mapowanie tworzylo 9 generowanych dokumentow oraz przenosilo ryzyka duplikacji/source-of-truth do `quality_risks.md`.
- [Policy] Dodano globalna zasade, ze tresc dokumentow Markdown generowanych przez PAUL ma byc pisana po polsku; sciezki, komendy, klucze konfiguracji i identyfikatory kodu pozostaja bez tlumaczenia.
- [Maintenance] Przejrzano komendy PAUL dla Codex i Claude Code: dodano lokalny `language_policy` do wszystkich komend, spolszczono `$paul-help` i `$paul-config`, usunieto artefakty starego `CONCERNS.md` / `13 documents`, oraz potwierdzono zgodnosc wrapperow Claude z frameworkiem.

View File

@@ -1,616 +1,59 @@
# Architecture # Architecture
Last refresh: 2026-05-18.
orderPRO is a PHP 8.4 custom MVC-style application with manual routing and manual dependency wiring.
## Entry Points
- HTTP: `public/index.php`.
- Root forwarder: `index.php`.
- Bootstrap: `bootstrap/app.php`.
- CLI migrations: `bin/migrate.php`.
- CLI cron: `bin/cron.php`.
## Request Flow ## Request Flow
``` `public/index.php` -> `bootstrap/app.php` -> `src/Core/Application.php` -> `routes/web.php` -> `src/Core/Routing/Router.php` -> middleware -> controller -> service/repository -> `src/Core/View/Template.php` -> response.
HTTP Request
→ public/index.php ## Core Layers
→ bootstrap/app.php (loads config, registers PDO, services)
→ Application::boot() (loads routes/web.php) - `src/Core/` contains application boot, routing, request/response, database connection/migrations, sessions, CSRF, flash, views, translation, logging, and HTTP helpers.
→ Router::dispatch(Request) (matches URL, runs middleware pipeline) - `routes/web.php` owns route registration and constructor wiring; there is no route auto-discovery.
→ [Middleware] (AuthMiddleware, ApiKeyMiddleware) - `src/Modules/*` contains feature modules with controllers, services, repositories, API clients, and cron handlers.
→ Controller::method() (parse input → call repository/service → render) - `resources/views/` contains native PHP templates and shared components.
→ Template::render() (PHP native, layout composition) - `resources/scss/` contains SCSS sources; compiled CSS is under `public/assets/css/`.
→ Response::send() - `public/assets/js/modules/` contains browser modules loaded by layouts/views.
```
## Main Modules
## Layer Map
- Auth/session: `src/Modules/Auth/`.
| Layer | Location | Responsibility | - Users: `src/Modules/Users/`.
|-------|----------|----------------| - Orders: `src/Modules/Orders/`.
| Entry | `public/index.php` | Bootstrap only | - Statistics: `src/Modules/Statistics/`.
| Routes | `routes/web.php` (581 lines) | All ~80 routes; manual DI wiring | - Settings/integrations/mappings: `src/Modules/Settings/`.
| Core | `src/Core/` (25 files) | Framework infrastructure | - Shipments/tracking/delivery statuses: `src/Modules/Shipments/`.
| Controllers | `src/Modules/*/Controller.php` | Request parsing → response | - Accounting/receipts/invoices: `src/Modules/Accounting/`.
| Services | `src/Modules/*/Service.php` | Business logic | - Email: `src/Modules/Email/`.
| Repositories | `src/Modules/*/Repository.php` | PDO data access (34+ repos) | - Automation: `src/Modules/Automation/`.
| Views | `resources/views/` | PHP templates with `$e()` / `$t()` | - Cron: `src/Modules/Cron/`.
| Components | `resources/views/components/` | Reusable UI blocks | - Printing: `src/Modules/Printing/`.
| Frontend modules | `public/assets/js/modules/` | Small vanilla JS enhancements loaded by layout | - SMS: `src/Modules/Sms/`.
- Notifications: `src/Modules/Notifications/`.
## Module Inventory (`src/Modules/`) - Info pages: `src/Modules/Info/`.
| Module | Files | Key Classes | Purpose | ## Key Flows
|--------|-------|-------------|---------|
| **Auth** | 3 | `AuthController`, `AuthMiddleware`, `AuthService` | Login/logout, session | - Order import: cron/manual integration controller -> marketplace sync service -> `src/Modules/Orders/OrderImportRepository.php` -> order aggregate tables -> automation.
| **Users** | 2 | `UserController`, `UserRepository` | User CRUD | - Order UI: order route in `routes/web.php` -> `src/Modules/Orders/OrdersController.php` -> `src/Modules/Orders/OrdersRepository.php` -> `resources/views/orders/`.
| **Orders** | 3 | `OrdersController` (1187 LOC), `OrdersRepository` (1221 LOC) | Order list, detail, status, payment, correlated subquery for return-risk | - Shipment creation: route -> `src/Modules/Shipments/ShipmentController.php` -> provider registry/service -> `src/Modules/Shipments/ShipmentPackageRepository.php` -> tracking sync.
| **Shipments** | 17 | `ShipmentController`, provider services + tracking services | Shipment creation, label download, tracking polling | - Accounting: route -> `src/Modules/Accounting/ReceiptService.php` or `src/Modules/Accounting/InvoiceService.php` -> repository -> PDF/export/email flow.
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export | - Cron: `bin/cron.php` or web cron -> `src/Modules/Cron/CronHandlerFactory.php` -> `src/Modules/Cron/CronRunner.php` -> handler class.
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments | - View rendering: controller -> `src/Core/View/Template.php` -> layout in `resources/views/layouts/` -> view/component.
| **Automation** | 6 | `AutomationService` (834 LOC), `AutomationRepository`, `AutomationExecutionLogRepository` | Event→condition→action rules, email triggers |
| **Settings** | 54+ | Integration controllers, OAuth clients, API clients (Fakturownia incl.), mappers | Allegro/shopPRO/Apaczka/InPost/Fakturownia config, status mappings | ## Architectural Hotspots
| **Sms** | 3 | `SmsMessageRepository`, `SmsConversationService`, `SmsplanetWebhookController` | SMSPLANET outbound order SMS, inbound webhook parsing, order matching |
| **Notifications** | 3 | `NotificationRepository`, `NotificationController`, `NotificationApiController` | Global notification history, unread polling API, mark-read actions | - `routes/web.php` is large and central; route changes can affect DI wiring for many modules.
| **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh | - `src/Modules/Settings/` is the largest boundary and mixes integration configs, API clients, mapping controllers, and sync services.
| **Printing** | 4 | `PrintApiController`, `PrintJobRepository`, `ApiKeyMiddleware` | REST API for Windows print client | - `src/Modules/Orders/OrdersController.php` and `src/Modules/Orders/OrdersRepository.php` are central to most user workflows.
| **Statistics** | 3 | `OrdersStatisticsController`, `OrdersStatisticsRepository`, `statistics-summary-charts.js` | Daily order statistics and monthly summary charts | - `resources/views/orders/show.php` and `resources/views/shipments/prepare.php` are large view surfaces with embedded JS behavior.
| **Info** | 1 | `InfoController` | Health check |
## Frontend Enhancement Modules
### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`)
- Loaded globally from `resources/views/layouts/app.php`.
- Enhances native `<select multiple data-checkbox-multiselect>` controls after `DOMContentLoaded`.
- Keeps the original select in the form, synchronizes option `selected` state, and preserves native GET/POST names such as `channels[]` and `status_groups[]`.
- Used by `/statistics/orders` and `/statistics/summary` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
- Progressive enhancement: if JavaScript fails, the native multi-select remains visible.
### Statistics Summary Charts (`public/assets/js/modules/statistics-summary-charts.js`)
- Loaded globally from `resources/views/layouts/app.php` after Chart.js 4.4.8 CDN; activates only when `#js-statistics-summary-data` exists.
- Reads JSON produced by `OrdersStatisticsController::summary()` and renders two interactive Chart.js line charts on `/statistics/summary`.
- Chart 1 displays monthly order counts per selected integration plus a `Razem` line.
- Chart 2 displays monthly gross order values per selected integration plus a `Razem` line.
- The PHP view keeps table fallbacks under both charts, so the data remains visible if JavaScript fails.
## Key Data Flows
### Order Lifecycle
1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
2. **Re-import (Phase 111 + 112 + 119)**`OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). **Phase 112-01 (delta-only re-import):** przy `created=false` repo nie wywoluje `replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory``order_items.id` i flagi lokalne (np. `project_generated` z Phase 97) pozostaja stabilne. `updateOrderDelta()` aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Anulowanie ze zrodla (`is_canceled_by_buyer=1` lub zmapowany pull `status_code='anulowane'`) nadpisuje preservacje statusu. Identical-payload guard (`normalizePayloadJson`) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji. **Phase 119-01 (total_paid protection):** gdy `paymentStatusUnchanged=true` (`oldPaymentStatus === newPaymentStatus`), `updateOrderDelta()` nie dolacza `total_paid` do UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe). `is_canceled_by_buyer` jest pomijane analogicznie, chyba ze `cancelledBySource=true` (cancel propagation ze zrodla zawsze wymusza wpis flagi). Pozostale pola (`status_code`, `payment_status`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`) zachowuja niezmieniony kontrakt z Phase 112-01.
3. **Status update**`OrdersController::updateStatus()``OrdersRepository::updateStatus()` → automation check
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` / `ErliStatusSyncService` → marketplace API
### Statistics Summary
1. **Request**`/statistics/summary``OrdersStatisticsController::summary()`
2. **Filters** — controller reuses statistics filter semantics: date range, `channels[]`, `status_groups[]`, default status groups excluding cancelled; default history starts at `2026-04-01`.
3. **Aggregation**`OrdersStatisticsRepository::aggregateByMonth()` groups existing `orders` rows by `YYYY-MM` and channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report.
4. **View model** — controller builds per-integration series and total series for order count and gross value charts.
5. **Render**`resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
### Shipment Flow
1. **Create**`ShipmentController::create()``ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()``ShipmentPackageRepository::insert()`
2. **Track** — Cron `ShipmentTrackingHandler``ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
### Receipt / Invoice
1. **Generate**`ReceiptController::store()``ReceiptService::generateReceipt()``ReceiptRepository::insert()` + Dompdf PDF
2. **Email**`EmailSendingService::send()``VariableResolver::resolve()``AttachmentGenerator::generatePdf()` → PHPMailer SMTP
### Automation Rules
1. **Setup**`AutomationController``AutomationRepository::insertRule()`
2. **Trigger**`AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`) → runs action (send email, update status)
3. **Log**`AutomationExecutionLogRepository` tracks every run
### Cron Jobs
| Handler | Task |
|---------|------|
| `AllegroOrdersImportHandler` | Fetch new Allegro orders |
| `AllegroStatusSyncHandler` | Push status changes to Allegro |
| `ErliStatusSyncHandler` | Pull Erli status events via inbox or push manual local status changes to Erli |
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
| `ShopproStatusSyncHandler` | Push status to shopPRO |
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
| `OrderStatusAgedHandler` | Trigger automation for stuck statuses |
| `AutomationHistoryCleanupHandler` | Purge old automation logs |
## Dependency Injection
Manual constructor injection in `routes/web.php` — no DI container library. Example:
```php
$ordersController = new OrdersController(
$template, $translator, $auth,
$app->orders(), $shipmentPackageRepository,
$receiptRepository, $receiptConfigRepository, ...
);
```
All production classes are `final` — prevents accidental inheritance.
## Directory Structure
```
bootstrap/ app.php (service wiring, config loading)
bin/ migrate.php, cron.php (CLI entry points)
config/ app.php, database.php
database/
migrations/ 84 SQL files (YYYYMMDD_NNNNNN_description.sql)
drafts/ WIP migrations
public/
index.php HTTP entry point
.htaccess Apache rewrite rules
assets/css/ Compiled CSS (app.css, login.css, modules/)
assets/js/ jquery-alerts.js, global-search.js, automation-form.js
resources/
views/ PHP templates by module + components/ layouts/
scss/ SCSS sources (app.scss, login.scss, modules/_*.scss)
modules/ jquery-alerts JS+SCSS source
lang/pl/ Polish translations
routes/
web.php All routes (581 lines)
src/
Core/ Framework (25 files)
Modules/ 13 feature modules (~200+ PHP files)
storage/
logs/ app.log
sessions/ PHP session files
cache/ PHPUnit cache, etc.
tests/
Unit/ PHPUnit tests (7+ service test files)
bootstrap.php PSR-4 autoloader for tests
```
## Phase 108 — Delivery Status Management
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
- CRUD dla tabeli `delivery_statuses`
- Per-request static cache (`private static ?array $cache`)
- Blokuje edycję/usunięcie statusów systemowych (`is_system=1`)
- Blokuje usunięcie statusów używanych w `delivery_status_mappings` lub `shipment_packages`
### DeliveryStatusesController (`src/Modules/Settings/DeliveryStatusesController.php`)
- Panel `/settings/delivery-statuses`
- Dwie zakładki via `?tab=` param: `statuses` (CRUD) i `mapping` (embed mapowania)
- Wstrzykuje `DeliveryStatusRepository` i `DeliveryStatusMappingRepository`
### DeliveryStatus::setRepository() (dynamic loading)
- Wywoływane raz w `routes/web.php` po bootstrap
- `label()`, `getAllOptions()`, `getAllStatuses()`, `getColor()` ładują z DB gdy repo ustawione
- Fallback na hardcoded stałe gdy repo niedostępne
### AutomationController + AutomationService (Phase 108 Plan 02)
- `AutomationController::buildShipmentStatusOptions()` — buduje listę opcji `[key => ['label' => ...]]` z `DeliveryStatus::getAllOptions()` (DB-driven)
- Walidacja `shipment_status` warunku i `update_shipment_status` akcji w `parseConditionValue()`/`parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService::evaluateShipmentStatusCondition()` — bezpośrednie porównanie kluczy DB (usunięto mapping grupowy `SHIPMENT_STATUS_OPTION_MAP`)
- `AutomationService::resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) nie matchują się — operator musi je odtworzyć przy użyciu nowych kluczy DB
## Phase 113 — Fakturownia Integration Foundation
### Schema (Plan 113-01)
- Tabele `invoice_configs`, `invoices`, `invoice_number_counters` (mirror `receipt_configs`/`receipts`/`receipt_number_counters` plus delegation fields: `invoice_configs.integration_id`, `is_delegated`; `invoices.external_invoice_id`, `external_pdf_url`).
- Tabela `fakturownia_integration_settings` (multi-account: `integration_id INT UNSIGNED NOT NULL UNIQUE` FK -> `integrations(id)`).
- `orders.invoice_requested TINYINT(1) NOT NULL DEFAULT 0` z indexem `idx_orders_invoice_requested`.
### FakturowniaIntegrationRepository (`src/Modules/Settings/FakturowniaIntegrationRepository.php`)
- `findAll()` JOIN `integrations` + `fakturownia_integration_settings` zwraca listę kont Fakturowni.
- `findByIntegrationId(int)` zwraca jedno konto (z resolved `api_token_encrypted` z `integrations.api_key_encrypted` z fallbackiem na settings).
- `save(?int $integrationId, array $payload)` - upsert (insert do `integrations` przez `IntegrationsRepository::ensureIntegration` gdy `$integrationId=null`; w przeciwnym razie update name/is_active). Token szyfrowany przez `IntegrationSecretCipher` i zapisywany do `integrations.api_key_encrypted` (źródło prawdy) oraz settings.api_token_encrypted (cache).
- `delete(int $integrationId)` — blokuje usunięcie gdy `invoice_configs.integration_id = X` (FK SET NULL chroniony aplikacyjnie przez `IntegrationConfigException`).
- `getDecryptedToken(int $integrationId)` — dla użycia w przyszłych planach (createInvoice/downloadPdf).
### FakturowniaApiClient (`src/Modules/Settings/FakturowniaApiClient.php`)
- `testConnection(string $prefix, string $apiToken): array` — GET `https://{prefix}.fakturownia.pl/account.json?api_token=...` z cURL + `SslCertificateResolver::resolve()`. Zwraca `['ok' => bool, 'http_code' => int, 'message' => string]`.
- `createInvoice()` i `downloadPdf()` — STUB-y rzucające `RuntimeException` do implementacji w kolejnym planie.
### IntegrationsRepository::updateTestResult()
- Nowa metoda zapisująca `last_test_status / last_test_http_code / last_test_message / last_test_at` po wywołaniu API test. Używana przez `FakturowniaIntegrationController::test()` (i będzie reuse'owana w przyszłych integracjach).
### FakturowniaIntegrationController (`src/Modules/Settings/FakturowniaIntegrationController.php`)
- Routy `/settings/integrations/fakturownia` (lista), `.../edit`, `.../save`, `.../test`, `.../delete` (POST z `_token` CSRF).
- Wykorzystuje `Flash::set('fakturownia.save'|'fakturownia.test'|'fakturownia.error')` i `RedirectPathResolver`.
### IntegrationsHubController
- Nowy parametr konstruktora `FakturowniaIntegrationRepository $fakturownia` i nowa metoda `buildFakturowniaRow()` agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
## Phase 118 — Fakturownia Single Instance
### FakturowniaIntegrationRepository
- Zarzadza jedna globalna konfiguracja `fakturownia_integration_settings.id=1` i jednym rekordem `integrations.type='fakturownia'`.
- `getSettings()` zasila formularz i hub integracji; `saveSettings()` zapisuje prefix, token, department/defaults i aktywnosc.
- `getIntegrationId()` jest zrodlem prawdy dla delegowanych `invoice_configs.integration_id`.
- `findAll()` zostaje kompatybilnym wrapperem zwracajacym liste z jednym elementem.
### FakturowniaIntegrationController + UI
- `/settings/integrations/fakturownia` pokazuje jeden formularz i test polaczenia.
- Legacy `/new` i `/edit` przekierowuja do globalnej konfiguracji; delete nie jest oferowany w UI.
- Hub integracji pokazuje jedna instancje Fakturowni, bez licznika kont.
### Invoice Config Delegation
- `InvoiceConfigRepository::save()` przy `is_delegated=1` ignoruje wieloinstancyjny wybor i ustawia globalny Fakturownia integration id.
- UI konfiguracji faktury pokazuje status globalnej konfiguracji zamiast selecta kont.
- `invoice_configs.integration_id` zostaje dla kompatybilnosci z `InvoiceService` i istniejaca historia faktur.
### Migration 20260512_000109
- Wybiera aktywna instancje Fakturowni; fallback: uzywana w `invoice_configs`, potem najnizsze id.
- Przepina delegowane `invoice_configs.integration_id` na zachowany rekord i usuwa nadmiarowe rekordy Fakturowni po przepieciu zaleznosci.
## Phase 115 — Wystawianie faktury z zamowienia
### InvoiceService (`src/Modules/Accounting/InvoiceService.php`)
- `issue(array $params): array` — orchestrator. Walidacja config (active), order details fetch, build snapshots (seller z `company_settings`, buyer merged z payload_json+addresses+manual override, items z VAT-aware netto/brutto split), routing do `issueLocal()` lub `issueDelegated()` zaleznie od `invoice_configs.is_delegated`.
- `issueLocal()``InvoiceRepository::nextLocalNumber()` (atomowy counter z `invoice_number_counters`) -> `insertLocal()` -> zwraca `{invoice_id, invoice_number, total_gross, mode='local'}`.
- `issueDelegated()` — Phase 136 retry-safe flow dla Fakturowni. Serwis uzywa stabilnego `oid` z `orders.internal_order_number`, sprawdza `GET /invoices.json?oid=...` przed POST, zapisuje lokalny wiersz `pending_external` przed POST, finalizuje go po sukcesie albo auto-podpina dokument znaleziony po timeoutcie. `invoice_number_counters` NIE jest dotykany dla delegated.
- Static `extractBuyerTaxNumber($order, $buyerAddress)` — parsuje NIP z payload_json sciezki: `invoice.address.taxId` (Allegro), `invoice.taxId/nip`, `buyer.tax_number/nip`, `client.nip/tax_number`, top-level `nip/tax_number`. Fallback na `order_addresses.company_tax_number`.
### InvoiceRepository (`src/Modules/Accounting/InvoiceRepository.php`)
- `findByOrderId/findById` — JOIN `invoices` + `invoice_configs` + `integrations` (type='fakturownia') + `fakturownia_integration_settings` (LEFT JOIN dla `account_prefix`).
- `insertLocal/insertDelegated` — wspolny prywatny `insert()` z roznymi NULL-amizacjami `external_*` pol.
- `nextLocalNumber()``INSERT ... ON DUPLICATE KEY UPDATE last_number = last_number + 1` na `invoice_number_counters`, mirror `ReceiptRepository::getNextNumber`.
- `paginate()` — filtry: `search` (numer/order ref), `config_id`, `mode` (local/delegated rozroznia po `external_invoice_id IS NULL`), `date_from`/`date_to`.
### FakturowniaApiClient (rozszerzony)
- `createInvoice(array $settings, array $invoice)` — POST `https://{prefix}.fakturownia.pl/invoices.json` z body `{api_token, invoice}`. cURL z `SslCertificateResolver`, timeout `$timeoutSeconds`. On 2xx parsuje JSON na `{id, number, view_url, pdf_url, raw}`. On non-2xx rzuca `RuntimeException("HTTP {code}: {error}")`.
- `findInvoiceByOid(array $settings, string $oid)` — GET `https://{prefix}.fakturownia.pl/invoices.json?oid=...&api_token=...`; uzywane do reconciliacji po niepewnym POST i przed retry, bo Fakturownia dokumentuje lookup po `oid`, a nie dokumentuje `Idempotency-Key`.
- `buildPdfUrl(prefix, invoiceId, apiToken)` — string-builder dla `https://{prefix}.fakturownia.pl/invoices/{id}.pdf?api_token=...`. Bez fetcha; uzywany w redirect 302.
- Dodany `httpPostJson()` (private) odpowiednik istniejacego `httpGet()`.
### InvoiceController (`src/Modules/Accounting/InvoiceController.php`)
- `create(Request)` — GET `/orders/{id}/invoice/create`. Walidacja `orders.invoice_requested=1` (przekierowanie z flash error gdy 0). Active configs (filter `is_active=1`). NIP auto-prefill via `InvoiceService::extractBuyerTaxNumber()`. Renderuje `accounting/invoice_form`.
- `store(Request)` — POST `/orders/{id}/invoice/store`. CSRF, `config_id` walidacja. Wywoluje `InvoiceService::issue()` z buyer overrides z formularza. On success: `OrdersRepository::recordActivity('invoice_issued')`, flash success, redirect na `/orders/{id}/invoice/{invoiceId}`. On `InvoiceIssueException`: flash do `invoice.error`, redirect z powrotem na form.
- `show(Request)` — GET `/orders/{id}/invoice/{invoiceId}`. HTML preview z snapshotow.
- `pdf(Request)` — GET `/orders/{id}/invoice/{invoiceId}/pdf`. Gdy `external_pdf_url` istnieje -> redirect 302; inaczej Dompdf inline z templatu `accounting/invoice_pdf`.
- `issuedList(Request)` — GET `/settings/accounting/invoices/issued`. Filtry GET, paginacja 50/strona.
### orders.invoice_requested toggle
- `OrdersRepository::setInvoiceRequested(int, bool)` — UPDATE z `updated_at = NOW()`.
- `OrdersController::toggleInvoiceRequested` — POST `/orders/{id}/invoice-requested/toggle`. CSRF, JSON response `{success, invoice_requested}`. Loguje `order_activity_log` z `event_type='invoice_requested_changed'`.
- `public/assets/js/modules/invoice-requested-toggle.js` — vanilla JS, idempotent guard `dataset.bound='1'`. AJAX POST przy `change`, optimistic show/hide `[data-invoice-button-wrap]`. Rollback checkbox przy HTTP/network blad.
### Auto-import flagi invoice_requested (zaktualizowane Phase 125-01)
- **shopPRO:** `ShopproOrderMapper::resolveInvoiceRequested($payload)` jest jedynym zrodlem heurystyki — sprawdza top-level klucze payloadu: `is_invoice`, `invoice.required`, `invoice` (bool), oraz obecnosc danych firmowych (`firm_name`/`firm_nip`/`invoice.company_name`/`invoice.tax_id`/`invoice.nip`/`company_name`/`tax_id`/`nip` etc.). Wynik eksponowany w `mapOrderAggregate()` jako top-level klucz `invoice_detected` (transient, nie pisany do DB). `ShopproOrdersSyncService::importOne` propaguje `!empty($aggregate['invoice_detected'])` do `setInvoiceRequested(true)` tylko przy `wasCreated=true`. Stara metoda `shouldRequestInvoice` w sync service usunieta (zastapiona heurystyka mappera).
- **Allegro:** `AllegroOrderImportService::shouldRequestInvoice($payload)` sprawdza w kolejnosci: `invoice.required` (truthy), `invoice.naturalPerson === false` (klient firmowy), `invoice.address.taxId` (NIP w adresie faktury), `invoice.companyName`/`invoice.address.company.name`. Wywolane tylko przy `wasCreated=true` w `importSingleOrder`. Rozszerzenie heurystyki naprawia luke gdy klient Allegro podaje NIP bez ustawiania `invoice.required=true`.
- **Kontrakt Phase 115/112 zachowany:** auto-set TYLKO przy `wasCreated=true`. Delta-only re-import nie nadpisuje manualnej flagi operatora (manualny toggle przez `/orders/{id}/invoice-requested/toggle` przezywa kolejne synchronizacje).
- **Legacy:** kolumna `orders.is_invoice` (Phase 115) usunieta migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql`. Backfill: 7 zamowien gdzie `is_invoice=1 AND invoice_requested=0` dostalo `invoice_requested=1` przed DROP COLUMN.
### View hierarchy
- `accounting/invoice_form.php` — formularz wystawiania.
- `accounting/invoice_preview.php` — HTML preview po wystawieniu.
- `accounting/invoice_pdf.php` — template Dompdf, mirror `receipts/print.php` z dodatkowymi polami faktury VAT (parties, netto/VAT/brutto per stawka, termin platnosci).
- `accounting/invoices_issued_list.php` — lista pod `/settings/accounting/invoices/issued`.
- `orders/show.php` — checkbox toggle + warunkowy przycisk "Wystaw fakture" + sekcja "Faktury" w tabie documents.
### DI wiring (`routes/web.php`)
- `$invoiceRepository = new InvoiceRepository($app->db());` (po `InvoiceConfigRepository`).
- `$invoiceService = new InvoiceService($invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $fakturowniaIntegrationRepository, $fakturowniaApiClient);`
- `$invoiceController = new InvoiceController($template, $translator, $auth, $invoiceRepository, $invoiceConfigRepository, $companySettingsRepository, new OrdersRepository(...), $invoiceService);`
- `$ordersController` rozszerzony o 2 trailing params: `$invoiceRepository`, `$invoiceConfigRepository`.
### BREAKING / migration
- Zadnych nowych migracji — Phase 113-01 dostarczyla `orders.invoice_requested`, `invoice_configs/invoices/invoice_number_counters` i `fakturownia_integration_settings`.
- `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
### Edge cases / known limits
- INVOICE-IDEMP-115 (`.paul/codebase/todo.md`) — resolved in Phase 136 przez `external_oid`, `pending_external`, lookup po `oid` i auto-attach po timeoutcie.
- Brak `invoice.created` event automatyzacji (per Phase 113 decision).
- Brak download+cache PDF z Fakturowni — tylko redirect 302 (kazdy klik na PDF dla delegated faktury fetchuje PDF z Fakturowni).
---
## Phase 126 — Invoice GUS Field Mapping (KRS heuristic)
### MfWhitelistApiClient (`src/Core/Http/MfWhitelistApiClient.php`)
- `lookupByNip()` zwraca dodatkowo `krs: string` i `is_jdg: bool` (true gdy `subject.krs === ''`). Pozostaly kontrakt bez zmian.
- Heurystyka: JDG = brak KRS w MF. Spolka = `krs` niepuste. Pattern do reuse w przyszlych formularzach opartych o NIP lookup.
### InvoiceController::nipLookup (`/api/nip/lookup`)
- JSON `data` rozszerzony o `is_jdg: bool`. Konsumowane przez JS w `accounting/invoice_form.php`.
### invoice_form.php JS — warunkowe mapowanie pola docelowego
- `d.is_jdg=true` (JDG): MF `name` (osoba fizyczna) -> `#buyer_name` (Imie i nazwisko). `#buyer_company_name` nie ruszane (pre-fill z `order_addresses.name` zachowany — czesto trzyma pelna nazwe firmy JDG).
- `d.is_jdg=false` (spolka): MF `name` (legal name) -> `#buyer_company_name`. `#buyer_name` nie ruszane (pre-fill z zamowienia — np. osoba kontaktowa).
- Pola adresowe (street/postal_code/city) zawsze nadpisywane.
## Phase 116 - HostedSMS Integration Settings
### HostedSmsIntegrationRepository (`src/Modules/Settings/HostedSmsIntegrationRepository.php`)
- Zarzadza pojedynczym rekordem `hostedsms_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `hostedsms`.
- Szyfruje haslo przez `IntegrationSecretCipher`; formularz widzi tylko flage `has_password`.
- Udostepnia `getCredentials()` dla kontrolera testowej wysylki SMS.
### HostedSmsApiClient (`src/Modules/Settings/HostedSmsApiClient.php`)
- Wykonuje `POST https://api.hostedsms.pl/SimpleApi` jako `application/x-www-form-urlencoded`.
- Wysyla `UserEmail`, `Password`, `Sender`, `Phone`, `Message` oraz opcjonalnie `ConvertMessageToGSM7`.
- Traktuje `MessageId` jako sukces, a `ErrorMessage` jako blad biznesowy nawet przy HTTP 200.
### HostedSmsIntegrationController (`src/Modules/Settings/HostedSmsIntegrationController.php`)
- Endpointy: `GET /settings/integrations/hostedsms`, `POST /settings/integrations/hostedsms/save`, `POST /settings/integrations/hostedsms/test`.
- `test` realnie wysyla SMS z edytowalna trescia i zapisuje wynik w `integrations.last_test_*`.
### IntegrationsHubController
- Dodaje wiersz HostedSMS do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
## Phase 117 - SMSPLANET Integration Settings
### SmsplanetIntegrationRepository (`src/Modules/Settings/SmsplanetIntegrationRepository.php`)
- Zarzadza pojedynczym rekordem `smsplanet_integration_settings` (`id=1`) i bazowym wpisem `integrations` typu `smsplanet`.
- Obsluguje dwie metody autoryzacji: Bearer token oraz `key` + `password`.
- Szyfruje token, klucz API i haslo przez `IntegrationSecretCipher`; formularz widzi tylko flagi `has_api_token`, `has_api_key` i `has_api_password`.
- Udostepnia `getCredentials()` tylko dla kompletnej i aktywnej konfiguracji testowej wysylki SMS, razem z opcjonalna `default_footer`.
### SmsplanetApiClient (`src/Modules/Settings/SmsplanetApiClient.php`)
- Wykonuje `POST https://api2.smsplanet.pl/sms` jako `application/x-www-form-urlencoded`.
- Dla Bearer token wysyla naglowek `Authorization: Bearer ...`; dla `key_password` wysyla parametry `key` i `password`.
- Wysyla `from`, `to`, `msg` oraz opcjonalnie `clear_polish` i `transactional`; test nie ustawia `test=1`, wiec wysyla realny SMS.
- Traktuje `messageId` jako sukces, a `errorMsg`/`errorCode` jako blad biznesowy.
### SmsplanetIntegrationController (`src/Modules/Settings/SmsplanetIntegrationController.php`)
- Endpointy: `GET /settings/integrations/smsplanet`, `POST /settings/integrations/smsplanet/save`, `POST /settings/integrations/smsplanet/test`.
- `test` realnie wysyla SMS z edytowalna trescia, dopisuje `default_footer` gdy jest skonfigurowana i zapisuje wynik w `integrations.last_test_*`.
### IntegrationsHubController
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
## Phase 127 — polkurier Integration Settings
### Schema
- Tabela `polkurier_integration_settings` (fixed `id=1`, `integration_id INT UNSIGNED NULL UNIQUE FK -> integrations(id) CASCADE`, `login`, `api_token_encrypted`, `default_label_format`).
- Pojedynczy rekord `integrations.type='polkurier'`, `name='polkurier'`, `base_url='https://api.polkurier.pl/'` (mirror Apaczki/HostedSMS/SMSPLANET).
- Migracja `20260514_000114_create_polkurier_integration_settings.sql` jest idempotentna (`CREATE TABLE IF NOT EXISTS` + `INSERT ... ON DUPLICATE KEY UPDATE`).
### PolkurierIntegrationRepository (`src/Modules/Settings/PolkurierIntegrationRepository.php`)
- Konstruktor `(PDO $pdo, string $secret)` — buduje wewnetrznie `IntegrationsRepository` i `IntegrationSecretCipher` (mirror `HostedSmsIntegrationRepository`).
- `getSettings()` zwraca `login`, `default_label_format`, flage `has_api_token: bool` (NIE plaintext), `is_active`, `last_test_*`.
- `saveSettings($payload)` waliduje `login` (<=190 znakow) i `default_label_format` (PDF/ZPL/EPL), szyfruje Token API; gdy token w payloadzie jest pusty -> nie nadpisuje istniejacego (BC).
- `getCredentials()` zwraca odszyfrowany `login + api_token + default_label_format` TYLKO gdy `is_active=1` i token istnieje; inaczej `null`. Konsumowane przez `PolkurierApiClient::testConnection()` i przyszly `PolkurierShipmentService`.
- `getIntegrationId()` — single source of truth dla przyszlych modulow.
### PolkurierApiClient (`src/Modules/Settings/PolkurierApiClient.php`)
- Kontrakt API zweryfikowany na podstawie oficjalnego SDK (https://github.com/Polkurier/polkurier-sdk): jedno publiczne POST endpoint `https://api.polkurier.pl/`, JSON body `{"authorization": {"login", "token"}, "apimetod": "<method>", "data": {...}}`.
- `testConnection(login, apiToken)` wywoluje `apimetod="test_auth_api"` z `data={platform: 'orderPRO', platform_version: '1.0'}`; sukces gdy `status='ok'` lub `response.authorization` niepusta.
- cURL z `SslCertificateResolver::resolve()`, `CURLOPT_TIMEOUT=$timeoutSeconds` (default 15), `CURLOPT_SSL_VERIFYPEER=true`, `Content-Type: application/json`. PHP 8.5 compatible (brak `curl_close()`).
- Stuby `createShipment()`, `getLabel()`, `getStatus()`, `cancelOrder()` rzucaja `RuntimeException("Not implemented in Phase 127")` — dolozone w kolejnych fazach.
### PolkurierIntegrationController (`src/Modules/Settings/PolkurierIntegrationController.php`)
- Endpointy: `GET /settings/integrations/polkurier`, `POST /settings/integrations/polkurier/save`, `POST /settings/integrations/polkurier/test`.
- `test` realnie wywoluje API polkurier i zapisuje wynik w `integrations.last_test_*` przez `IntegrationsRepository::updateTestResult()`.
- Flash przez legacy `Flash::set('settings_success'|'settings_error'|'polkurier_test', ...)` — spojnie z HostedSMS/SMSPLANET; renderer flash w `layouts/app.php` (Phase 120) obsluguje BC mapping przez `Flash::all()`.
- Widok `resources/views/settings/polkurier.php` uzywa wylacznie komponentu `resources/views/components/alert.php` (Phase 120 contract).
### IntegrationsHubController (Phase 127 patch)
- Dodany parametr `PolkurierIntegrationRepository $polkurier`.
- Metoda `buildPolkurierRow()` zwraca te same klucze co `buildApaczkaRow()` (`provider`, `instance`, `authorization_status`, `secret_status`, `is_active`, `last_test_at`, `configure_url`).
- Wiersz polkurier wstawiony zaraz po Apaczka (sasiednio — semantycznie oba to brokery kurierskie).
### Boundaries / co NIE zostalo dotkniete
- `ShipmentProviderRegistry` i `src/Modules/Shipments/*``PolkurierShipmentService` nie istnieje w Phase 127. Tworzenie przesylek, etykiety, tracking i mapowania metod dostawy beda dodane w kolejnej fazie.
- `apaczka_integration_settings`, `ApaczkaShipmentService`, `ApaczkaTrackingService` — Apaczka netknieta, dziala rownolegle.
- `delivery_status_mappings` — brak nowych wpisow `provider='polkurier'` (dolozone razem z tracking service w kolejnej fazie).
## Phase 128 — polkurier ShipmentService + Tracking + UI prepare
### PolkurierApiClient (Phase 128 extension)
- 6 nowych metod publicznych obok zachowanego `testConnection()`:
- `getAvailableCarriers($login, $token)``apimetod=available_carriers`. Zwraca tablice przewoznikow z polami `servicecode`, `name`, `additional_data`, `foreign_shipments`. Konsumowane przez `PolkurierShipmentService::getDeliveryServices()`.
- `createShipment($login, $token, $payload)``apimetod=create_order`. Payload zgodny z oficjalna doca PDF v1.11 (zweryfikowany): `shipmenttype` (lowercase: box/envelope/palette/small_parcel/parcel_size_20), `courier` (servicecode), `description`, `sender`/`recipient` (company/person/street/housenumber/flatnumber/postcode/city/email/phone/country/point_id), `packs[]` (length/width/height/weight/amount/type), `pickup` (pickupdate/pickuptimefrom/pickuptimeto/nocourierorder), opcjonalnie `COD` i `insurance`.
- `getLabel($login, $token, $orderno)``apimetod=get_label`. **API przyjmuje WYLACZNIE `orderno: Array<String>`** (zweryfikowane w dokumentacji PDF). Rozmiar etykiety A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API. Odpowiedz: `{file: <base64 PDF>}`.
- `getStatus($login, $token, $orderno)``apimetod=get_status`. Zwraca `{url, status_date, status, status_code, delivered_date}`. Kody w tabeli ORDER_STATUS (O/P/A/WP/D/Z/W).
- `cancelOrder($login, $token, $orderno)``apimetod=cancel_order`. Zwraca `{cancellation: true}`. Nie wywolywane przez nasz kod w Phase 128 (operator anuluje w panelu polkuriera).
- `getInpostParcelMachines` + `getCourierPoints` — stuby na przyszle rozszerzenie UI (panel paczkomatow). Aktualnie nie wykorzystywane w UI (operator wpisuje `receiver_point_id` recznie w sekcji Adres odbiorcy).
- Wspolny prywatny `call($apimetod, $data, $login, $token): mixed` parsuje envelope `{status, response}`. Sukces -> zwraca tresc `response`. Blad -> rzuca `RuntimeException` z trescia z `response` (string albo zserializowany JSON dla tablic).
### PolkurierShipmentService (`src/Modules/Shipments/PolkurierShipmentService.php`)
- `final class implements ShipmentProviderInterface` (`code()='polkurier'`). DI: `PolkurierIntegrationRepository`, `PolkurierApiClient`, `ShipmentPackageRepository`, `CompanySettingsRepository`, `OrdersRepository`.
- `getDeliveryServices()` cache'uje per-request liste z `available_carriers`, normalizuje do `[{id, name, supports_pickup_point, point_courier, foreign_shipments, raw}]`. Pole `supports_pickup_point` to heurystyka po `servicecode`/`name` (paczkomat/parcel/inpost/orlen/pocztex/kurier48/punkt).
- `createShipment($orderId, $formData)` orchestruje pelny flow:
1. Walidacja: order istnieje, `service_code`/`delivery_method_id` niepusty, credentials aktywne, sender ma street/city/postcode + name|company.
2. Mapowanie `package_type` przez `normalizeShipmentType()` na zbior `[box, envelope, palette, small_parcel, parcel_size_20]` (lowercase, aliases dla PACKAGE/PARCEL/PACZKA/...).
3. Buduje `recipient` z `order_addresses` (delivery → fallback customer) + override z formularza. Splituje ulice na `street`/`housenumber`/`flatnumber` regexem (`Marszalkowska 10/5` → street="Marszalkowska", house="10", flat="5").
4. `pickup` default: `nextBusinessDay()` + 10:00-16:00, `nocourierorder=false` (override mozliwy przez formularz: `pickup_date`, `pickup_time_from`, `pickup_time_to`, `no_courier_order`).
5. `COD` jezeli `cod_amount > 0` (codtype='transfer', codbankaccount z `company_settings.bank_account` po stripowaniu nie-cyfr; throw `ShipmentException` jezeli pusty).
6. INSERT do `shipment_packages` (provider='polkurier', status='pending', payload_json z pelnym requestem).
7. Wywolanie `apiClient->createShipment()`. On success: parsing przez `extractOrderNumber()` (priorytet `number` → Order entity z SDK, fallback `orderno`/`order_no`/`order_number`/`order_id`/`id`, obsluga wrappera `{order:{...}}` i list) i `extractTrackingNumber()` (priorytet `waybills[0].number` → OrderWaybill entity, fallback top-level klucze).
8. UPDATE `shipment_packages`: `status='created'`, `shipment_id=command_id=orderno`, `tracking_number`.
9. Synchroniczna proba `downloadLabel()` (niekrytyczna — przy bledzie ignoruje, operator klikni "Pobierz" pozniej).
10. Diagnostyka: gdy `orderno=''` mimo `status=success`, zapisuje fragment surowej odpowiedzi (400 znakow) do `shipment_packages.error_message` dla debug.
- `downloadLabel($packageId, $storagePath)`: wywoluje `get_label`, parsuje `extractLabelBase64()` (priorytet klucz `file` — zweryfikowane w SDK GetLabel.php, fallback `label`/`pdf`/`data`/`content`/`zpl`/`epl`), `base64_decode`, zapis do `storage/labels/polkurier_{packageId}_{orderno}.pdf`, UPDATE `label_path`.
- `checkCreationStatus($packageId)`: graceful — przy bledzie API zwraca zachowany `tracking_number` z DB (cron sam zaktualizuje przez TrackingService).
### PolkurierTrackingService (`src/Modules/Shipments/PolkurierTrackingService.php`)
- `final class implements ShipmentTrackingInterface`. DI: `PolkurierApiClient`, `PolkurierIntegrationRepository`, `DeliveryStatusMappingRepository`.
- `supports('polkurier')`. `getDeliveryStatus($package)` woła `get_status` po `shipment_id`/`command_id` (orderno), parsuje `status_code` z `response` (z obsluga listy w response[0]).
- Mapowanie surowego `status_code` (O/P/A/WP/D/Z/W) → znormalizowany przez `DeliveryStatus::normalizeWithOverrides('polkurier', $rawStatus, $overrides)` z DB. Seed mapowan w migracji `20260514_000115`.
- Odporny na bledy: brak credentials → `null` (skip), wyjatek API → `null`, brak `status_code``null`. Cron nie crashuje.
### DeliveryStatus::trackingUrl (Phase 128 patch)
- Carrier_id routing (DPD/UPS/GLS/InPost/Pocztex/...) dziala dla polkuriera automatycznie przez istniejacy `matchCarrierByName($encoded, $carrier)` (carrier_id ustawiany na servicecode z polkuriera, np. "INPOST", "DPD" — pasuje do substring matchu).
- Fallback dla provider='polkurier' bez carrier matchu: `https://polkurier.pl/sledz-paczke/<tracking>`.
### Wiring
- `routes/web.php`: `new PolkurierShipmentService(...)` zarejestrowany w `ShipmentProviderRegistry` obok Apaczki/InPost/AllegroWZA.
- `src/Modules/Cron/CronHandlerFactory.php`: `new PolkurierTrackingService(...)` w `ShipmentTrackingRegistry` w `shipment_tracking_sync` handler.
- `src/Modules/Shipments/ShipmentController.php`: `prepare()` fetchuje `polkurierServices` przez registry i przekazuje do widoku. `create()` rozszerzony o `service_code`/`pickup_date`/`pickup_time_from`/`pickup_time_to` przekazywane do `createShipment()`.
### UI prepare panel (`resources/views/shipments/prepare.php`)
- Opcja "polkurier" w dropdownie `#shipment-carrier-select` (obok Allegro/InPost/Apaczka).
- `<div id="shipment-polkurier-panel">` z dynamicznym `<select id="shipment-polkurier-select">` (lista uslug z `available_carriers`).
- `<input type="hidden" name="service_code">` synchronizowany z polkurier select przez `syncPolkurierFields()`.
- Brak dedykowanego selektora punktu odbioru — operator wpisuje `receiver_point_id` w istniejacy text input w sekcji Adres odbiorcy (np. `POP-RZE54`). Format string `POP-RZE54 | Lukasiewicza 78, 35-604 Rzeszow` z importu zamowienia nie jest parsowany — operator skraca recznie.
- JS toggle widocznosci paneli rozszerzony o polkurier; `clearHiddenFields()` czysci `service_code`; `showPanel('polkurier')` ustawia `provider_code='polkurier'`.
### Rozmiar etykiety A4 vs A6
- API polkurier nie udostepnia parametru sterowania rozmiarem etykiety w `get_label` ani `create_order` (zweryfikowane w PDF v1.11).
- Domyslny rozmiar ustawiany jest w **panelu klienta polkurier.pl → Ustawienia konta → Preferencje etykiet** (per-konto, globalnie dla wszystkich `get_label` calli).
- `polkurier_integration_settings.default_label_format` (PDF/ZPL/EPL) sluzy tylko typowi pliku, NIE rozmiarowi.
### Seed delivery_status_mappings (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`)
- 7 wpisow `provider='polkurier'` (kody z oficjalnej tabeli ORDER_STATUS w PDF v1.11):
- `O``created` (Oczekuje na platnosc)
- `P``confirmed` (Potwierdzone, list wygenerowany)
- `A``cancelled` (Anulowane)
- `WP``in_transit` (W przewozie)
- `D``delivered` (Dostarczona)
- `Z``returned` (Zwrot do nadawcy)
- `W``problem` (Wyjatek)
- Idempotentne: `ON DUPLICATE KEY UPDATE normalized_status / description / updated_at`.
### Boundaries / co NIE zostalo zmienione
- Apaczka (`ApaczkaShipmentService`, `ApaczkaTrackingService`, `apaczka_integration_settings`) niezalezna, dziala obok polkuriera.
- `ShipmentProviderInterface` i `ShipmentTrackingInterface` kontrakty niezmienione.
- `getInpostParcelMachines`/`getCourierPoints` w API client zaimplementowane ale nieuzywane przez UI w Phase 128 (operator wpisuje punkt recznie).
- `cancelOrder` zaimplementowane w API client ale nie wywolywane z UI/cron — operator anuluje w panelu polkuriera.
- Brak presetow przesylek dla polkuriera (`shipment_presets.provider_code='polkurier'`) — kolejna faza.
## Phase 121 - SMSPLANET Conversation + Notifications
### SmsConversationService (`src/Modules/Sms/SmsConversationService.php`)
- Wysyla SMS z poziomu zamowienia przez `SmsplanetApiClient`, dopisuje `default_footer` gdy jest skonfigurowana, zapisuje finalna tresc w `sms_messages` i uzywa `sender_mode` do wyboru nadpisu albo numeru 2WAY.
- Parsuje publiczny webhook `/webhooks/smsplanet/inbound`, normalizuje telefony i dopasowuje przychodzacy SMS do najnowszego zamowienia po telefonie klienta/adresu.
- Endpoint inbound akceptuje POST i GET; format 2WAY `message=<JSON>` jest dekodowany, sukces zwraca plain `OK`, a dopasowanie zamowienia korzysta z `order_addresses.phone`.
- Tworzy `notifications.type='sms_inbound'` z linkiem do `/orders/{id}?tab=sms`.
### Notifications module
- `/notifications` pokazuje historie powiadomien i pozwala oznaczac wpisy jako przeczytane.
- `/api/notifications/unread` zasila topbar badge oraz `public/assets/js/modules/notifications.js`.
- Browser Notification API jest progresywne: brak zgody nie blokuje strony ani pollingu.
## Phase 123 — Receipts Export VAT Breakdown
### ReceiptService::buildItemsSnapshot (`src/Modules/Accounting/ReceiptService.php`)
- Snapshot pozycji w `receipts.items_json` ma teraz pole `vat` (procent jako float). Zrodlo: `order_items.tax_rate` (fallback `item.vat`, ostatecznie 23.0).
- Pozycja "Koszt wysylki" (gdy `delivery_price > 0`) dostaje `vat = 23.0`.
- Stary kontrakt (`name`, `quantity`, `price`, `total`, `sku`, `ean`) zachowany — tylko dodatek pola `vat`. Widoki paragonu (print/preview) nie wymagaja zmian.
### AccountingController::export (`src/Modules/Accounting/AccountingController.php`)
- Naglowki XLSX: `Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT`. Usunieto: Data sprzedazy, Konfiguracja, Nr zamowienia, Nr referencyjny.
- `buildVatBreakdown(itemsJson, totalNet, totalGross)` grupuje pozycje `items_json` po `vat`, oblicza per-grupa `net = round(gross / (1 + rate/100), 2)` i `vat = gross - net`. Zwraca liste `[{rate_label, net, vat}, ...]` posortowana malejaco po stawce.
- Legacy fallback: gdy zaden item nie ma klucza `vat`, zwraca pojedynczy wiersz `[{rate_label: '23%', net: total_net, vat: total_gross - total_net}]`.
- Multi-rate paragon = wiele wierszy w XLSX (ten sam Numer, Data wystawienia i Kwota brutto powtarzane).
- Helper `formatVatRate()` formatuje stawke (23.0 -> "23%", 7.5 -> "7.5%").
## Phase 135 — Accounting Net Correctness
### ReceiptService::buildItemsSnapshot (`src/Modules/Accounting/ReceiptService.php`)
- Dla nowych paragonow snapshot nadal zachowuje kontrakt `name`, `quantity`, `price`, `total`, `vat`, `sku`, `ean`.
- Metoda zwraca teraz takze `total_net`; `ReceiptService::issue()` zapisuje `receipts.total_net` z sumy netto per linia zamiast kopiowac brutto.
- Netto pozycji: `lineGross / (1 + vat/100)` z VAT z `tax_rate`/`vat`; brak stawki oznacza fallback 23.0.
- Koszt wysylki pozostaje osobna pozycja "Koszt wysylki" z VAT 23.0; operator zdecydowal, ze historyczne paragony nie sa backfillowane.
### OrdersStatisticsRepository::netAmountSql (`src/Modules/Statistics/OrdersStatisticsRepository.php`)
- Statystyki dzienne preferuja `orders.total_without_tax`, potem `orders.total_net`, jezeli wartosc jest dodatnia.
- Gdy net z zamowienia jest pusty, repozytorium liczy fallback z `order_items`: najpierw `original_price_without_tax * quantity`, potem `original_price_with_tax * quantity / (1 + tax_rate/100)`.
- Dla pozycji bez VAT fallback stawki wynosi 23.0; dostawa bez osobnej stawki jest doliczana jako `delivery_price / 1.23`.
- Stare `gross / 1.23` pozostaje tylko jako ostatni fallback dla legacy zamowien bez uzywalnych pozycji.
## Phase 120 — Alert Component Unification
### Alert component (`resources/views/components/alert.php`)
- Reusable alert renderer with params: `$type` (info|success|warning|danger; fallback 'info'), `$message` (escaped) lub `$messageHtml` (trusted), `$dismissible` (default true), `$role` ('alert'|'status').
- Renders inline SVG icon per type + body + optional dismiss button. Markup: `<div class="alert alert--TYPE" data-alert>...<button data-alert-dismiss>...</button></div>`.
- Used via `include __DIR__ . '/../components/alert.php'` po ustawieniu lokalnych `$type/$message/$dismissible`.
### SCSS — `.alert` w `resources/scss/shared/_ui-components.scss`
- `.alert` jest teraz flex (icon + body + dismiss). Dodane: `.alert__icon`, `.alert__body`, `.alert__dismiss`.
- Nowy wariant `.alert--info` (blue: border #bfdbfe, bg #eff6ff, color #1e3a8a) — wczesniej brakowal i renderowal sie jako czarny tekst na bialym tle.
- Wariantow `--success/--warning/--danger` nie zmieniono kolorystycznie.
- Wrapper `.alerts-stack` (gap 8px) do stackowania wielu alertow z layoutu.
### JS — `public/assets/js/modules/alert-dismiss.js`
- Vanilla JS, idempotent guard (`window.__alertDismissBound`).
- Delegated click handler na `[data-alert-dismiss]` — usuwa najblizszy `[data-alert]` z DOM bez przeladowania.
- Ladowany globalnie w `layouts/app.php`, `layouts/auth.php`, `layouts/public.php`.
### Flash — `App\Core\Support\Flash` rozszerzenie
- Nowa kolejka typowana `$_SESSION['_flash_queue']` z entries `{type, message}`.
- `Flash::push(string $type, string $message): void` — append do kolejki (whitelist info/success/warning/danger, fallback info).
- `Flash::all(): array` — zwraca i czysci kolejke + skanuje legacy `_flash` (heurystyka klucza: `error/fail/danger` → danger, `warning` → warning, `success/.save/.created/.deleted/.toggled` → success, reszta → info). BC zachowany: `Flash::set/get` dziala bez zmian.
### Centralny renderer flash w layoutach
- `layouts/app.php`, `layouts/auth.php`, `layouts/public.php` na poczatku glownego content area iteruja `Flash::all()` i wlaczaja komponent `alert.php` per wpis (wrap `.alerts-stack`).
- Kontrolery NIE wymagaly zmian — pre-fetched `Flash::get('module.key', '')` przekazany do widoku jako lokalna zmienna jest dalej renderowany inline przez widok (przez ten sam komponent). Centralny renderer przejmuje wpisy `Flash::push(...)` oraz nieskonsumowane legacy entries.
### Migracja widokow
- Wszystkie inline `<div class="alert alert--TYPE">...</div>` w widokach (36 plikow razem ze `shipments/prepare.php` i `orders/show.php`) zastapione przez `<?php $type=...; $message=...; $dismissible=...; include dirname(__DIR__) . '/components/alert.php'; ?>`.
- `.flash--error` / `.flash--success` w `orders/show.php` i `shipments/prepare.php` zastapione komponentem (klasa `.flash--*` w SCSS pozostaje bez uzycia, deferred cleanup).
- Wyjatek: `settings/email-mailboxes.php` ma JS-generowane alerty (`resultDiv.className = 'mt-12 alert alert--success'`) z dynamicznej odpowiedzi AJAX test polaczenia SMTP — uzywaja klas SCSS bez markupu komponentu (out of scope dla tej fazy).
## Phase 114 — Accounting Configs Refactor
### Sekcja Ksiegowosc — struktura URL
- `/settings/accounting` — hub-rozdroze z 2 kartami: "Paragony" i "Faktury". `ReceiptConfigController::hub()`.
- `/settings/accounting/receipts` — lista konfiguracji paragonow. `ReceiptConfigController::list()`.
- `/settings/accounting/receipts/new`, `/edit?id=N` — formularz na osobnej podstronie. `ReceiptConfigController::edit()`.
- `/settings/accounting/receipts/save|toggle|delete` — POST actions.
- **Legacy aliasy:** `/settings/accounting/save|toggle|delete` (POST) zostaja jako duplicate routes (wsteczna kompatybilnosc z `<form action>` w starszych szablonach/bookmarkach).
- `/settings/accounting/invoices` + `/new`, `/edit`, `/save`, `/toggle`, `/delete` — analogicznie dla `invoice_configs`. `InvoiceConfigController`.
### InvoiceConfigRepository (`src/Modules/Settings/InvoiceConfigRepository.php`)
- `listAll()` JOIN `invoice_configs LEFT JOIN integrations` (`type='fakturownia'`) — zwraca `integration_name` gdy `is_delegated=1`.
- `save(array $data): int` — walidacja serwerowa wszystkich pol. Krytyczna regula: gdy `is_delegated=1` musi byc `integration_id > 0` wskazujacy na `integrations.type='fakturownia'`, inaczej rzuca `IntegrationConfigException`. Gdy `is_delegated=0`, ignoruje `integration_id` (NULL).
- `toggleStatus(int $id)` przez `ToggleableRepositoryTrait::toggleActive()`.
- `delete(int $id)` — pre-check `SELECT 1 FROM invoices WHERE config_id` zeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE z FK RESTRICT.
### Seed
- Migracja `20260511_000107_seed_default_invoice_config.sql` — idempotentny insert `Domyslny VAT` (NOT EXISTS guard, `invoice_configs.name` nie jest UNIQUE).
### invoice-config-form.js (`public/assets/js/modules/invoice-config-form.js`)
- Vanilla JS modul ladowany globalnie przez `layouts/app.php`.
- Toggle widocznosci `[data-invoice-delegation]` wrappera w zaleznosci od stanu `[data-invoice-delegated]` checkboxa.
- Ustawia `select[name=integration_id].required` zgodnie ze stanem checkboxa; przy unchecked czysci `value`.
### Ujednolicony wyglad list paragonow/faktur
- Tabela `table.table` w `table-wrap`, badge `badge--{success,muted}` na statusy.
- Edycja przez `<a href=".../edit?id=N">`, toggle/delete przez `<form>` z `_token` i `js-confirm-delete`.
- Wspolny pattern miedzy `accounting-receipts.php` i `accounting-invoices.php` (faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni).
## Phase 124 — SMS Templates
### SmsTemplateRepository (`src/Modules/Sms/SmsTemplateRepository.php`)
- CRUD na `sms_templates` (PDO prepared statements, ToggleableRepositoryTrait).
- `listAll()` (cala lista alfabetycznie po `name`), `listActive()` (tylko is_active=1, kolumny `id|name|body` do dropdownu w UI).
- `save(array): int` waliduje wymagane `name` + `body` (rzuca `RuntimeException` gdy puste); wykonuje INSERT albo UPDATE wg obecnosci `id` w payloadzie; zwraca id rekordu.
- `delete(int)`, `toggleStatus(int)` przez `toggleActive('sms_templates', $id)`.
### SmsVariableResolver (`src/Modules/Sms/SmsVariableResolver.php`)
- Wydzielony z `Email\VariableResolver` — wspolna logika zmiennych dla Email i SMS.
- `buildVariableMap(order, addresses, companySettings)` zwraca mape placeholderow: `zamowienie.*`, `kupujacy.*`, `adres.*`, `firma.*`, `przesylka.*` (`przesylka.numer`/`przesylka.link_sledzenia` z najnowszej paczki przez `ShipmentPackageRepository::findLatestByOrderId` + `DeliveryStatus::trackingUrl`).
- `resolve(template, variableMap)` zastepuje `{{group.var}}` wartoscia z mapy (puste gdy brak klucza).
### Email\VariableResolver (refaktor)
- Pozostaje final class z tym samym API publicznym (`buildVariableMap`/`resolve`) — `EmailSendingService` niezmieniony.
- Konstruktor: `(ShipmentPackageRepository $repo, ?SmsVariableResolver $inner = null)`. Gdy `$inner` nie podany, sam tworzy SmsVariableResolver — backward compat dla starego wiringu.
- Metody publiczne deleguja do `$this->inner` — zero duplikacji logiki zmiennych.
### SmsTemplateController (`src/Modules/Settings/SmsTemplateController.php`)
- Mirror `EmailTemplateController` bez Quill/skrzynki/zalacznika/duplikacji.
- Akcje: `index` (lista), `create`/`edit`/`save` (form CRUD), `delete`, `toggleStatus` (AJAX JSON), `getVariables` (JSON paleta dla ewentualnego dynamic palette).
- `VARIABLE_GROUPS` jako stala klasy — pelne 5 grup (zamowienie/kupujacy/adres/firma/przesylka) zgodnie ze wspolnym SmsVariableResolver.
- Routy: `/settings/sms-templates`, `/create`, `/edit`, `/save`, `/delete`, `/toggle`, `/variables`. CSRF `_token` na POST. Flash `settings.sms_templates.success|error`.
### OrdersController (rozszerzenie)
- Dodane optional params konstruktora: `?SmsTemplateRepository $smsTemplates`, `?SmsVariableResolver $smsVariableResolver`, `?CompanySettingsRepository $companySettingsRepo` (po istniejacych SMS params; default null = backward compat).
- `show()` przekazuje `$smsTemplates` (list active) do widoku jako `smsTemplates`.
- Nowa metoda `smsTemplate(Request)` -> `GET /orders/{id}/sms/template?template_id=N` -> JSON `{ok, body, name}` z rozwinietymi zmiennymi. 400/404/500 dla nieprawidlowych parametrow/braku rekordu.
### Widok `orders/show.php`
- Nad textarea `name="message"` (`#js-sms-message`) dodany conditional `<select data-sms-template-picker data-order-id data-message-target="js-sms-message">` z opcja domyslna + aktywne szablony (renderowany tylko gdy `$smsTemplatesList !== []`).
- Textarea ma teraz `id="js-sms-message"` — JS target.
### Frontend module `public/assets/js/modules/sms-template-picker.js`
- Vanilla JS, idempotent guard `window.__smsTemplatePickerBound` + per-element `dataset.smsPickerBound`.
- Na `change` selecta: fetch `/orders/{id}/sms/template?template_id=N`, podstaw body do textarea, fire `input` event.
- Gdy textarea ma juz tresc -> `OrderProAlerts.confirm({...})` options-object API (Phase 114 pattern). Po zatwierdzeniu nadpisuje, po anulowaniu resetuje select. Fallback na natywny `confirm()`.
- Ladowany globalnie z `layouts/app.php` (linia po `notifications.js`).
### Wspolny resolver — wiring DI (`routes/web.php`)
- `$smsVariableResolver = new SmsVariableResolver($shipmentPackageRepositoryForOrders);`
- `$variableResolver = new VariableResolver($shipmentPackageRepositoryForOrders, $smsVariableResolver);` (drugi argument opcjonalny dla BC).
- `$smsTemplateRepository = new SmsTemplateRepository($app->db());`
- `$smsTemplateController = new SmsTemplateController($template, $translator, $auth, $smsTemplateRepository);`
- `$ordersController` rozszerzony o 3 trailing params (smsTemplateRepository, smsVariableResolver, companySettingsRepository).
### SCSS — `_sms-templates.scss`
- Nowy partial `resources/scss/modules/_sms-templates.scss` z klasami `.sms-template-*` (active label, counter, body grid) oraz `.sms-var-panel/.sms-var-group/.sms-var-item` dla palety zmiennych.
- Import w `app.scss` po `customer-risk-alert`.
### Stopka — preserved Phase 122 contract
- Szablony SMS NIE zawieraja `default_footer` — operator wpisuje sama tresc.
- `SmsConversationService::buildFinalOutboundBody()` dokleja stopke raz przy `sendFromOrder()` (po wstawieniu szablonu i ewentualnej edycji przez operatora). Walidacja `MAX_SMS_LENGTH = 918` obowiazuje na finalnej tresci.
### BREAKING / migration
- Migracja `20260512_000112_create_sms_templates.sql``CREATE TABLE IF NOT EXISTS sms_templates` (DDL, brak SELECT no-op).
- Brak innych zmian schematu. `OrdersController` ctor: 3 NEW optional params (default null) — backwards compatible.

View File

@@ -1,122 +0,0 @@
# Technical Concerns & Debt
## Status audytu Phase 134 (2026-05-16)
Szczegoly i dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
| Group / item | Status po audycie | Krotki wniosek |
|--------------|-------------------|----------------|
| God Classes | **Active** | Klasy nadal sa duze; stare LOC/method counts sa nieaktualne, ale Phase 141 pozostaje zasadny. |
| SonarQube Issues | **Fresh baseline / active patterns** | Phase 139 odswiezyl baseline i zredukowal dwie fale issue; pozostale grupy sa aktualne po skanie 139-02 z 2026-05-17. |
| Breaking: delivery status group keys | **Closed in Phase 137** | DB-driven statusy sa wdrozone, a read-only DB check nie znalazl starych ani niepoprawnych kluczy automatyzacji. |
| Breaking: `SHIPMENT_STATUS_OPTION_MAP` | **Implemented / stale** | Symbol nie wystepuje juz w runtime source. |
| Breaking: `_csrf_token` -> `_token` | **Implemented / stale** | Formularze/kontrolery uzywaja `_token`; wewnetrzny session key w `Csrf` nie jest problemem formularzy. |
| Known Bugs: `STAT-NET` | **Resolved in Phase 135** | Runtime statystyk liczy net z source-level net albo item-level VAT; `RECEIPT-NET-FIX` naprawiony dla nowych paragonow bez backfillu historii. |
| Deferred Indexes | **Active / deferred** | Indeksy nadal nie sa w migracjach; wykonac po decyzji operatora w Phase 140. |
| Security: print API keys | **Implemented / stale** | Przechowywany jest hash i prefix, nie raw `api_key`. |
| Security: mailbox TLS | **Resolved in Phase 138** | Test SMTP uzywa strict peer/name verification dla `ssl` i STARTTLS; self-signed/unverified tylko przez lokalny `SMTP_ALLOW_SELF_SIGNED_DEV`. |
| Security: template variables | **Resolved in Phase 138** | Nowe/edytowane szablony e-mail/SMS blokuja nieznane `{{grupa.zmienna}}` przez wspolny `TemplateVariableCatalog`. |
| Architecture Concerns | **Active / low impact** | Zostawic do decyzji w Phase 142. |
| Duplication Areas | **Mixed** | `SslCertificateResolver` i `RedirectPathResolver` sa czesciowo wdrozone; reszta wymaga selektywnej decyzji. |
| Legacy patterns | **Mostly resolved in Phase 139-02** | Raw `$_SESSION` jest izolowany w `Session`; targetowane hard `require`/alert includes i inline FQCN w widokach sa usuniete przez `$component()` i lokalne importy. |
| Performance Risks | **Active / needs profiling** | Return-risk indexes i cron backoff aktywne; `findDetails()` najpierw profilowac. |
## God Classes (Priority Refactor Targets)
| Class | LOC | Methods | Issue |
|-------|-----|---------|-------|
| `src/Modules/Orders/OrdersRepository.php` | 1,221 | 29 | Query building spread across 29 methods; SonarQube S1448 |
| `src/Modules/Orders/OrdersController.php` | 1,187 | 22 | UI + AJAX + list + detail + search combined; S1448 |
| `src/Modules/Automation/AutomationService.php` | 834 | 24 | All action handlers in one class; S1448 |
| `src/Modules/Settings/ShopproOrderMapper.php` | 867 | 25 | 25+ transformation methods; S1448 |
| `src/Modules/Statistics/OrdersStatisticsRepository.php` | 901 | 43 | Reporting SQL, schema detection and row mapping combined; S1448 remains after Phase 139-01 |
| `src/Modules/Settings/ApaczkaShipmentService.php` | 1,044 | — | API payload deeply nested |
| `src/Modules/Settings/ShopproIntegrationsController.php` | 1,044 | — | OAuth + mapping + sync combined |
**Planned fix:** Phase 68 (Code Deduplication Refactor) — deferred, never started.
## SonarQube Issues (new code since 2026-03-28)
Fresh Phase 139 baseline after plan 139-02: **495 OPEN BLOCKER/CRITICAL/MAJOR issues** (BLOCKER=0, CRITICAL=178, MAJOR=317).
| Rule | Count | Severity | Examples |
|------|-------|----------|---------|
| `php:S1142` — Excess return statements | 148 | MAJOR | Many service/controller methods still have 4+ returns |
| `php:S1192` - Duplicated string literals | 98 | CRITICAL | Route paths, SQL fragments, status strings, HTTP headers |
| `php:S4833` - Use namespace import / direct include patterns | 3 | MAJOR | Remaining issues are core framework `require` calls in Application/Translator/Template |
| `php:S3776` — Cognitive complexity > 15 | 54 | CRITICAL | Mapper/service/reporting methods needing focused refactor |
| `php:S1172` — Unused parameters | 41 | MAJOR | Handler payload/request params |
| `php:S112` - Generic exceptions | 23 | MAJOR | Remaining generic exceptions outside selected compact Settings/Automation clusters |
| `php:S1448` — Class too large | 16 | MAJOR | See god classes above |
| `php:S4423` — Weak TLS protocol | stale | **CRITICAL** | Resolved in Phase 138: `EmailMailboxController::testConnection()` uzywa strict SSL context i STARTTLS |
| `Web:TableHeaderHasIdOrScopeCheck` | 16 | MAJOR | Tables without explicit header scope/id |
| `Web:S6819` — Accessibility | 5 | MAJOR | Use semantic output/status elements where applicable |
Phase 139-01 reduced the fresh total by 43 issues and cleared all selected delivery-status files. Phase 139-02 reduced the post-139-01 total by 110, mainly through broad `$component()` alert rendering and typed exceptions. Remaining detailed baseline: `.paul/phases/139-sonar-critical-major-cleanup/SONAR-BASELINE.md`.
## Breaking Changes
| Change | Phase | Impact | Migration |
|--------|-------|--------|-----------|
| Delivery status group keys przeniesione do DB | Phase 108 (2026-04-27), verified Phase 137 (2026-05-17) | Stare reguly automation z kluczami `registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return` byly ryzykiem po breaking change | Zamkniete: read-only DB check znalazl 0 starych i 0 niepoprawnych kluczy |
| `SHIPMENT_STATUS_OPTION_MAP` usunięty | Phase 108 (2026-04-27) | `AutomationService` porównuje klucze statusów bezpośrednio z DB | Brak wpływu po odtworzeniu reguł |
| `_csrf_token``_token` | Phase 105 (2026-04-19) | Stare nazwy pól formularzy | Sprawdzić czy nie ma starych referencji `_csrf_token` w widokach |
## Known Bugs & Issues
| Issue | Location | Status |
|-------|----------|--------|
| `STAT-NET`: hardcoded 23% VAT fallback for net calculations | `src/Modules/Statistics/OrdersStatisticsRepository.php` | Fixed in Phase 135; gross `/1.23` remains only as legacy fallback without usable items |
| Missing net amounts for shopPRO orders | `.paul/codebase/todo.md` (STAT-NET) | Runtime fallback fixed in Phase 135 through `order_items` VAT calculation |
| `order.status_aged` condition fallback | `AutomationService` | Fixed 2026-04-25 |
## Deferred Indexes (Phase 106)
After Phase 106 (customer return alert), two indexes should be added before dataset exceeds ~50k orders:
```sql
-- INDEX-106-01 (deferred in SUMMARY.md)
CREATE INDEX idx_order_addresses_order_type ON order_addresses(order_id, address_type);
CREATE INDEX idx_shipment_packages_order_delivery ON shipment_packages(order_id, delivery_status);
```
These support the correlated subquery in `OrdersRepository` used for return-risk detection.
## Security Items to Verify
| Item | Risk | Action |
|------|------|--------|
| `print_api_keys.api_key` encryption | MEDIUM | Verify column is encrypted (same as `integrations.api_key_encrypted`) |
| SMTP TLS in `EmailMailboxController::testConnection()` | MEDIUM | Resolved in Phase 138: strict certificate verification by default; local dev override via `SMTP_ALLOW_SELF_SIGNED_DEV`. |
| Email/SMS variable injection via `{{var}}` templates | LOW | Resolved in Phase 138: `TemplateVariableCatalog` blocks unknown placeholders on save. |
## Architecture Concerns
| Concern | Impact | Notes |
|---------|--------|-------|
| No repository interfaces | LOW | Cannot mock repositories cleanly in tests without `bypass-finals` workaround |
| String-typed event/action names | LOW | `event_type = 'order.status_changed'` — typos not caught at compile time |
| No validation layer | LOW | Validation scattered across controllers and repositories |
| No HTTP caching headers | LOW | Responses don't set ETag, Cache-Control; acceptable for low-concurrency use |
| No query caching | LOW | Every request re-queries; no Redis/Memcached layer |
## Duplication Areas (Phase 68 scope)
- `SslCertificateResolver` — pattern duplicated across multiple API client files
- `ToggleableRepositoryTrait` — mixes query building + toggling
- `RedirectPathResolver` — similar redirect logic in 4+ controllers
- `ReceiptService` vs accounting logic — overlapping responsibilities
## Legacy / Deprecated Patterns
| Pattern | Location | Status |
|---------|----------|--------|
| `fsockopen('ssl://')` / weak SMTP TLS | `EmailMailboxController::testConnection()` | Resolved in Phase 138; strict stream context + STARTTLS, local dev override only. |
| `require` / direct alert includes in targeted views | `resources/views/...` targeted by Phase 139-02 | Resolved through `$component()` helper. Remaining `php:S4833` issues are core framework file loading, not alert components. |
| Raw `$_SESSION` access | Auth/Flash/Csrf/OAuth before Phase 138 | Resolved in Phase 138; raw access is isolated in `App\Core\Support\Session`. |
## Performance Risks
1. **Correlated subquery for return-risk** (per-row COUNT) — slow at >50k orders without `INDEX-106-01`
2. **N+1 potential in order details**`findDetails()` queries orders + addresses + items separately (not verified as actual problem)
3. **Cron queue growth** — no exponential backoff if queue grows; may pile up on slow syncs

View File

@@ -1,183 +1,37 @@
# Conventions & Patterns # Conventions
## Naming Last refresh: 2026-05-18.
| Element | Convention | Example | ## PHP
|---------|-----------|---------|
| Classes | PascalCase | `OrdersController`, `AllegroApiClient` |
| Methods / variables | camelCase | `findDetails()`, `$statusCode` |
| Constants | UPPER_SNAKE_CASE | `SESSION_KEY`, `OPTION_KEYS` |
| DB columns | snake_case | `source_order_id`, `payment_status` |
| PHP files | Match class name | `OrdersController.php` |
| View files | kebab-case | `table-list.php`, `order-status-panel.php` |
| SCSS partials | `_kebab-case.scss` | `_automation.scss` |
| No abbreviations | Full names | `$translatedText` not `$t` (except loop indices) |
## Code Constraints (CLAUDE.md) - App code lives under `src/` with PSR-4 namespace `App\\`; tests use `Tests\\` from `tests/`.
- Use `declare(strict_types=1);` and `final class` for PHP classes, matching files such as `src/Core/View/Template.php`.
- Naming: classes PascalCase, methods/variables camelCase, constants UPPER_SNAKE_CASE, DB columns snake_case, view filenames kebab-case.
- Keep controllers thin: parse `Request`, validate, call repository/service, prepare view data, return `Response`.
- Keep data access in repositories/services and use prepared statements for dynamic values.
- Do not wire `DB_HOST_REMOTE` into runtime; runtime database config uses `DB_HOST`.
- Max **~50 lines** per method; longer → split ## Views and UI
- Max **3 nesting levels** (if/foreach); deeper → extract to method
- Single Responsibility: one class = one job
- All classes are `final` (no accidental inheritance)
- `declare(strict_types=1)` in every file
- Comments only for **WHY**, never for WHAT
## Database Pattern - Views are native PHP under `resources/views/`.
- Escape output in views with `$e()`.
- Use `$t()` for translations where applicable.
- Reuse PHP components through `$component()` from `src/Core/View/Template.php`; shared components live in `resources/views/components/`.
- Put styles in SCSS under `resources/scss/` or module sources under `resources/modules/`; compiled assets belong in `public/assets/`.
- Use `window.OrderProAlerts` from `resources/modules/jquery-alerts` / `public/assets/js/modules/jquery-alerts.js` for alerts and confirmations.
- Treat inline styles/scripts in older views such as `resources/views/orders/show.php`, `resources/views/shipments/prepare.php`, and `resources/views/receipts/print.php` as legacy, not a pattern for new work.
**PDO prepared statements only — no ORM, no string concatenation.** ## Configuration and Security
```php - Environment parsing is in `src/Core/Support/Env.php`.
// Correct - CSRF handling is in `src/Core/Security/Csrf.php`; forms should include valid CSRF tokens.
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id'); - Session behavior is handled through `src/Core/Support/Session.php` and auth middleware.
$stmt->bindValue(':id', $id, PDO::PARAM_INT); - SMTP TLS verification is the default; `SMTP_ALLOW_SELF_SIGNED_DEV=true` is dev-only.
$stmt->execute();
// Never ## Reusable Patterns
$pdo->query("SELECT * FROM orders WHERE id = $id"); // forbidden
```
- `ATTR_EMULATE_PREPARES = false` (real server-side preparation) - Alert/confirmation module source: `resources/modules/jquery-alerts/jquery-alerts.js`.
- `ATTR_ERRMODE = ERRMODE_EXCEPTION` - Shared delete confirm wrapper: `public/assets/js/modules/confirm-delete.js`.
- Parameter type hints: `PDO::PARAM_INT` for integers - Reusable table/list component: `resources/views/components/table-list.php`.
- Shared order status panel: `resources/views/components/order-status-panel.php`.
## Security Patterns - Template helpers are injected by `src/Core/View/Template.php`.
### CSRF
```php
// Generate (in controller)
'csrfToken' => Csrf::token() // stores in $_SESSION['_csrf_token']
// In view
<input type="hidden" name="_token" value="<?= $e($csrfToken) ?>">
// Validate (in controller)
if (!Csrf::validate((string) $request->input('_token', ''))) { ... }
```
Field name is always `_token`. Uses `hash_equals()` for timing-safe comparison.
### XSS Escaping
All user-controlled output escaped with `$e()` helper (available in all views):
```php
$e = fn(mixed $v): string => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
// Usage
<?= $e($order['customer_name']) ?>
<?= $e($t('orders.status.label')) ?>
```
**Never** output raw variables without `$e()`.
### Session
Configured with: `cookie_httponly=true`, `cookie_secure=true`, `cookie_samesite=Lax`, `use_strict_mode=true`.
Access via `Session::get()` / `Session::set()` helpers — not raw `$_SESSION` in business logic.
## Controller Pattern
```php
final class OrdersController {
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly OrdersRepository $orders,
// ...
) {}
public function index(Request $request): Response {
// 1. Parse & validate input
$filters = ['search' => trim((string) $request->input('search', ''))];
// 2. Call repository
$result = $this->orders->paginate($filters);
// 3. Prepare view data
$rows = array_map(fn($row) => $this->toTableRow($row), $result['items']);
// 4. Render
return Response::html(
$this->template->render('orders/index', ['rows' => $rows], 'layouts/app')
);
}
}
```
## View Pattern
Views use two magic helpers injected by `Template::renderFile()`:
- `$e($value)` — HTML-escape
- `$t($key, $replace)` — translate
Layout composition:
```php
$this->template->render('orders/index', $data, 'layouts/app')
// renders views/orders/index.php, wraps in views/layouts/app.php via $content
```
## UI Rules
### Alerts & Confirmations
- **Always** use `window.OrderProAlerts.confirm({message, onConfirm})` from `jquery-alerts.js`
- **Never** use native `alert()` or `confirm()`
### CSS / SCSS
- All styles go in `resources/scss/` — never inline `<style>` or `style=""` attributes in PHP templates
- CSS custom properties for dynamic colors: `style="--status-color: <?= $e($color) ?>"` → used via `var(--status-color)` in SCSS
- Build: `npm run build:assets`
- UI must be **compact** — maximize info density, minimize whitespace
### Reusable Components
- Extract repeated UI blocks to `resources/views/components/`
- Current components: `table-list.php`, `order-status-panel.php`
- Changes to a component must be verified in **all** places it is used
## Flash Messages
```php
// Set (in controller)
Flash::set('error', $this->translator->get('auth.errors.csrf_expired'));
return Response::redirect('/login');
// Read (in view)
<?php if (!empty($errorMessage)): ?>
<div class="alert alert--danger"><?= $e($errorMessage) ?></div>
<?php endif; ?>
```
## Exception Hierarchy
```
OrderProException (base)
├── AllegroApiException
├── AllegroOAuthException
├── ApaczkaApiException
├── IntegrationConfigException
└── ShipmentException
```
Throw specific domain exceptions, not generic `\Exception`.
## Error Handling
Global exception handler in `Application::registerErrorHandlers()`:
- Always logs to `storage/logs/app.log` with JSON context
- Shows `message` in debug mode, `"Internal server error"` in production
Log format: `[2026-04-26 14:30:00] ERROR message {"context":"value"}`
## Routing Convention
```php
// Public
$router->get('/login', [AuthController::class, 'showLogin']);
$router->post('/login', [AuthController::class, 'login']);
// Authenticated
$router->get('/orders', [OrdersController::class, 'index'], [$authMiddleware]);
// JSON API with API key
$router->post('/api/print-jobs', [PrintApiController::class, 'store'], [$apiKeyMiddleware]);
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
# Impact Map
Last refresh: 2026-05-18.
## Orders
- Controllers/repositories: `src/Modules/Orders/OrdersController.php`, `src/Modules/Orders/OrdersRepository.php`, `src/Modules/Orders/OrderImportRepository.php`, `src/Modules/Orders/OrderNotesService.php`.
- Views: `resources/views/orders/show.php`, `resources/views/orders/list.php`, `resources/views/orders/partials/`.
- Frontend modules: `public/assets/js/modules/inline-status-change.js`, `public/assets/js/modules/order-notes.js`.
- Data: `orders`, `order_items`, `order_addresses`, `order_payments`, `order_notes`, `order_activity_log`.
- Verification: affected PHPUnit service/repository tests plus manual list/detail UI smoke.
## Shipments
- Controllers/services: `src/Modules/Shipments/ShipmentController.php`, `src/Modules/Shipments/*ShipmentService.php`, `src/Modules/Shipments/*TrackingService.php`.
- Registries/interfaces: `src/Modules/Shipments/ShipmentProviderRegistry.php`, `src/Modules/Shipments/ShipmentProviderInterface.php`, `src/Modules/Shipments/ShipmentTrackingRegistry.php`.
- Views: `resources/views/shipments/prepare.php`, carrier settings views under `resources/views/settings/`.
- Data: `shipment_packages`, `shipment_presets`, `delivery_statuses`, `delivery_status_mappings`, carrier mapping tables.
- Verification: provider-specific tests and manual create/delete/tracking UI smoke.
## Integrations and Settings
- Surface: `src/Modules/Settings/*Integration*`, `src/Modules/Settings/*ApiClient.php`, `src/Modules/Settings/*SyncService.php`, `src/Modules/Settings/*MappingRepository.php`.
- Routes/wiring: `routes/web.php`.
- Views: `resources/views/settings/*.php`.
- Data: `integrations`, provider settings tables, mapping tables, sync-state tables.
- Verification: target service tests, settings form CSRF/session smoke, external API dry-run where available.
## Accounting
- Controllers/services/repositories: `src/Modules/Accounting/*`, `src/Modules/Settings/Fakturownia*`, `src/Modules/Settings/InvoiceConfig*`, `src/Modules/Settings/ReceiptConfig*`.
- Views: `resources/views/accounting/*.php`, `resources/views/receipts/*.php`, accounting settings views.
- Data: `receipt_configs`, `receipts`, `receipt_number_counters`, `invoice_configs`, `invoices`, `invoice_number_counters`.
- Verification: receipt/invoice PHPUnit tests, PDF/export smoke, invoice idempotency checks.
## Cron and Automation
- Cron: `bin/cron.php`, `src/Modules/Cron/*`, `routes/web.php`.
- Automation: `src/Modules/Automation/*`, `resources/views/automation/*.php`.
- Data: `cron_jobs`, `cron_schedules`, `automation_rules`, `automation_conditions`, `automation_actions`, `automation_execution_logs`, `automation_email_once_deliveries`.
- Verification: target handler/service test plus manual cron dry-run in local/dev.
## Frontend Assets
- Sources: `resources/scss/`, `resources/modules/jquery-alerts/`.
- Compiled CSS/JS: `public/assets/css/`, `public/assets/js/modules/`.
- Verification: `npm run build:assets` and manual affected-view check.
## Polish UI Copy / Translations
- Translation source: `resources/lang/pl.php`, loaded through `src/Core/I18n/Translator.php` and exposed to views as `$t()` by `src/Core/View/Template.php`.
- View surfaces: `resources/views/orders/`, `resources/views/settings/`, `resources/views/accounting/`, `resources/views/automation/`, `resources/views/layouts/`, and shared components.
- Frontend copy: `public/assets/js/modules/*.js`, especially alert/confirm/status messages.
- Backend operator messages: controllers/services under `src/Modules/*` that write flash messages or JSON errors shown in UI.
- Verification: PHP lint for touched files, residual search for mojibake/common ASCII Polish forms, `git diff --check`, and manual smoke of main UI pages.
- Apply result 2026-05-18: normalized Polish copy across the translation file, legacy views, public JS modules and selected module messages; no DB schema, route, API payload or template-placeholder contract changes.

View File

@@ -1,42 +0,0 @@
# orderPRO — Codebase Map
**Generated:** 2026-04-28 | **Version:** 3.2.0 | **Milestone:** v3.2 zamknięty (Phase 108 COMPLETE)
## What Is This Project
orderPRO is a **multi-channel order management system** for Polish e-commerce. It aggregates orders from Allegro (OAuth2) and shopPRO platforms, manages shipments via Apaczka and InPost APIs, generates PDF receipts/invoices, sends automated emails, and exposes a REST API for a Windows print client.
## Quick Navigation
| Document | Contents |
|----------|----------|
| [stack.md](stack.md) | PHP 8.4, custom framework, PDO, SCSS, PHPUnit, Composer deps |
| [architecture.md](architecture.md) | MVC + Repository + Service layers, modules, routing, data flows |
| [conventions.md](conventions.md) | Naming, code patterns, security, UI rules (from CLAUDE.md) |
| [testing.md](testing.md) | PHPUnit 11.5, test patterns, coverage areas |
| [integrations.md](integrations.md) | Allegro, shopPRO, Apaczka, InPost, Email, Print queue |
| [concerns.md](concerns.md) | Tech debt, SonarQube issues, known bugs, performance |
| [db_schema.md](db_schema.md) | Pełny schemat bazy danych — 55 tabel, typy kolumn, klucze, indeksy |
| [tech_changelog.md](tech_changelog.md) | Chronologiczny log zmian technicznych — co i dlaczego, per-phase |
## Key Directories
```
src/Core/ Custom framework (router, PDO, session, logger, CSRF)
src/Modules/ 13 feature modules (Orders, Shipments, Accounting, Email, …)
routes/web.php All ~80 routes in one file
resources/views/ PHP templates organized by module
resources/scss/ SCSS sources → public/assets/css/
database/migrations/ 84 SQL migration files (timestamped)
tests/Unit/ PHPUnit tests for services
.paul/codebase/ Mapa kodu (architecture, db_schema, tech_changelog, ...)
.paul/changelog/ Dziennik zmian per dzień (klient-friendly)
```
## Current State
- **Active phase:** — (Phase 108 COMPLETE, v3.2 zamknięty)
- **Last completed:** Phase 108 — Delivery Status Management (2026-04-27)
- **Total migrations:** 84+ (latest: delivery_status tables from Phase 108)
- **God classes to watch:** `OrdersRepository` (1221 LOC), `OrdersController` (1187 LOC), `AutomationService` (834 LOC)
- **BREAKING CHANGE (Phase 108):** Automation rules z kluczami group delivery status (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up`) wymagają ręcznego odtworzenia po migracji do DB

View File

@@ -1,116 +1,41 @@
# External Integrations # Integrations
## Allegro (Polish e-commerce — OAuth2) Last refresh: 2026-05-18.
**Auth:** OAuth2 Authorization Code Grant ## Marketplace and Shop APIs
**Scopes:** `orders:read/write`, `sale:offers:read`, `shipments:read/write`
**Token storage:** Encrypted in `allegro_integration_settings`
**Token refresh:** `AllegroTokenManager` — auto-refreshes before expiry
**User-Agent:** Required from 01.07.2026 (env: `ALLEGRO_USER_AGENT_URL`)
| File | Purpose | - Allegro OAuth/API: `src/Modules/Settings/AllegroOAuthClient.php`, `src/Modules/Settings/AllegroApiClient.php`, `src/Modules/Settings/AllegroTokenManager.php`.
|------|---------| - Allegro orders/status: `src/Modules/Settings/AllegroOrderImportService.php`, `src/Modules/Settings/AllegroStatusSyncService.php`, `src/Modules/Settings/AllegroOrdersSyncService.php`.
| `src/Modules/Settings/AllegroApiClient.php` | REST calls: `getCheckoutForm()`, `listCheckoutForms()`, `getCheckoutFormShipments()` | - Allegro shipments/tracking: `src/Modules/Shipments/AllegroShipmentService.php`, `src/Modules/Shipments/AllegroTrackingService.php`.
| `src/Modules/Settings/AllegroOAuthClient.php` | OAuth2 flow, token exchange | - shopPRO API: `src/Modules/Settings/ShopproApiClient.php`, `src/Modules/Settings/ShopproOrdersSyncService.php`, `src/Modules/Settings/ShopproStatusSyncService.php`, `src/Modules/Settings/ShopproPaymentStatusSyncService.php`.
| `src/Modules/Settings/AllegroTokenManager.php` | Token refresh & storage | - Erli API: `src/Modules/Settings/ErliApiClient.php`, `src/Modules/Settings/ErliOrdersSyncService.php`, `src/Modules/Settings/ErliStatusSyncService.php`, `src/Modules/Settings/ErliExternalShipmentService.php`.
| `src/Modules/Settings/AllegroOrderImportService.php` | Transform & insert Allegro orders |
| `src/Modules/Settings/AllegroOrdersSyncService.php` | Continuous order sync |
| `src/Modules/Settings/AllegroStatusSyncService.php` | Push status changes to Allegro |
| `src/Modules/Settings/AllegroStatusDiscoveryService.php` | Fetch available Allegro statuses |
| `src/Modules/Shipments/AllegroShipmentService.php` | Create shipments via Allegro |
| `src/Modules/Shipments/AllegroTrackingService.php` | Track delivery status |
## shopPRO (Polish e-commerce — API Key) ## Carriers
**Auth:** API Key + Base URL in integration config - Apaczka config/API: `src/Modules/Settings/ApaczkaIntegrationRepository.php`, `src/Modules/Settings/ApaczkaApiClient.php`.
**Pagination:** page/per_page, max 100 items - Apaczka shipment/tracking: `src/Modules/Shipments/ApaczkaShipmentService.php`, `src/Modules/Shipments/ApaczkaTrackingService.php`.
**Date filter:** `updated_from` parameter - InPost config/shipment/tracking: `src/Modules/Settings/InpostIntegrationRepository.php`, `src/Modules/Shipments/InpostShipmentService.php`, `src/Modules/Shipments/InpostTrackingService.php`.
- Polkurier config/API: `src/Modules/Settings/PolkurierIntegrationRepository.php`, `src/Modules/Settings/PolkurierApiClient.php`.
- Polkurier shipment/tracking: `src/Modules/Shipments/PolkurierShipmentService.php`, `src/Modules/Shipments/PolkurierTrackingService.php`.
- Shared shipment surface: `src/Modules/Shipments/ShipmentProviderInterface.php`, `src/Modules/Shipments/ShipmentProviderRegistry.php`, `src/Modules/Shipments/ShipmentTrackingRegistry.php`.
| File | Purpose | ## Accounting, Email, SMS, Print
|------|---------|
| `src/Modules/Settings/ShopproApiClient.php` | `fetchOrders()`, status/payment sync |
| `src/Modules/Settings/ShopproOrdersSyncService.php` | Order import/sync |
| `src/Modules/Settings/ShopproPaymentStatusSyncService.php` | Payment sync |
| `src/Modules/Settings/ShopproStatusSyncService.php` | Status mapping |
| `src/Modules/Settings/ShopproOrderMapper.php` | Order transformation (867 LOC) |
## Apaczka (Polish parcel aggregator) - Fakturownia: `src/Modules/Settings/FakturowniaApiClient.php`, `src/Modules/Settings/FakturowniaIntegrationRepository.php`.
- HostedSMS: `src/Modules/Settings/HostedSmsApiClient.php`, `src/Modules/Settings/HostedSmsIntegrationRepository.php`.
- SMSPLANET: `src/Modules/Settings/SmsplanetApiClient.php`, `src/Modules/Settings/SmsplanetIntegrationRepository.php`, `src/Modules/Sms/SmsplanetWebhookController.php`.
- SMTP: `src/Modules/Email/EmailSendingService.php`, `src/Modules/Settings/SmtpSecurityContextFactory.php`.
- MF whitelist lookup: `src/Core/Http/MfWhitelistApiClient.php`.
- Print API: `src/Modules/Printing/PrintApiController.php`, `src/Modules/Printing/ApiKeyMiddleware.php`, `clients/windows/OrderPROPrint/`.
**Auth:** App ID + App Secret ## Cron and External Scheduling
**Base URL:** `https://www.apaczka.pl/api/v2`
**Custom exception:** `src/Core/Exceptions/ApaczkaApiException.php`
| File | Purpose | - CLI cron entry: `bin/cron.php`.
|------|---------| - Web cron setting surface: `config/app.php`, `routes/web.php`.
| `src/Modules/Settings/ApaczkaApiClient.php` | `getServiceStructure()`, `sendOrder()`, `getOrderDetails()`, `getWaybill()` | - Handler wiring: `src/Modules/Cron/CronHandlerFactory.php`, `src/Modules/Cron/CronRunner.php`.
| `src/Modules/Shipments/ApaczkaShipmentService.php` | Implements `ShipmentProviderInterface` | - Scheduled integration handlers include `src/Modules/Cron/ShopproOrdersImportHandler.php`, `src/Modules/Cron/AllegroOrdersImportHandler.php`, `src/Modules/Cron/ErliOrdersImportHandler.php`, `src/Modules/Cron/ShipmentTrackingHandler.php`.
| `src/Modules/Shipments/ApaczkaTrackingService.php` | Implements `ShipmentTrackingInterface` |
## InPost (Parcel lockers + courier) ## CDN and Browser Integrations
**Auth:** Organization token - Google Fonts and Chart.js are loaded in `resources/views/layouts/app.php`.
**Production:** `https://api-shipx-pl.easypack24.net/v1` - Quill is loaded from CDN in `resources/views/settings/email-mailboxes.php` and `resources/views/settings/email-templates-form.php`.
**Sandbox:** `https://sandbox-api-shipx-pl.easypack24.net/v1`
**Services:** Paczkomat Standard, Kurier Standard, Kurier Express
| File | Purpose |
|------|---------|
| `src/Modules/Shipments/InpostShipmentService.php` | Create shipments |
| `src/Modules/Shipments/InpostTrackingService.php` | Track delivery status |
## Provider Abstraction
Shipment providers implement a common interface:
- `ShipmentProviderInterface``createShipment()`, `downloadLabel()`
- `ShipmentTrackingInterface``getDeliveryStatus()`
- `ShipmentProviderRegistry` — selects correct provider by type
- `ShipmentTrackingRegistry` — selects correct tracker
## Email (SMTP via PHPMailer)
**Library:** PHPMailer 7.0
**Config:** Multiple mailboxes from DB (`EmailMailboxRepository`)
**Features:** HTML + attachments, template variable resolution, logging
| File | Purpose |
|------|---------|
| `src/Modules/Email/EmailSendingService.php` | Compose & send via configured mailbox |
| `src/Modules/Email/VariableResolver.php` | Replace `{{var}}` in templates with order data |
| `src/Modules/Email/AttachmentGenerator.php` | Generate PDF attachments via Dompdf |
| `src/Modules/Email/EmailMailboxRepository.php` | SMTP credentials & config |
| `src/Modules/Email/EmailTemplateRepository.php` | Email template storage |
## Print Queue API (Windows client)
**Auth:** Bearer API key (header `Authorization: Bearer {key}`)
**Purpose:** Windows desktop client retrieves print jobs (shipment labels)
| File | Purpose |
|------|---------|
| `src/Modules/Printing/PrintApiController.php` | `POST /api/print/jobs`, status endpoints |
| `src/Modules/Printing/ApiKeyMiddleware.php` | Validates API key against DB |
| `src/Modules/Printing/PrintApiKeyRepository.php` | API key management |
| `src/Modules/Printing/PrintJobRepository.php` | Job queue tracking |
## PDF & Excel (Libraries)
| Library | Version | Used For |
|---------|---------|---------|
| `dompdf/dompdf` | ^3.1 | Receipts, invoices, email attachments |
| `phpoffice/phpspreadsheet` | ^5.5 | Accounting export to XLSX |
## SSL / HTTP
All external API calls use cURL with certificate validation.
Resolver: `src/Core/Http/SslCertificateResolver.php`
Config: `CURL_CA_BUNDLE_PATH` in `.env`
## Integration Config Storage
| Table | Contents |
|-------|---------|
| `integrations` | Base record (source type, enabled, API key encrypted) |
| `allegro_integration_settings` | OAuth tokens (encrypted), Allegro-specific config |
| `*_status_mappings` | Bidirectional status code translations |
| `email_mailboxes` | SMTP connection settings |
| `print_api_keys` | Print client API keys |

View File

@@ -0,0 +1,57 @@
# Quality Risks
Last refresh: 2026-05-18.
## Radar Status
- `jscpd` ran through `npx` and produced `.paul/codebase/radar/jscpd/jscpd-report.json`.
- `codebase-memory-mcp` is installed globally at version 0.6.1; a fresh Codex MCP process indexed the repo in `fast` mode with 8165 nodes and 13610 edges.
- `ast-grep` is degraded: command unavailable and `npx --package @ast-grep/cli ast-grep` failed under the current Node/npm environment.
- Structural fallback used `rg` and code/document review.
## Primary Risks
- God-class risk: large controllers/repositories mix request validation, orchestration, rendering state, SQL, and integration details, especially `src/Modules/Orders/OrdersController.php`, `src/Modules/Orders/OrdersRepository.php`, `src/Modules/Settings/ShopproIntegrationsController.php`.
- Native prompt risk: remaining `window.confirm`, `confirm`, and `alert` fallbacks bypass `resources/modules/jquery-alerts`.
- Inline frontend risk: many legacy `<script>`, inline handlers, and style fragments remain in views such as `resources/views/orders/show.php`, `resources/views/shipments/prepare.php`, and `resources/views/settings/email-mailboxes.php`.
- Test gap risk: high-change controllers, routes, JS modules, cron wiring, and Windows print client lack direct automated tests.
- Data/query risk: repositories use PDO directly; static SQL is normal, but any dynamic table/query construction should be reviewed before extension.
## Native Prompt Findings
- `public/assets/js/modules/confirm-delete.js` has `window.confirm` fallback.
- `public/assets/js/modules/sms-template-picker.js` has `window.confirm` fallback.
- `resources/views/shipments/prepare.php` has `confirm` fallback.
- `resources/views/orders/show.php` has `confirm` fallback.
- `resources/views/components/table-list.php` has `window.confirm` fallback.
- `resources/views/accounting/invoice_form.php` has native `alert`.
- `resources/views/settings/email-mailboxes.php` has `confirm` fallback.
## Hardcoded or External Surface
- CDN assets live in `resources/views/layouts/app.php`, `resources/views/settings/email-mailboxes.php`, and `resources/views/settings/email-templates-form.php`.
- API endpoints/config handling is spread across `src/Modules/Settings/*ApiClient.php` and provider config repositories.
- Runtime DB host must remain `DB_HOST`; do not use `DB_HOST_REMOTE` in application config.
## Recommended Follow-Up
- Plan a focused prompt-cleanup phase for the native `alert()` / `confirm()` leftovers.
- Exclude `.playwright-mcp/`, `.scannerwork/`, `.vscode/ftp-kr.diff.*`, generated assets, and cache folders from future jscpd scans.
- Use MCP tools through a fresh Codex session/process for structured codebase-memory graph output; the already-running session may not hot-reload new MCP servers.
- Add direct tests around high-risk services before refactoring large controllers.
## Targeted Risk: Polish UI Copy Apply
- Date: 2026-05-18.
- `resources/lang/pl.php`, hardcoded view copy, JS alerts and selected backend operator messages were normalized to proper Polish diacritics.
- Preserved technical contracts: routes, form names, CSS classes, JS selectors, status/provider codes, API payload keys and template placeholders such as `{{zamowienie.numer}}` and `{{przesylka.numer}}`.
- Residual search shows remaining ASCII Polish-like strings are intentional technical identifiers/provider URLs or template keys.
- Verification gap: `sonar-scanner` is unavailable in PATH, so Sonar was not run for this APPLY.
## Targeted Risk: Polish UI Copy Plan
- Date: 2026-05-18.
- `resources/lang/pl.php` contains many ASCII-transliterated user-facing values (`Zamowienia`, `Przesylki`, `Platnosci`, `Zrodlo`, etc.) and should be corrected as the primary source of truth.
- Hardcoded visible copy also exists in views, JS modules and selected controllers/services. Correcting it is safe only when the string is clearly user-facing.
- Main risk is over-translation: routes, array keys, status codes, provider codes, payload fields, CSS classes and JS selectors must remain unchanged.
- Encoding must remain valid UTF-8; residual scans should check for mojibake markers such as `Ă`, `Ĺ`, `—`, and `â†`.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# codebase-memory-mcp full scan
Timestamp: 2026-05-18T22:55:00+02:00
Status: installed, MCP-tested, and indexed through a fresh Codex process.
Version: codebase-memory-mcp 0.6.1
Binary: C:\Users\jacek\AppData\Roaming\npm\codebase-memory-mcp.ps1
Codex MCP command: C:/Users/jacek/AppData/Roaming/npm/node_modules/codebase-memory-mcp/bin/codebase-memory-mcp.exe
Commands:
- codebase-memory-mcp --version
- codebase-memory-mcp --help
- codebase-memory-mcp --% cli index_repository {"repo_path":"C:\visual studio code\projekty\orderPRO"}
- codebase-memory-mcp --% cli list_projects {}
- codebase-memory-mcp --% cli index_status {"repo_path":"C:\visual studio code\projekty\orderPRO"}
- codebase-memory-mcp --% cli get_architecture {"repo_path":"C:\visual studio code\projekty\orderPRO"}
- codex exec used MCP tools: list_projects, index_repository, list_projects, get_architecture
Result:
- CLI is now available in PATH and reports version 0.6.1.
- Codex config lists `codebase-memory-mcp` as enabled stdio MCP server.
- Current already-running Codex session does not hot-reload the new MCP server, but a fresh `codex exec` process loaded it correctly.
- Initial MCP index completed in `fast` mode.
- Project: `C-visual studio code-projekty-orderPRO`.
- Path: `C:/visual studio code/projekty/orderPRO`.
- Index: 8165 nodes, 13610 edges.
- Architecture summary from MCP: 1775 `Method`, 225 `Class`, 160 `Route`, 698 `File`, 698 `Module`; main edge types include `DEFINES`, `CALLS`, `DEFINES_METHOD`, `TESTS`, `THROWS`, `USAGE`.
- Direct PowerShell CLI JSON invocation still does not pass `repo_path` reliably; use MCP tools through a fresh Codex session/process for graph operations.

View File

@@ -0,0 +1,15 @@
Mode: post-apply
Timestamp: 2026-05-18 23:40 Europe/Warsaw
Scope: .paul/plans/20260518-2305-polskie-tlumaczenia/PLAN.md resources/lang/pl.php resources/views public/assets/js/modules src/Modules DOCS .paul/codebase
Tool:
- codebase-memory-mcp --version: codebase-memory-mcp 0.6.1
- MCP detect_changes project: C-visual studio code-projekty-orderPRO
Result:
- changed_count: 110
- impacted_symbols: []
Interpretation:
- The graph did not report impacted callable symbols for this copy-only change.
- Main impact remains UI/documentation surface: translations, legacy views, public JS modules and selected operator-facing module messages.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,71 +1,56 @@
# Technology Stack # Stack
Last refresh: 2026-05-18.
## Runtime ## Runtime
| Layer | Technology | Version | Notes | - Backend: PHP `^8.4`, custom lightweight MVC-style application.
|-------|-----------|---------|-------| - Database: MySQL/InnoDB through PDO; no ORM.
| PHP | PHP | ^8.4 | `declare(strict_types=1)` in all files | - Frontend: native PHP views plus plain JS modules; no JS bundler.
| Web server | Apache | XAMPP (local) | `public/.htaccess` handles routing | - Assets: Sass from `resources/scss/` and `resources/modules/jquery-alerts/`, compiled into `public/assets/`.
| Database | MySQL | InnoDB | utf8mb4_unicode_ci | - Windows helper client: C# WinForms tray app in `clients/windows/OrderPROPrint/`.
| Node.js | npm | dev only | Sass build tool only, no runtime JS bundler |
## PHP Dependencies (`composer.json`) ## Manifests
| Package | Version | Purpose | - PHP manifest: `composer.json`.
|---------|---------|---------| - PHP lockfile: no `composer.lock` found, so dependency versions are not pinned in-repo.
| `dompdf/dompdf` | ^3.1 | PDF generation (receipts, labels) | - Node manifests: `package.json`, `package-lock.json`.
| `phpoffice/phpspreadsheet` | ^5.5 | Excel/XLSX export (accounting) | - PHPUnit config: `phpunit.xml`.
| `phpmailer/phpmailer` | ^7.0 | SMTP email sending | - Sonar config: `sonar-project.properties`.
| `phpunit/phpunit` | ^11.5 (dev) | Unit testing |
| `dg/bypass-finals` | ^1.9 (dev) | Mock final classes in tests |
## Framework ## Core Framework Paths
**Custom lightweight framework** — no Laravel/Symfony. - Application bootstrap: `public/index.php`, `index.php`, `bootstrap/app.php`.
- Application shell: `src/Core/Application.php`.
- Routing: `src/Core/Routing/Router.php`, `routes/web.php`.
- HTTP primitives: `src/Core/Http/Request.php`, `src/Core/Http/Response.php`.
- Views: `src/Core/View/Template.php`, `resources/views/`.
- Database: `src/Core/Database/ConnectionFactory.php`, `src/Core/Database/Migrator.php`.
- Security/session: `src/Core/Security/Csrf.php`, `src/Core/Support/Session.php`, `src/Modules/Auth/`.
- Environment/config: `.env.example`, `src/Core/Support/Env.php`, `config/app.php`, `config/database.php`.
| Component | File | ## Commands
|-----------|------|
| Application bootstrap | `src/Core/Application.php` |
| Router | `src/Core/Routing/Router.php` |
| Request / Response | `src/Core/Http/Request.php`, `Response.php` |
| Template engine | `src/Core/View/Template.php` (PHP-native, `$e()` + `$t()` helpers) |
| Session | `src/Core/Support/Session.php` |
| Logger | `src/Core/Support/Logger.php``storage/logs/app.log` |
| CSRF | `src/Core/Security/Csrf.php` |
| i18n | `src/Core/I18n/Translator.php` (Polish primary: `resources/lang/pl/`) |
| DB connection | `src/Core/Database/ConnectionFactory.php` (PDO, no ORM, no medoo) |
| Migrator | `src/Core/Database/Migrator.php` (custom SQL runner, `migrations` table) |
| SSL resolver | `src/Core/Http/SslCertificateResolver.php` (env: `CURL_CA_BUNDLE_PATH`) |
## Frontend - Dev server: `composer serve`.
- Migrations: `composer migrate` or `php bin/migrate.php`.
- Cron: `composer cron` or `php bin/cron.php`.
- PHP tests: `composer test` or `vendor/bin/phpunit -c phpunit.xml --testdox`.
- Asset build: `npm run build:assets`.
- CSS build only: `npm run build:css`.
- Alert module build: `npm run build:modules`.
- Sass watch: `npm run watch:css`.
- **No CSS framework** — custom SCSS design tokens ## Notable Dependencies
- **jQuery** — used only for `jquery-alerts` module (`resources/modules/jquery-alerts/`)
- **No JS bundler** — files served directly from `public/assets/js/`
- **Build**: `npm run build:assets` (Sass → compressed CSS + JS copy)
## Build Scripts - PDF/rendering: `dompdf/dompdf`.
- Spreadsheet export: `phpoffice/phpspreadsheet`.
- SMTP: `phpmailer/phpmailer`.
- Tests: `phpunit/phpunit`, `dg/bypass-finals`.
- Frontend build: `sass`.
```json ## Configuration Notes
"build:css" sass --style=compressed resources/scss/app.scss public/assets/css/app.css
"build:modules" sass jquery-alerts.scss + copy jquery-alerts.js
"build:assets" build:css && build:modules
"watch:css" sass --watch (development)
```
```json - Runtime database host is `DB_HOST` through `config/database.php`.
"serve" php -S localhost:8000 -t public public/index.php - `DB_HOST_REMOTE` is agent-only/manual migration infrastructure and must not be wired into runtime.
"migrate" php bin/migrate.php - TLS/CA behavior is centralized around `CURL_CA_BUNDLE_PATH` and `src/Core/Http/SslCertificateResolver.php`.
"cron" php bin/cron.php - SMTP self-signed override is dev-only via `SMTP_ALLOW_SELF_SIGNED_DEV`.
"test" vendor/bin/phpunit -c phpunit.xml --testdox
```
## Environment Variables (`.env.example`)
| Variable | Purpose |
|----------|---------|
| `DB_HOST`, `DB_PORT`, `DB_DATABASE` | MySQL connection |
| `DB_HOST_REMOTE` | Agent-only manual DB ops (NOT used by app runtime) |
| `CURL_CA_BUNDLE_PATH` | SSL cert path (XAMPP: `C:/xampp/php/extras/ssl/cacert.pem`) |
| `ALLEGRO_USER_AGENT_URL` | Required by Allegro REST API from 01.07.2026 |
| `CRON_RUN_ON_WEB`, `CRON_WEB_LIMIT`, `CRON_PUBLIC_TOKEN` | Cron configuration |

View File

@@ -1,626 +0,0 @@
# Technical Changelog
## 2026-05-17 - Phase 139 Plan 02: Sonar Critical/Major Cleanup
**Co zrobiono:**
- Finalny Sonar: 495 OPEN BLOCKER/CRITICAL/MAJOR po 139-02 (spadek z 605 po 139-01).
- Dodano typowane wyjatki w selected Settings/Automation clusters: Fakturownia, Polkurier, Erli mapping, automation duplicate i email template duplicate.
- Targetowane widoki/layouty renderuja alert component przez `$component()`; `messageHtml` pozostaje tylko dla zaufanego gotowego HTML.
- `SmsTemplateController` i `UsersController` maja stale tras/flashy oraz male helpery walidacyjne, bez zmiany routingu i UX.
**Dlaczego:**
- Najwiekszy bezpieczny zysk po 139-01 byl w `php:S4833` i `php:S112`; plan nie mial ruszac schematu DB ani god-class splitow.
**BREAKING / migracja:**
- Brak migracji DB i brak breaking changes.
## 2026-05-17 - Phase 139 Plan 01: Sonar Critical/Major Cleanup
**Co zrobiono:**
- Swiezy Sonar baseline: 648 OPEN BLOCKER/CRITICAL/MAJOR przed cleanupem; finalnie 605 po zmianach.
- Delivery status cluster wyczyszczony do 0 issue w targetowanych plikach: domenowy `DeliveryStatusException`, guard helpers w repozytorium, tabelaryczne mapowania/URL-e w `DeliveryStatus`, kontrolery bez duplikowanych redirectow i widoki bez bezposrednich include.
- `OrdersStatisticsRepository` ma uproszczone cache kolumn i generowanie SQL kwot/daty/IN/ROUND; usunieto potwierdzone issue z nadmiarowymi returnami, zlozonoscia i duplikatami literalow w targetowanym zakresie.
- Pozostawiono `php:S1448` w `OrdersStatisticsRepository` jako nastepny refactor-slice, bo wymaga rozdzielenia klasy.
**Dlaczego:**
- Phase 139 ma pracowac na aktualnych wynikach SonarQube. Pierwsza fala wybiera bezpieczne refaktory bez zmiany zachowania biznesowego.
**BREAKING / migracja:**
- Brak migracji DB i brak zmian breaking.
## 2026-05-17 - Phase 138 Plan 01: Security and Legacy Hardening
**Co zrobiono:**
- `EmailMailboxController::testConnection()` uzywa strict TLS verification dla implicit SSL i STARTTLS.
- `SMTP_ALLOW_SELF_SIGNED_DEV` pozwala na self-signed/unverified certyfikaty tylko lokalnie/dev/testing.
- `TemplateVariableCatalog` centralizuje zmienne e-mail/SMS i blokuje zapis szablonow z nieznanymi placeholderami.
- `Session` dostal helpery `get/set/has/forget/pull`; raw `$_SESSION` przeniesiono do tej warstwy.
- `Template` dostal `$component()` helper, a wskazane widoki przestaly uzywac hard `require` dla komponentow/partiali.
**Dlaczego:**
- Phase 134 potwierdzil aktywne ryzyka security/legacy: weak SMTP TLS, niespojna polityka zmiennych, raw session access i view include debt.
**BREAKING / migracja:**
- Brak migracji DB. Nowe/edytowane szablony z nieznanymi zmiennymi sa odrzucane.
## 2026-05-17 - Phase 137 Plan 01: Delivery Status Backlog Verification
**Co zrobiono:**
- Zweryfikowano runtime code dla `DELIVERY-STATUS-MGMT`: `SHIPMENT_STATUS_OPTIONS` i `SHIPMENT_STATUS_OPTION_MAP` nie istnieja juz w source poza historyczna dokumentacja.
- Potwierdzono, ze `AutomationController` uzywa `DeliveryStatus::getAllOptions()` / `getAllStatuses()` dla dropdownow i walidacji.
- Potwierdzono, ze `AutomationService` porownuje `shipment_status` bezposrednio po znormalizowanych kluczach DB.
- Lokalny MySQL byl niedostepny, wiec wykonano manualny read-only check przez `DB_HOST_REMOTE` bez zmiany runtime config.
- Read-only DB check: `delivery_statuses=11`, `shipment_status` conditions=3, `update_shipment_status` actions=0, stare klucze=0, invalid keys=0.
**Dlaczego:**
- Phase 134 oznaczyl `DELIVERY-STATUS-MGMT` jako wdrozone w kodzie, ale wymagajace operator verification po breaking change z Phase 108. Phase 137 potwierdzila, ze w aktualnych danych nie ma reguly wymagajacej recznej migracji.
**BREAKING / migracja:**
- Brak zmian runtime i brak migracji. Phase 137 jest verification-only.
## 2026-05-17 - Phase 136 Plan 01: Fakturownia Invoice Idempotency
**Co zrobiono:**
- Dodano migracje `20260517_000118_add_invoice_external_idempotency_state.sql` z polami `invoices.external_status`, `external_oid`, `external_attempted_at`, `external_error_message` oraz unikalnym indeksem `(config_id, external_oid)`.
- `InvoiceService::issueDelegated()` uzywa stabilnego Fakturownia `oid` z `orders.internal_order_number` (fallback `orderpro-{order_id}` tylko gdy brak numeru wewnetrznego).
- Przed zewnetrznym POST system sprawdza Fakturownie przez `GET /invoices.json?oid=...`; znaleziony dokument jest automatycznie podpiety do lokalnego wiersza.
- Delegowane wystawienie tworzy lokalny wiersz `pending_external` przed POST i finalizuje go po odpowiedzi Fakturowni.
- Po timeoutcie lub bledzie polaczenia system ponownie sprawdza `oid`; jesli faktura istnieje, podpina ja i zwraca sukces, a jesli nie istnieje, oznacza wiersz jako `failed_retryable`.
- `FakturowniaApiClient` dostal `findInvoiceByOid()` i wspolna normalizacje odpowiedzi faktury.
- Dodano `FakturowniaInvoiceIdempotencyTest` dla lookup-first retry, sukcesu POST, auto-attach po timeoutcie i retryable failure.
**Dlaczego:**
- `INVOICE-IDEMP-115` grozil druga faktura w Fakturowni, gdy pierwszy POST utworzyl dokument, ale odpowiedz nie dotarla do orderPRO. Fakturownia dokumentuje lookup po `oid`, a nie dokumentuje `Idempotency-Key`, wiec retry-safe flow opiera sie na stabilnym `oid` i lokalnym stanie.
**BREAKING / migracja:**
- Wymagana migracja `20260517_000118_add_invoice_external_idempotency_state.sql`.
- Brak zmian breaking dla faktur lokalnych; `invoice_number_counters` pozostaje bez zmian.
## 2026-05-16 - Phase 135 Plan 01: Accounting Net Correctness
**Co zrobiono:**
- `ReceiptService::buildItemsSnapshot()` zwraca teraz `total_net` obok `total_gross`, liczac netto per pozycja po realnej stawce VAT z `tax_rate`/`vat`.
- `ReceiptService::issue()` zapisuje nowe `receipts.total_net` z obliczonej sumy netto zamiast kopiowac kwote brutto.
- Koszt wysylki w paragonach i statystykach pozostaje traktowany jako 23% VAT.
- `OrdersStatisticsRepository::netAmountSql()` preferuje `orders.total_without_tax`, potem `orders.total_net`, a przy braku net zrodla liczy fallback z `order_items` (`original_price_without_tax` albo brutto/VAT/ilosc).
- Dodano test `ReceiptServiceNetCalculationTest` oraz rozszerzono `OrdersStatisticsRepositoryTest` o source-net precedence i mieszane stawki VAT.
**Dlaczego:**
- Phase 134 potwierdzil aktywne bugi `RECEIPT-NET-FIX` i `STAT-NET`: paragony zapisywaly netto jako kopie brutto, a statystyki zakladaly 23% VAT dla wszystkich zamowien bez source-level net.
**BREAKING / migracja:**
- Brak migracji i brak breaking changes. Operator wybral brak backfillu historycznych paragonow; zmiana dotyczy nowych paragonow i runtime statystyk.
---
## 2026-05-16 - Phase 129 Plan 01: Erli Status Mapping + Sync
**Co zrobiono:**
- Dodano mapowania statusow Erli pull/push, `ErliStatusSyncService`, `ErliStatusSyncHandler`, endpoint `PATCH /orders/{id}/status` w kliencie API oraz UI mapowan w `/settings/integrations/erli`.
- Import Erli odkrywa surowe statusy z inboxa i uzywa `erli_order_status_pull_mappings`; push obejmuje tylko reczne zmiany statusu z `order_status_history.change_source='manual'`.
**Dlaczego:**
- Phase 128 miala domyslne statusy; Phase 129 daje operatorowi kontrolowane mapowanie i bezpieczny push bez petli automatyzacji.
---
## 2026-05-14 - Phase 130 Plan 01: polkurier delivery status mappings UI
**Co zrobiono:**
- `src/Modules/Shipments/DeliveryStatus.php` — nowe stałe `POLKURIER_MAP` i `POLKURIER_DESCRIPTIONS` z 7 oficjalnymi kodami ORDER_STATUS z dokumentacji polkurier API v1.11 (`O``created`, `P``confirmed`, `A``cancelled`, `WP``in_transit`, `D``delivered`, `Z``returned`, `W``problem`). Wartości identyczne z migracją Phase 128 (`20260514_000115_seed_polkurier_delivery_status_mappings.sql`).
- `src/Modules/Shipments/DeliveryStatus.php` — rejestracja `'polkurier' => self::POLKURIER_MAP` w `PROVIDER_MAPS` (po `'allegro_edge'`), analogicznie w `PROVIDER_DESCRIPTIONS`, oraz w match expressions `normalize()`/`description()`. `getDefaultMappings('polkurier')` zwraca 7 wpisów.
- `src/Modules/Settings/DeliveryStatusesController.php` + `DeliveryStatusMappingController.php` — stałe `PROVIDERS` rozszerzone z 3 do 4 wpisów: `'polkurier' => 'polkurier'` (lowercase, spójne z Phase 127).
- `src/Modules/Shipments/DeliveryStatusMappingRepository.php``countAllUnmappedForBadge()`: lista providerów rozszerzona z `['inpost', 'apaczka', 'allegro_wza']` do `['inpost', 'apaczka', 'allegro_wza', 'polkurier']`. Badge "niezmapowane statusy" w menu Ustawień reaguje teraz na nieznane raw statusy polkuriera.
- View `_delivery-status-mappings-content.php` automatycznie iteruje po `$providersList` z controllera — żadnych zmian w widoku nie trzeba.
**Dlaczego:**
- Phase 128 zaseed-owała DB override (`delivery_status_mappings` 7 wpisów) ale UI mapowania pozostał hardcoded na 3 providerów. Operator nie miał jak zmapować/podejrzeć statusów polkuriera w panelu.
- Defaultowe mapowania hardcoded w kodzie (nie tylko z DB) — spójność z InPost/Apaczka/Allegro (wszyscy mają hardcoded fallback). UI działa od razu, niezależnie czy operator uruchomił migrację Phase 128.
- Pattern `provider addition`: 5 punktów edycji w 4 plikach (1 const definition + 2 PROVIDER_* + 2 match arms + 2× PROVIDERS controller + 1 badge providers list) — checklist do reuse dla następnych przewoźników.
**Side-effects:**
- Migracja `20260514_000115_seed_polkurier_delivery_status_mappings.sql` (Phase 128) staje się no-op po wdrożeniu Phase 130 — DB override == hardcoded default → render `is_custom=true` ale ta sama wartość. Migracja może być uruchomiona lub nie.
**Files modified:**
- `src/Modules/Shipments/DeliveryStatus.php`
- `src/Modules/Settings/DeliveryStatusesController.php`
- `src/Modules/Settings/DeliveryStatusMappingController.php`
- `src/Modules/Shipments/DeliveryStatusMappingRepository.php`
- `.paul/codebase/tech_changelog.md` (this entry)
---
## 2026-05-14 - Phase 129 Plan 01: Order User Notes module
**Co zrobiono:**
- Migracja `database/migrations/20260514_000116_extend_order_notes_user_authored.sql``order_notes` rozszerzona o `user_id INT UNSIGNED NULL` (FK → `users(id)` ON DELETE SET NULL), `author_name VARCHAR(190) NULL`, oraz indeks `idx_order_notes_type_order (note_type, order_id)`. Wszystkie ADD owinięte w `INFORMATION_SCHEMA` guard z DDL no-op fallback (`ALTER TABLE COMMENT`) — pattern Phase 115/125.
- `src/Modules/Orders/OrderNotesService.php` — nowy serwis CRUD nad `order_notes` z `note_type='user'`. Metody: `listUserNotes`, `listImportedNotes`, `countUserNotes`, `findById`, `create`, `update`, `delete`. Autoryzacja przez `WHERE user_id = :user_id` w UPDATE/DELETE — rowCount=0 ⇒ rzut `RuntimeException(code=403)`. Walidacja `body`: trim, niepuste, ≤ 2000 znakow.
- `src/Modules/Orders/OrdersRepository.php` — dodany `userNotesCountSubquerySql($orderAlias)` (subquery `COUNT(*) FROM order_notes WHERE note_type='user'`) używany w `paginateSql()` jako kolumna `user_notes_count`. `loadOrderNotes()` zawężony do `note_type <> 'user'` (importowane ze źródła). `transformOrderRow()` ekspozuje `user_notes_count`.
- `src/Modules/Orders/OrdersController.php` — nowa opcjonalna zależność `OrderNotesService` w konstruktorze (na końcu, nullable, BC-safe). 3 metody: `storeNote`, `updateNote`, `deleteNote` (każda CSRF + sesja + try/catch `RuntimeException`/`InvalidArgumentException`; rejestruje `order_activity_log event_type='note'` przez `OrdersRepository::recordActivity`). `toTableRow()` renderuje `<a class="order-notes-badge" href="/orders/{id}#notes">[N]</a>` obok numeru zamówienia gdy `user_notes_count > 0`. `show()` pobiera `userNotes` + `currentUserId` i przekazuje do widoku.
- `routes/web.php` — 3 nowe route'y `POST /orders/{id}/notes`, `POST /orders/{id}/notes/{noteId}/update`, `POST /orders/{id}/notes/{noteId}/delete`. `OrderNotesService` instancjonowany przed `new OrdersController(...)` i przekazany ostatnim argumentem.
- `resources/views/orders/show.php` — sekcja "Wiadomości i załączniki" przebudowana na 3 bloki: (1) `<div class="order-user-notes" id="notes">` z listą notatek (data · autor) + akcjami edit/delete dla autora + inline formularz dodawania, (2) ukryty `order-note-edit-form` per notatka rozwijany przez JS, (3) opcjonalny blok "Wiadomości ze źródła" gdy `$notesList !== []` (importowane, bez akcji).
- `resources/lang/pl.php` — 10 nowych kluczy `orders.details.notes_user_*` / `notes_imported_title` (UI labels w PL).
- `resources/scss/modules/_order-notes.scss` (nowy) + `@use` w `app.scss``.order-notes-badge` (niebieskoszary `#eef2ff/#4338ca`), `.order-user-notes`, `.order-event--user` (lewa krawędź `#6366f1`), `.order-imported-notes` (opacity 0.75), `.btn-link`, `.order-note-form`, `.order-note-edit-form`. CSS przebudowany via `npm run build:css`.
- `public/assets/js/modules/order-notes.js` (nowy) + `<script>` w `layouts/app.php` — wanilijowy JS: klik "Edytuj" toggle'uje `js-order-note-body``js-order-note-edit-form`, klik "Anuluj" wraca, submit formularza DELETE przechwycony i potwierdzany przez `OrderProAlerts.confirm({title, message, danger:true, onConfirm})` (options-object API z decyzji Phase 114). Idempotent guard `window.__orderNotesInit` + `dataset.bound`.
- `.paul/codebase/db_schema.md` — sekcja `order_notes` rozszerzona o pełne kolumny + notatkę Phase 129-01 (note_type='user' vs imported).
**Dlaczego:**
- Operator potrzebował miejsca na własne adnotacje per zamówienie (uzgodnienia z klientem, ustalenia wewnętrzne) niezależne od notatek importowanych z shopPRO/Allegro. Bez badge'a na liście trzeba by wchodzić w każde zamówienie żeby sprawdzić czy ma notatki.
- Reuse istniejącej tabeli `order_notes` (Plan clarification #1) zamiast nowej tabeli — mniej obiektów DB, jeden punkt zarządzania, semantyka rozróżniona przez `note_type`. UNIQUE `(order_id, source_note_id)` nadal działa bo MySQL traktuje wiele NULL jako unique.
- Brak admin override (Plan clarification #3 z dopiskiem): aplikacja nie ma systemu ról (`grep -rn "is_admin|role=" src/Modules/Auth` zwrócił 0 trafień). Autoryzacja przez `note.user_id = session.user_id` — operator który dodał notatkę edytuje/usuwa, inni widzą ale nie modyfikują. Pełen admin-override odłożony do osobnej fazy po wprowadzeniu ról.
- Indeks `idx_order_notes_type_order (note_type, order_id)` zapewnia że subquery `user_notes_count` w paginacji `/orders/list` nie degraduje performance przy rosnącej liczbie notatek (Phase 106 pattern dla `customer_returned_count`).
**BREAKING:** brak — wszystkie zmiany BC. `loadOrderNotes()` teraz zwraca tylko `note_type <> 'user'`, ale nikt poza `findDetails()` jej nie używa, a sekcja widoku zachowuje wstecznie kompatybilne `$notesList` z importowanych notatek (osobny blok pod nową sekcją "Notatki").
---
## 2026-05-14 - Phase 128 Plan 01: polkurier ShipmentService + Tracking + UI prepare
**Co zrobiono:**
- `src/Modules/Settings/PolkurierApiClient.php` — pelen kontrakt API: `createShipment` (`apimetod=create_order`), `getLabel` (`get_label`), `getStatus` (`get_status`), `cancelOrder` (`cancel_order`), `getAvailableCarriers` (`available_carriers`), `getInpostParcelMachines` (`inpost_parcel_machines`), `getCourierPoints` (`get_courier_point`). Wspolny prywatny `call($apimetod, $data, $login, $token): mixed` parsuje envelope `{status, response}`; sukces -> zwraca `response`, blad -> rzuca `RuntimeException` z trescia z `response` (string albo zserializowany JSON dla tablic). Kontrakt zweryfikowany na oficjalnej dokumentacji PDF v1.11 (marzec 2026) — pobrana z `https://www.polkurier.pl/files/download/api_documentation_pdf`, zachowana w `.paul/phases/128-polkurier-shipment-service/polkurier-api-docs.txt`.
- `src/Modules/Shipments/PolkurierShipmentService.php``final class implements ShipmentProviderInterface` (`code()='polkurier'`). Pelen flow `createShipment($orderId, $formData)`: walidacja credentials/sender, `normalizeShipmentType()` mapuje `package_type` (PACKAGE/BOX/...) na zbior polkuriera `[box,envelope,palette,small_parcel,parcel_size_20]` (lowercase wymagane przez API — odkryte podczas live testu), `splitStreetAndNumber()` rozdziela ulice regexem na `street`/`housenumber`/`flatnumber`, `buildRecipient` z payload `order_addresses` + override z formularza, `buildPickup` z domyslnym `nextBusinessDay()` + 10:00-16:00, `COD` z bank account z `company_settings`. Po sukcesie API: `extractOrderNumber` (priorytet `number` z SDK Order entity), `extractTrackingNumber` (priorytet `waybills[0].number` z OrderWaybill entity), synchroniczna proba `downloadLabel`. Diagnostyka: gdy `orderno=''`, zapisuje fragment surowej odpowiedzi do `shipment_packages.error_message`.
- `src/Modules/Shipments/PolkurierTrackingService.php``final class implements ShipmentTrackingInterface`. `getDeliveryStatus($package)` woła `get_status`, parsuje `status_code`, mapuje przez `DeliveryStatus::normalizeWithOverrides('polkurier', ...)` z `delivery_status_mappings`. Graceful: null przy braku credentials/wyjatku API/braku `status_code`.
- `src/Modules/Shipments/DeliveryStatus.php` — fallback URL sledzenia dla `provider='polkurier'`: `https://polkurier.pl/sledz-paczke/<tracking>`. Carrier_id routing (DPD/UPS/GLS/InPost/Pocztex) dziala automatycznie przez istniejacy `matchCarrierByName()` (carrier_id ustawiany na servicecode z polkuriera, np. "INPOST" lub "DPD" — substring match).
- `routes/web.php``new PolkurierShipmentService(...)` w `ShipmentProviderRegistry`. Import `App\Modules\Settings\PolkurierApiClient` + `App\Modules\Shipments\PolkurierShipmentService`.
- `src/Modules/Cron/CronHandlerFactory.php``new PolkurierTrackingService(new PolkurierApiClient(), new PolkurierIntegrationRepository($this->db, $this->integrationSecret), new DeliveryStatusMappingRepository($this->db))` w `ShipmentTrackingRegistry` w handlerze `shipment_tracking_sync`.
- `src/Modules/Shipments/ShipmentController.php``prepare()` fetchuje `polkurierServices` przez registry i przekazuje do widoku. `create()` rozszerzony o pola `service_code`/`pickup_date`/`pickup_time_from`/`pickup_time_to` przekazywane do `createShipment()`.
- `resources/views/shipments/prepare.php` — opcja "polkurier" w `#shipment-carrier-select`, panel `#shipment-polkurier-panel` z `<select id="shipment-polkurier-select">` (lista uslug z `available_carriers`), hidden `name="service_code"`, JS toggle (`showPanel`, `syncPolkurierFields`). Brak dedykowanego selektora punktu odbioru — operator wpisuje `receiver_point_id` w istniejacym text inpucie w sekcji Adres odbiorcy (np. `POP-RZE54`).
- Nowa migracja `database/migrations/20260514_000115_seed_polkurier_delivery_status_mappings.sql` — 7 wpisow `provider='polkurier'`: `O``created`, `P``confirmed`, `A``cancelled`, `WP``in_transit`, `D``delivered`, `Z``returned`, `W``problem`. Idempotentne (`ON DUPLICATE KEY UPDATE`). Kody z oficjalnej tabeli ORDER_STATUS PDF v1.11.
- `.paul/codebase/architecture.md` + `db_schema.md` — opisy fazy 128.
**Dlaczego:**
- Phase 127 dostarczyl fundament (settings + test_auth_api). Bez ShipmentService polkurier byl tylko `wystawiony w hubie integracji ale niedzialajacy`. Operator chcial realnie nadawac paczki przez polkurier obok Apaczki — szczegolnie dla DPD/UPS/GLS/InPost gdzie polkurier oferuje lepsze ceny.
- Pelny zakres (ShipmentService + TrackingService + UI prepare + delivery_status_mappings) w jednej fazie zgodnie z decyzja operatora z planu (clarifications, `delegation: off`, `autonomous: false` z checkpointem live testu na #114/#115).
**Live test iteracje (zarejestrowane podczas APPLY):**
1. Pierwszy submit polkurier → "Blad tworzenia przesylki: Nie podano uslugi Apaczka." Przyczyna: `ReferenceError` na zmiennej `polkurierPointIdInput` (pozostalej po usunieciu duplikatu selektora punktu) w `clearHiddenFields()` → handler `carrierSelect.change` crashowal przed `showPanel()`, `provider_code` zostawal na PHP-renderowanej wartosci `apaczka`. Fix: usuniecie martwej referencji.
2. Drugi submit → "Blad tworzenia przesylki: polkurier create_order: Typ paczki musi przyjmowac jeden z parametrow ze zbioru [box, envelope, palette, small_parcel, parcel_size_20]". Przyczyna: wysylanie `BOX` uppercase. Fix: `normalizeShipmentType()` z lowercase + aliasami.
3. Trzeci submit → utworzona w polkurier, ale w orderPRO `status=pending` (brak orderno w parsing). Przyczyna: shape odpowiedzi `create_order` zwraca `Order` entity z polem `number` (nie `orderno`). Fix: `extractOrderNumber` z priorytetem `number` + fallback list + obsluga wrappera `{order:{...}}`. Etykieta poprawnie parsowana z pola `file` (response GetLabel.php).
4. Czwarty test → etykieta A4 zamiast A6. Iteracja w bogus parametry (`format`/`label_size`/`paper_size`) wyslane do `get_label` — bez efektu, bo API ignoruje. Pobranie oficjalnej dokumentacji PDF potwierdzilo: `get_label` przyjmuje WYLACZNIE `orderno`. Rozmiar A4 vs A6 sterowany jest w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API. Operator zmienil w panelu — etykieta A6 OK.
**Deviation vs PLAN:**
- Plan deklarowal `delegation: off` — wykonane inline. Tasks 1-3 napisane przez orchestrator, checkpoint i Task 5/6 inline. Bez sub-agentow ze wzgledu na potrzebe szybkich iteracji po live test feedback (4 iteracje przed sukcesem) — spawn agentow zdublowalby koszt research z PDF.
- Plan deklarowal AJAX endpoint `/shipments/polkurier/points` + UI selector punktow paczkomatowych (Task 3 action). USUNIETY po feedback operatora — istnieje juz pole "Punkt odbioru" w sekcji Adres odbiorcy, operator wpisuje ID recznie (np. `POP-RZE54`). Usuniety: `PolkurierShipmentService::lookupPickupPoints()`, `ShipmentController::polkurierPoints()`, route, JS handler.
- AC-3 (TrackingService cron) dostarczony, ale niezweryfikowany na zywej bazie podczas APPLY (operator anulowal paczki w panelu polkurier po teście — cron tracking nie mial co pingowac). Dziala defensywnie (graceful null przy bledach), pierwszy realny passthrough nastapi przy nastepnej zywej paczce.
- Plan zakladal seedowanie mapowan po zaobserwowaniu realnych statusow w live tescie. Seed wykonany bazujac na oficjalnej tabeli ORDER_STATUS z PDF v1.11 (kody O/P/A/WP/D/Z/W) zamiast obserwacji — bezpieczniejsze i wyczerpujace.
- AC-1 wzmiankowal getAvailableCarriers/getParcelMachines/getPostOffices. `getInpostParcelMachines` i `getCourierPoints` zaimplementowane jako stuby na przyszle rozszerzenie UI, ale nie uzywane przez aktualny UI (operator wpisuje punkt recznie). `getPostOffices` POMINIETY — brak dedykowanej metody w SDK (jest tylko `inpost_parcel_machines` per courier, `pocztex_post_offices`, `kurier48_post_offices` — zlozenie tego w UI panel paczkomatow odlozone na kolejna faze).
**Follow-up:**
- Operator musi uruchomic migracje gdy XAMPP MySQL online: `php bin/migrate.php` (utworzy 7 wpisow `provider='polkurier'`).
- Po pierwszej zywej paczce w `in_transit` — weryfikacja crona `shipment_tracking_sync` (1x ping API polkuriera, zapis `delivery_status` + `delivery_status_raw` w `shipment_packages`).
- Kolejne fazy v3.7: paczkomaty UI panel (`InpostParcelMachines`/`PocztexPostOffices`/`Kurier48PostOffices`), presety przesylek z `provider_code='polkurier'`, `OrderValuationV2` (wycena przed nadaniem).
## 2026-05-14 - Phase 127 Plan 01: polkurier Integration Foundation
**Co zrobiono:**
- Nowa migracja `database/migrations/20260514_000114_create_polkurier_integration_settings.sql` — tabela `polkurier_integration_settings` (fixed `id=1`, FK do `integrations` CASCADE, kolumny: `login VARCHAR(190)`, `api_token_encrypted TEXT`, `default_label_format VARCHAR(8) DEFAULT 'PDF'`) + idempotentny seed rekordu `integrations.type='polkurier'`, `base_url='https://api.polkurier.pl/'`.
- `src/Modules/Settings/PolkurierIntegrationRepository.php` — single-instance repository (mirror `HostedSmsIntegrationRepository`): `getSettings()` zwraca `has_api_token: bool` zamiast plaintext, `saveSettings()` szyfruje Token API przez `IntegrationSecretCipher`, `getCredentials()` gatuje na `is_active=1`, `getIntegrationId()` jako single source of truth.
- `src/Modules/Settings/PolkurierApiClient.php` — POST do `https://api.polkurier.pl/` z JSON body `{authorization:{login,token}, apimetod, data}`. Endpoint test = `apimetod="test_auth_api"`. cURL z `SslCertificateResolver::resolve()`, PHP 8.5 compatible (brak `curl_close()`). Stuby createShipment/getLabel/getStatus/cancelOrder rzucaja RuntimeException — do implementacji w kolejnych fazach.
- `src/Modules/Settings/PolkurierIntegrationController.php` — endpointy `GET /settings/integrations/polkurier`, `POST .../save`, `POST .../test` (CSRF `_token`). `test` zapisuje wynik przez `IntegrationsRepository::updateTestResult()`.
- `resources/views/settings/polkurier.php` — formularz konfiguracji + przycisk realnego testu polaczenia. Wszystkie alerty przez komponent `resources/views/components/alert.php` (Phase 120 contract).
- `src/Modules/Settings/IntegrationsHubController.php` — dodany parametr `PolkurierIntegrationRepository $polkurier` i metoda `buildPolkurierRow()`; wiersz polkurier wstawiony zaraz po Apaczka.
- `routes/web.php` — DI wiring `PolkurierIntegrationRepository` + `PolkurierIntegrationController`, rozszerzony ctor `IntegrationsHubController`, 3 nowe routy `/settings/integrations/polkurier{,/save,/test}`.
- `resources/lang/pl.php` — sekcja `settings.polkurier.*` (title/description/fields/hints/token/status/actions/flash) + `settings.integrations_hub.providers.polkurier`.
- `.paul/codebase/db_schema.md` + `architecture.md` — opisy fazy 127.
**Dlaczego:**
- Operator dostaje drugiego brokera kurierskiego rownolegle z Apaczka (decyzja w `.paul/phases/127-polkurier-integration-foundation/127-01-PLAN.md`, clarifications).
- Single-instance bo polkurier to jedno konto operatora (mirror Apaczka/InPost/HostedSMS/SMSPLANET).
- Faza zamyka tylko warstwe ustawien + realny test (`apimetod=test_auth_api`); tworzenie przesylek, etykiety, tracking i mapowania metod dostawy beda w kolejnych fazach — analogicznie do tego jak Phase 116/117 zamknely tylko fundament HostedSMS/SMSPLANET.
**Deviation vs PLAN:**
- AC-1 wymagal kolumny `environment ENUM('production','sandbox')`. polkurier nie ma srodowiska sandbox (jeden produkcyjny endpoint `https://api.polkurier.pl/`), wiec kolumna `environment` zostala POMINIETA jako YAGNI.
- AC-1/AC-2 wymagaly tylko `api_token_encrypted`. polkurier API wymaga `login + token` razem w `authorization` (zweryfikowane w oficjalnym SDK https://github.com/Polkurier/polkurier-sdk — pliki `Auth.php`/`Request.php`/`Config.php`), wiec dodana kolumna `login VARCHAR(190)` z walidacja serwerowa.
- Plan deklarowal `delegation: auto` (sub-agents). Zadania wykonane inline z powodu swiezo zgromadzonego research o API polkuriera (Config/Auth/Request/Methods z SDK); spawn agentow powtorzylby ten research. Decyzja chroni kontekst i czas. Boundaries i acceptance criteria niezmienione.
**BREAKING:** brak.
## 2026-05-13 - Phase 126 Plan 01: Invoice GUS Field Mapping Fix (KRS heuristic)
**Co zrobiono:**
- Bugfix `/orders/{id}/invoice/create`: po kliknieciu "Pobierz z GUS" wartosci "Imie i nazwisko" i "Nazwa firmy" sprawialy wrazenie zamienionych dla JDG. Root cause: MF Biala Lista dla JDG zwraca w `subject.name` osobe fizyczna (np. "JACEK PYZIAK"), a JS bezwarunkowo wpisywal te wartosc do `#buyer_company_name` — pole "Imie i nazwisko" zostawalo z pre-fillem z `order_addresses.name`, ktory dla JDG czesto trzyma pelna nazwe firmy (np. "Project-Pro Pyziak Jacek").
- `MfWhitelistApiClient::lookupByNip()` — dodane do return array pola `krs: string` i `is_jdg: bool` (true gdy `subject.krs` jest puste). Pozostaly kontrakt (`name`, `tax_no`, address, regon, status_vat, raw) bez zmian.
- `InvoiceController::nipLookup` — propaguje `is_jdg` w JSON response `/api/nip/lookup` jako `data.is_jdg`.
- JS w `resources/views/accounting/invoice_form.php` — wybor pola docelowego dla `d.company_name` zalezy od `d.is_jdg`:
- `is_jdg=true` -> `#buyer_name` (Imie i nazwisko)
- `is_jdg=false` -> `#buyer_company_name` (Nazwa firmy)
Drugie pole nie jest tkniete — operator zachowuje pre-fill z zamowienia. Pola adresowe (street/postal_code/city) nadpisywane jak dotychczas.
**Dlaczego:**
- MF Biala Lista nie eksponuje "nazwy firmy" dla JDG (pole `name` to wlasciciel — osoba fizyczna). Pre-fill z `order_addresses.name` jest dla JDG bardziej wartosciowy (zawiera pelna nazwe firmy z zamowienia) niz MF `name`, wiec nie powinien byc nadpisany. Dla spolki (krs!=null) MF `name` to legal name — wlasciwe miejsce to "Nazwa firmy".
**Key decision:**
- Heurystyka JDG = `subject.krs === ''` (sygnal z MF). Pattern do reuse w innych miejscach jesli pojawia sie inny formularz oparty o NIP lookup.
**Files modified:**
- `src/Core/Http/MfWhitelistApiClient.php`
- `src/Modules/Accounting/InvoiceController.php`
- `resources/views/accounting/invoice_form.php`
**Pending verification:**
- AC-1: smoke `/orders/1090/invoice/create` (JDG, NIP 5170167517) -> "Imie i nazwisko"="JACEK PYZIAK", "Nazwa firmy"="Project-Pro Pyziak Jacek" niezmieniona.
- AC-2: smoke dla NIP spolki z aktywnym KRS -> "Nazwa firmy" otrzymuje legal name, "Imie i nazwisko" niezmienione.
- AC-3: `curl /api/nip/lookup?nip=5170167517` -> `data.is_jdg=true`.
---
## 2026-05-13 - Phase 125 Plan 01: invoice_requested Import Fix
**Co zrobiono:**
- Bugfix #1089: shopPRO order z `firm_nip` (bez kluczy z 5-elementowej listy `shouldRequestInvoice`) nie ustawial `invoice_requested=1` przy imporcie. Mapper wykrywal poprawnie (`is_invoice` przez heurystyke NIP), ale `ShopproOrdersSyncService::shouldRequestInvoice` mial wezsza liste kluczy -> UI w zakladce Platnosci wyswietlal odznaczony checkbox.
- `ShopproOrderMapper::mapOrderAggregate()` zwraca teraz top-level klucz `invoice_detected` (wynik `resolveInvoiceRequested($payload)`). Klucz `is_invoice` usuniety z tablicy `order` (nie odpowiada juz zadnej kolumnie DB).
- `ShopproOrdersSyncService::importOne()` propaguje `!empty($aggregate['invoice_detected'])` do `setInvoiceRequested(true)` zamiast wlasnej heurystyki. Stara metoda `shouldRequestInvoice` usunieta (zastapiona heurystyka mappera — zero duplikacji).
- `AllegroOrderImportService::shouldRequestInvoice($payload)` (nowa prywatna metoda) — rozszerza detekcje o `invoice.naturalPerson === false`, `invoice.address.taxId`, `invoice.companyName`/`invoice.address.company.name`. Wczesniej tylko `invoice.required` -> analogiczna luka jak shopPRO dla klientow Allegro z NIP bez `required=true`.
- Migracja `20260513_000113_drop_orders_is_invoice_and_backfill_invoice_requested.sql`:
- Idempotentny guard przez `information_schema.COLUMNS` + prepared statements (DDL no-op gdy kolumna juz nie istnieje, pattern z Key Decision 2026-05-10).
- Backfill: `UPDATE orders SET invoice_requested=1 WHERE is_invoice=1 AND invoice_requested=0` (7 zamowien na produkcji, w tym #1089).
- DROP COLUMN `orders.is_invoice` (legacy z Phase 115, dryft wzgledem `invoice_requested`).
- `OrderImportRepository::insertOrder()` SQL — usuniety `is_invoice` z kolumn INSERT i `:is_invoice` z VALUES. `orderParams()` — usunieta linia mapowania. Docstring `updateOrderDelta()` (Phase 112) — usunieta wzmianka.
- `OrdersRepository` — usuniety `o.is_invoice` z SELECT (`paginate` query) i `transformOrderRow()` hydrate (klucz `is_invoice` nie wystepuje juz w zwracanych row'ach).
**Dlaczego:**
- Bug #1089: zamowienie shopPRO z fakturowymi danymi firmowymi mialo `is_invoice=1` (mapper) ale `invoice_requested=0` (sync service). UI pokazywal odznaczony checkbox -> przycisk "Wystaw fakture" niedostepny -> operator musial recznie klikac toggle.
- Dryft: Phase 115 zostawila dwie kolumny dla tej samej semantyki (`is_invoice` legacy + `invoice_requested` nowy). Dwie sciezki detekcji (mapper vs. sync service) mialy rozna szerokosc heurystyki -> systematyczny rozjazd dla shopPRO orders z `firm_name`/`firm_nip`.
- Fix architekturalny: jedno zrodlo prawdy (`invoice_requested`), jedna heurystyka per zrodlo (mapper dla shopPRO, prywatna metoda dla Allegro).
**BREAKING:**
- Kolumna `orders.is_invoice` przestaje istniec po migracji `20260513_000113_*`. Wewnetrzny kontrakt — nie wystepuje w API ani odpowiedziach JSON. Jezeli ktokolwiek (skrypt operatora, raport custom) czytal `is_invoice` z DB -> przelaczyc na `invoice_requested`.
- `ShopproOrderMapper::mapOrderAggregate()` zwraca teraz dodatkowy top-level klucz `invoice_detected` (boolean). Klucz `is_invoice` znika z podtablicy `order` (nie odpowiada juz kolumnie DB).
## 2026-05-12 - Phase 124 Plan 01: SMS Templates
**Co zrobiono:**
- Nowa tabela `sms_templates(id, name, body, is_active, created_at, updated_at)` + indeks `(is_active, name)` — migracja `20260512_000112_create_sms_templates.sql` (DDL).
- `Sms\SmsTemplateRepository` z minimalnym CRUD (`listAll/listActive/findById/save/delete/toggleStatus`); walidacja name+body w `save()`.
- `Sms\SmsVariableResolver` (wydzielony z `Email\VariableResolver`) — wspolna logika `buildVariableMap` + `resolve` dla Email i SMS (placeholdery `{{zamowienie.*|kupujacy.*|adres.*|firma.*|przesylka.*}}`).
- `Email\VariableResolver` zrefaktorowany na fasade — konstruktor opcjonalnie przyjmuje SmsVariableResolver; metody publiczne deleguja. `EmailSendingService` bez zmian.
- `Settings\SmsTemplateController` + widoki `settings/sms-templates.php` (lista) + `settings/sms-templates-form.php` (CRUD form z paleta zmiennych po prawej, licznikiem znakow, walidacja maxlength 918).
- 7 nowych rout: `GET/POST /settings/sms-templates`, `/create`, `/edit`, `/save`, `/delete`, `/toggle`, `/variables`.
- Dropdown "Wybierz szablon" w zakladce SMS na `/orders/{id}` (renderowany tylko gdy istnieja aktywne szablony) -> JS module `sms-template-picker.js` fetchuje `GET /orders/{id}/sms/template?template_id=N` i wkleja rozwiniete body do textarea. Przy niepustej textarea pyta przez `OrderProAlerts.confirm({...})` (options-object API).
- `OrdersController::smsTemplate()` — nowy endpoint JSON; rozszerzony konstruktor o `?SmsTemplateRepository`, `?SmsVariableResolver`, `?CompanySettingsRepository` (default null = BC).
- Sidebar Ustawien rozszerzony o link "Szablony SMS" (active state na `currentSettings === 'sms-templates'`).
- Nowy SCSS partial `modules/_sms-templates.scss` (paleta zmiennych, licznik znakow). Import w `app.scss`.
- Tlumaczenia `orders.details.sms.template_picker(_placeholder)` w `resources/lang/pl.php`.
**Dlaczego:**
- Operator wysyla powtarzalne SMS-y (numer sledzenia, przypomnienie o platnosci, prosba o opinie). Szablony eliminuja recznie wpisywanie tekstu i tracking number, redukujac wysylke do dropdown + ewentualnej korekty.
- Wspolny VariableResolver bo dokladnie te same placeholdery sa potrzebne w Email i SMS (DRY); zachowanie kontraktu `Email\VariableResolver` jako fasady = zero ryzyka regresji w EmailSendingService.
- Stopka SMSPLANET pozostaje doklejana wylacznie przez `SmsConversationService::buildFinalOutboundBody()` (Phase 122) — nie duplikujemy jej w szablonach, walidacja 918 znakow obowiazuje na finalnej tresci.
**Migracja:**
- `php bin/migrate.php` po wlaczeniu MySQL — utworzy `sms_templates`.
- Operator po wdrozeniu tworzy szablony manualnie z `/settings/sms-templates`.
**BREAKING:** brak. `OrdersController` ctor: nowe params optional. `Email\VariableResolver` ctor: nowy opcjonalny drugi argument (default null = self-construct SmsVariableResolver).
## 2026-05-12 - Phase 123 Plan 01: Receipts Export VAT Breakdown
**Co zrobiono:**
- `ReceiptService::buildItemsSnapshot()` zapisuje `vat` (procent) per pozycja w `items_json` (z `order_items.tax_rate`, fallback 23.0). Pozycja "Koszt wysylki" dostaje `vat=23.0`.
- `AccountingController::export()`: nowe naglowki XLSX `Numer | Data wystawienia | Kwota brutto | Kwota netto | Stawka VAT | Kwota VAT` (usunieto `Data sprzedazy`, `Konfiguracja`, `Nr zamowienia`, `Nr referencyjny`).
- Eksport emituje osobny wiersz na kazda stawke VAT wystepujaca w paragonie (multi-rate breakdown z grupowania `items_json` po `vat`).
- Legacy paragony (snapshot bez `vat`) zwracaja jeden wiersz ze stawka 23%, `Kwota netto = total_net`, `Kwota VAT = total_gross - total_net`.
- Dodany prywatny helper `AccountingController::buildVatBreakdown()` + `formatVatRate()` (np. 23.0 -> "23%", 7.5 -> "7.5%").
**Dlaczego:**
- Ksiegowy potrzebuje arkusza z rozbiciem VAT per stawka do zaczytania do ksiegowosci. Stary eksport zawieral pola operacyjne (data sprzedazy, konfiguracja, ref) bez podstawowych pol VAT.
**Weryfikacja:**
- `php -l` na obu plikach OK; manualny eksport XLSX dla mieszanej listy paragonow po wdrozeniu.
## 2026-05-12 - SMSPLANET Inbound Webhook Fix
**Co zrobiono:**
- Poprawiono inbound SMSPLANET: dopasowanie po telefonie uzywa `order_addresses.phone`, a nie nieistniejacego w produkcji `orders.buyer_phone`.
- Dodano GET dla `/webhooks/smsplanet/inbound` obok POST, dekodowanie formatu 2WAY `message=<JSON>`, odpowiedz plain `OK` po sukcesie i odporniejsze scalanie JSON body z parametrami requestu.
**Dlaczego:**
- Publiczny POST webhooka zwracal 422 przez blad SQL `Unknown column 'o.buyer_phone'`, wiec odpowiedzi SMS nie byly zapisywane.
## 2026-05-12 - Phase 122 Plan 01: SMSPLANET Default SMS Footer
**Co zrobiono:**
- Dodano migracje `20260512_000111_smsplanet_default_footer.sql` z kolumna `smsplanet_integration_settings.default_footer`.
- Rozszerzono konfiguracje SMSPLANET o opcjonalna stopke SMS z limitem 300 znakow.
- Testowa wysylka oraz SMS z zamowienia dopinaja stopke przez pusta linie, waliduja finalna tresc w limicie 918 znakow i zapisuja finalne body w historii rozmowy.
**Dlaczego:**
- Operator ma utrzymywac jeden wspolny podpis firmy bez recznego kopiowania go do kazdej wiadomosci SMS.
**Weryfikacja:**
- Do uzupelnienia po APPLY.
## 2026-05-12 - Phase 121 Plan 01: SMSPLANET Conversation + Notifications
**Co zrobiono:**
- Dodano migracje `20260512_000110_smsplanet_conversation_notifications.sql` z tabelami `sms_messages`, `notifications` oraz polami `sender_mode` i `sender_phone`.
- Dodano backend `Sms` i `Notifications`, publiczny webhook SMSPLANET, zakladke SMS w zamowieniu, centrum powiadomien, topbar badge i polling JS.
- Usunieto tymczasowy override testowego nadawcy SMSPLANET; API uzywa wybranego trybu nadawcy.
- Poprawiono migracje po pierwszej probie na bazie: rzeczywiste `orders.id` ma typ `BIGINT UNSIGNED`, wiec `sms_messages.order_id` i `notifications.related_order_id` tez musza miec `BIGINT UNSIGNED`.
**Dlaczego:**
- Operator ma prowadzic dwukierunkowa rozmowe SMSPLANET w szczegolach zamowienia i widziec nowe odpowiedzi klientow globalnie.
**Weryfikacja:**
- `php -l` PASS dla nowych/zmienionych PHP.
- `npm run build:css` PASS.
- Migracja PASS przez techniczne polaczenie `DB_HOST_REMOTE`; manualne smoke testy UI/webhook nadal wymagaja sesji w aplikacji.
- `sonar-scanner` niedostepny w PATH.
## 2026-05-12 - Phase 120 Plan 01: Alert Component Unification
**Co zrobiono:**
- Dodano komponent `resources/views/components/alert.php` (params: `$type` info|success|warning|danger, `$message`/`$messageHtml`, `$dismissible`, `$role`) renderujacy `.alert` z inline SVG ikona, body i opcjonalnym przyciskiem dismiss.
- SCSS `_ui-components.scss`: `.alert` zmieniony na flex z `.alert__icon`/`.alert__body`/`.alert__dismiss`; dodany brakujacy wariant `.alert--info` (#eff6ff/#bfdbfe/#1e3a8a); dodany wrapper `.alerts-stack`.
- Vanilla JS `public/assets/js/modules/alert-dismiss.js` z idempotent guardem i delegated handlerem na `[data-alert-dismiss]`.
- `Flash` rozszerzony o `push(string $type, string $message): void` i `all(): array` z BC dla `set/get``all()` konsumuje typowana kolejke + skanuje legacy `_flash` z heurystyka klucza (error/danger/fail → danger, warning → warning, success/.save/.created/.deleted/.toggled → success, reszta → info).
- Layouty `app.php`, `auth.php`, `public.php` na poczatku content area iteruja `Flash::all()` przez komponent w `.alerts-stack` i ladja `alert-dismiss.js`.
- Migracja 36 widokow (34 z planu + `orders/show.php` + `shipments/prepare.php`): inline `<div class="alert alert--TYPE">` zastapione `include components/alert.php`; `.flash--error`/`.flash--success` w starych widokach takze przeniesione na komponent.
**Dlaczego:**
- Po teste polaczenia Fakturowni alert `OK (HTTP 200)` mial klase `alert--info`, ale `.alert--info` nie istnial — efekt: czarny tekst na bialym tle bez ikony. Zglosenie operatora po Phase 118.
- Wiele wzorcow alertow w 36 widokach roznilo sie szczegolami (mt-12/role/struktura), brak komponentu wielokrotnego uzytku. Po Phase 120 jeden plik (`components/alert.php`) jest jedynym renderem markupu alert + centralny renderer flash w 3 layoutach przyjmuje przyszle `Flash::push()` bez koniecznosci powtarzania kodu w widokach.
**Boundaries (zachowane):**
- Kontrolery NIE zmieniane — `Flash::set/get` BC; wzorce `Flash::set('module.key', '...')` + view local `$flashXxx` dzialaja jak dotychczas (widok renderuje przez komponent).
- Modul `resources/modules/jquery-alerts` (dialogi `OrderProAlerts`) niezmieniany — osobny system.
- `email-mailboxes.php` JS-generowane alerty AJAX testu SMTP — pozostawione bez zmian (uzywaja `.alert` SCSS, brak markupu komponentu — out of scope).
- `.flash--*` SCSS nie usuniety (deferred cleanup) — widoki juz go nie uzywaja.
- Build SCSS → CSS poza zakresem; `public/assets/css/app.css` musi byc zregenerowany lokalnie po wdrozeniu.
## 2026-05-12 - Phase 119 Plan 01: Re-import total_paid Protection
**Co zrobiono:**
- `OrderImportRepository::updateOrderDelta()` przebudowane na dynamiczny SQL builder: `total_paid` i `is_canceled_by_buyer` są dołączane do UPDATE warunkowo. Gdy `payment_status` w bazie == `payment_status` z payloadu źródła, `total_paid` NIE jest aktualizowany. `is_canceled_by_buyer` jest pomijany w tej samej sytuacji, chyba że źródło flaguje anulowanie (`$cancelledBySource=true`) — wtedy zawsze wpisywane.
- `upsertOrderAggregate()` wylicza `$paymentStatusUnchanged` przed wywołaniem delty i propaguje wraz z `$cancelledBySource`.
- Test PHPUnit `tests/Unit/OrderImportRepositoryTest.php` z 3 scenariuszami (preserve / transition / cancel propagation). Uruchomienie odroczone — `vendor/` nieobecne w środowisku, składnia PHP zweryfikowana przez `php -l`.
**Dlaczego:**
- Incydent zamówienia #976: operator usunął 2 pozycje Girlanda i zwrócił klientowi 28,00 PLN, obniżając `total_paid` ze 119,00 na 91,00. Audyt re-importu (Phase 112-01 delta-only) wykazał, że istniejące `updateOrderDelta` nadpisywało `total_paid` z payloadu źródła przy każdym wywołaniu, jeśli identical-payload guard nie zadziałał. Ręczne korekty kwoty były ulotne.
**Boundaries (zachowane):**
- `paymentTransition` (Phase 111) i `statusOverwriteAllowed` (Phase 62) działają bez zmian.
- Cancel propagation (`$cancelledBySource` override) z Phase 112-01 nadal wymusza `status_code='anulowane'` i — od Phase 119 — `is_canceled_by_buyer=1` nawet gdy `payment_status` stabilne.
- Identical-payload no-op guard (Phase 112-01) nadal pierwsza linia obrony — Phase 119 dotyczy tylko ścieżki gdy guard nie zadziałał (payload się różni, ale `payment_status` stabilne).
## 2026-05-12 - Phase 118 Plan 01: Fakturownia Single Instance
**Co zrobiono:**
- Dodano migracje `20260512_000109_fakturownia_single_instance.sql`, ktora wybiera aktywna instancje Fakturowni, przepina delegowane `invoice_configs.integration_id` na jeden globalny rekord i usuwa nadmiarowe konta Fakturowni po przepieciu zaleznosci.
- Przebudowano `FakturowniaIntegrationRepository` na jedna globalna konfiguracje (`getSettings`, `saveSettings`, `getIntegrationId`, `getCredentials`) z kompatybilnym `findAll()` zwracajacym jeden element.
- Uproszczono `FakturowniaIntegrationController` i widok `/settings/integrations/fakturownia` do pojedynczego formularza konfiguracji i testu polaczenia.
- Hub integracji pokazuje Fakturownie jako jedna instancje, bez licznika kont.
- Zapis delegowanej konfiguracji faktury ustawia `invoice_configs.integration_id` na globalny rekord Fakturowni; UI konfiguracji faktury nie pokazuje juz selecta kont.
**BREAKING / migracja:**
- Po migracji nie ma juz wielu kont Fakturowni. Jesli baza miala wiele rekordow `integrations.type='fakturownia'`, zachowany zostaje aktywny rekord (fallback: uzywany przez konfiguracje faktur, potem najnizsze id), a pozostale sa usuwane.
## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS
**Co zrobiono:**
- Dodano migracje `20260512_000108_create_smsplanet_integration_settings.sql` z pojedyncza konfiguracja `smsplanet_integration_settings` i bazowym wpisem `integrations` typu `smsplanet`.
- Dodano `SmsplanetIntegrationRepository` z obsluga metod autoryzacji `token` oraz `key_password` i szyfrowaniem sekretow przez `IntegrationSecretCipher`.
- Dodano `SmsplanetApiClient` dla SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`.
- Dodano `SmsplanetIntegrationController` i trasy `/settings/integrations/smsplanet`, `/save`, `/test`.
- Dodano widok `resources/views/settings/smsplanet.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz panelem ostatniego testu (`OK`, HTTP, `messageId`).
- Dodano SMSPLANET do hubu integracji `/settings/integrations`.
- Poprawiono import `IntegrationSecretCipher`, aby rzucal istniejacy `App\Core\Exceptions\IntegrationConfigException`.
**Dlaczego:**
- Operator potrzebuje drugiej bramki SMS analogicznej do HostedSMS, ale bez uruchamiania jeszcze automatyzacji lub historii wysylek.
- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora.
- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API.
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
**Co zrobiono:**
- Dodano migracje `20260512_000107_create_hostedsms_integration_settings.sql` z pojedyncza konfiguracja `hostedsms_integration_settings` i bazowym wpisem `integrations` typu `hostedsms`.
- Dodano `HostedSmsIntegrationRepository` z szyfrowaniem hasla przez `IntegrationSecretCipher`.
- Dodano `HostedSmsApiClient` dla HostedSMS SimpleAPI (`POST https://api.hostedsms.pl/SimpleApi`).
- Dodano `HostedSmsIntegrationController` i trasy `/settings/integrations/hostedsms`, `/save`, `/test`.
- Dodano widok `resources/views/settings/hostedsms.php` z konfiguracja i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym panelem ostatniego testu (`OK`, HTTP, MessageId).
- Dodano HostedSMS do hubu integracji `/settings/integrations`.
**Dlaczego:**
- Operator potrzebuje najpierw zapisac dane HostedSMS i sprawdzic realna wysylke SMS, zanim integracja zostanie wykorzystana w automatyzacjach lub komunikacji z klientami.
- Test uzywa rzeczywistej wysylki, bo SimpleAPI nie udostepnia osobnego endpointu ping/test.
- Haslo nie jest ujawniane po zapisie; UI pokazuje tylko status zapisanego sekretu.
## 2026-05-10 - Phase 115 Plan 01: Wystawianie faktury z zamowienia
**Co zrobiono:**
- **GUS lookup (Task 3b):** `FakturowniaApiClient::lookupClientByNip()` — GET `/clients/gus.json?nip=...&api_token=...` z walidacja NIP (10 cyfr) i parsowaniem `{name, tax_no, street, post_code, city, country}`. `InvoiceController::gusLookup()` jako AJAX endpoint `GET /api/fakturownia/gus-lookup?nip=...&config_id=...` — resolwer konta Fakturownia (preferuje `invoice_configs.integration_id` gdy delegated, fallback na pierwsze aktywne konto type='fakturownia'). `invoice_form.php` ma przycisk "Pobierz z GUS" obok pola NIP — vanilla JS fetch + auto-fill `buyer_company_name/street/postal_code/city`. Czyni "wystaw fakture" jednoklikowym po wpisaniu NIP. `InvoiceController` ctor rozszerzony o 2 deps (`FakturowniaIntegrationRepository`, `FakturowniaApiClient`).
- `InvoiceRepository` (nowy) — CRUD dla `invoices`: `findByOrderId`, `findById`, `insertLocal`, `insertDelegated`, `nextLocalNumber` (atomowy INSERT ON DUPLICATE KEY UPDATE na `invoice_number_counters`), `paginate` z filtrami search/config/mode/date.
- `InvoiceService` (nowy) — orchestrator wystawiania:
- `issue()` rozgalezia na `issueLocal()` (numer lokalny + Dompdf-ready snapshot) lub `issueDelegated()` (POST do Fakturowni PRZED INSERT lokalnym; na sukcesie zapis `external_invoice_id`/`external_pdf_url`/`invoice_number` z odpowiedzi).
- Static `extractBuyerTaxNumber()` parsuje NIP z roznych sciezek payload_json (Allegro `invoice.address.taxId`, shopPRO `buyer.tax_number` i pokrewne).
- Snapshot pattern (snapshoty seller/buyer/items z Phase 8 wzorca) + obliczanie netto/brutto z VAT per pozycja.
- `buildFakturowniaPayload()` mapuje na format `https://app.fakturownia.pl/api`.
- `InvoiceIssueException` (nowy) — typed exception dla bledow biznesowych.
- `FakturowniaApiClient` rozszerzony — nowe `createInvoice()` (POST `/invoices.json` z body `{api_token, invoice}`, parsuje response na `id/number/view_url/pdf_url/raw`) i `buildPdfUrl()` (helper bez fetcha, do redirect 302). Stary stub `downloadPdf()` zastapiony.
- `InvoiceController` (nowy) — endpointy dla zamowienia (`create`, `store`, `show`, `pdf`) i lista `issuedList`. PDF: lokalna -> Dompdf inline (mirror `ReceiptController::pdf`); delegowana -> redirect 302 na `external_pdf_url`. Walidacja `invoice_requested=1` przed otwarciem formularza.
- `OrdersController::toggleInvoiceRequested` (nowy) — AJAX endpoint POST `/orders/{id}/invoice-requested/toggle` z CSRF + `recordActivity('invoice_requested_changed')`.
- `OrdersRepository::setInvoiceRequested(int, bool)` (nowy) — UPDATE `orders.invoice_requested`.
- `OrdersController::show()` rozszerzone — przekazuje `invoices` + `invoiceConfigs` do widoku przez 2 nowe optional ctor params (`InvoiceRepository`, `InvoiceConfigRepository`).
- `AllegroOrderImportService::importSingleOrder` — przy `wasCreated=true` i `payload.invoice.required` -> ustawia `orders.invoice_requested=1` (delta-only re-import nie nadpisuje manualnej flagi).
- `ShopproOrdersSyncService::shouldRequestInvoice()` (nowy) — flexible parser sprawdzajacy `wants_invoice`, `invoice_required`, `invoice.required`, `buyer.wants_invoice`, `buyer.invoice` w raw payload. Dziala tylko przy pierwszym imporcie.
- 4 nowe widoki:
- `resources/views/accounting/invoice_form.php` — formularz wystawiania (config select z label "Lokalnie/Fakturownia: {prefix}", NIP prefilled z payload, manualne nadpisanie buyer fields, podglad pozycji + sprzedawca; confirm dialog dla powtornego wystawienia uzywa `OrderProAlerts.confirm` z options-object API).
- `resources/views/accounting/invoice_preview.php` — HTML preview faktury z badgem trybu (Lokalnie/Fakturownia: {prefix}) + dual-button PDF.
- `resources/views/accounting/invoice_pdf.php` — Dompdf template "FAKTURA VAT" z parties section, items table (netto/brutto/VAT per pozycja), termin platnosci, konto bankowe.
- `resources/views/accounting/invoices_issued_list.php` — lista wystawionych faktur z filtrami (search/config/mode/date_from/date_to) + paginacja + badge Tryb (Lokalnie / Fakturownia: {prefix}).
- `resources/views/orders/show.php` — header dostal `data-invoice-button-wrap` z przyciskiem "Wystaw fakture" (visible tylko gdy `invoice_requested=1`); pod headerem checkbox `data-invoice-requested-toggle` (auto-bound przez `invoice-requested-toggle.js`); pod tabem "Documents" nowa sekcja "Faktury" z badgem Tryb i akcjami Podglad/PDF.
- `resources/views/settings/accounting.php` — link "Faktury wystawione" w karcie Faktury.
- `public/assets/js/modules/invoice-requested-toggle.js` (nowy) — vanilla JS, AJAX POST z `_token` przy `change`, rollback checkbox state przy bledzie, optimistic show/hide `data-invoice-button-wrap`. Idempotent guard `data-bound`.
- `resources/views/layouts/app.php` — rejestracja `invoice-requested-toggle.js` z cache-busting.
- 6 nowych routes: `POST /orders/{id}/invoice-requested/toggle`, `GET /orders/{id}/invoice/create`, `POST /orders/{id}/invoice/store`, `GET /orders/{id}/invoice/{invoiceId}`, `GET /orders/{id}/invoice/{invoiceId}/pdf`, `GET /settings/accounting/invoices/issued`. Wszystkie pod `AuthMiddleware`.
- DI wiring w `routes/web.php`: `InvoiceRepository`, `InvoiceService`, `InvoiceController`. `OrdersController` instantiation rozszerzona o 2 params (`$invoiceRepository`, `$invoiceConfigRepository`).
- Docs: `architecture.md` (nowa sekcja "Phase 115"), `tech_changelog.md`, `todo.md` (INVOICE-IDEMP-115).
**Dlaczego:**
- Plan zatwierdzony jako pelny vertical slice — Phase 113 (DB) + 114 (configs CRUD) byly fundamentem; bez wystawiania z zamowienia funkcja byla bezuzyteczna operacjonalnie.
- Kolejnosc "POST do Fakturowni najpierw, INSERT lokalny po sukcesie" (per user decision) gwarantuje ze nie ma orphan rows w `invoices` gdy API padnie. Trade-off: brak idempotencji przy double-POST -> notatka INVOICE-IDEMP-115.
- Auto-set `invoice_requested` z importu pozwala operatorowi nie myslec o flagi dla typowych przypadkow (Allegro `invoice.required=true`, shopPRO klienci wpisujacy NIP). Manualny toggle dla edge cases / korekty.
- Dual PDF flow (Dompdf lokalny / redirect 302 do Fakturowni) — operator dostaje "natywny" PDF z Fakturowni dla delegowanych (ten sam co ksiegowy widzi w Fakturowni), brak ryzyka rozjazdu wygladu/numeracji.
- `is_delegated=0` config zachowany jako pelny tryb lokalny — dla scenariuszy gdzie operator nie chce/nie ma konta Fakturowni.
**BREAKING:** Brak. `OrdersController` ctor dostal 2 NEW optional params (default null) — backwards compatible.
**Szczegolne uwagi:**
- `invoice-config-form.js` (Phase 114) i `invoice-requested-toggle.js` (Phase 115) — dwa rozne moduly, oba w `layouts/app.php`. Nazwa "invoice-*" ale rozne kontekstowo (jeden formularz config, drugi zamowienie).
- `OrderProAlerts.confirm` w `invoice_form.php` uzywa options-object API zgodnie z memory (Phase 114 fix).
- Migracje no-op nie potrzebne — Phase 113 dostarczyla `orders.invoice_requested`, `invoice_*` tabele i `fakturownia_integration_settings`.
- Skill audit: brak required skills dla tego planu.
---
## 2026-05-10 - Phase 114 Plan 01: Accounting Configs Refactor
**Co zrobiono:**
- Migracja `20260511_000107_seed_default_invoice_config.sql` - idempotentny seed `Domyslny VAT` config (format `FV/%N/%M/%Y`, monthly, lokalna numeracja, payment_to_days=7) przez `NOT EXISTS` guard.
- `InvoiceConfigRepository` - pelne CRUD `invoice_configs` z walidacja serwerowa wszystkich pol + krytyczna regula delegacji: `is_delegated=1` wymaga `integration_id` z `integrations.type='fakturownia'`. `delete()` pre-checkuje `invoices` zeby zwrocic PL komunikat zamiast SQLSTATE.
- `InvoiceConfigController` - index/edit/save/toggle/delete dla `/settings/accounting/invoices`. Flash `accounting.invoices.save/.error`. Edycja na osobnej podstronie.
- `ReceiptConfigController` refactor - rozdzielenie `index()` na `hub()` + `list()` + nowa `edit()`. Save/toggle/delete redirectuja na `/settings/accounting/receipts` (nie hub).
- 4 nowe widoki:
- `accounting.php` (REFAKTOR) - hub-rozdroze z 2 kartami Paragony/Faktury (usunieta lista i formularz inline).
- `accounting-receipts.php` - lista paragonow w stylu spojnym z fakturami (`table.table + badge`).
- `accounting-receipt-edit.php` - formularz paragonu na osobnej podstronie.
- `accounting-invoices.php` - lista faktur (7 kolumn z dodatkami Tryb, Konto Fakturowni).
- `accounting-invoice-edit.php` - formularz faktury z conditional `integration_id` select.
- `invoice-config-form.js` - vanilla JS toggle dla `is_delegated` checkbox -> show/hide `integration_id` select wrapper + dynamiczny `required`.
- `layouts/app.php` - rejestracja nowego modulu JS `invoice-config-form.js` (z cache-busting `?ver=mtime`).
- Routy: 12 nowych endpointow ksiegowosci (6 receipts + 6 invoices) + 3 legacy aliasy starych `/settings/accounting/save|toggle|delete`.
- Docs: `db_schema.md` (notka o seed), `architecture.md` (nowa sekcja "Phase 114"), tech_changelog.
**Dlaczego:**
- Phase 113 dostarczyl tabele `invoice_configs` - bez UI/CRUD nie da sie operacjonalnie wystawiac faktur. Phase 115 (wystawianie faktury z zamowienia) wymaga gotowych invoice_configs.
- Edycja paragonu pod tabela na `/settings/accounting` (stary uklad) miala 2 problemy: dlugi scroll przy wielu configach + brak miejsca na rosnacy formularz faktury (z conditional fields). Refaktor na osobne podstrony rozwiazuje oba.
- Seed `Domyslny VAT` - operator od razu po deployment moze wystawiac faktury bez recznego skonfigurowania (zerowy onboarding).
- Legacy aliasy `/settings/accounting/save|toggle|delete` - istniejace formularze w cache'u przegladarki / bookmarki / zewnetrzne narzedzia (jesli sa) dalej dzialaja bez 404.
- JS toggle - operator nie zaznaczy delegacji bez wybrania konta (UX + serwerowa walidacja jako backup).
**BREAKING:**
- Brak. Stare endpointy zachowane jako aliasy. Stary widok `/settings/accounting` to teraz hub z linkami zamiast listy - operator wie ze trzeba kliknac "Zarzadzaj paragonami".
---
## 2026-05-10 - Phase 113 Plan 01: Fakturownia Integration Foundation
**Co zrobiono:**
- Migracje SQL:
- `20260510_000104_create_invoices_tables.sql` - cztery nowe tabele: `invoice_configs`, `invoices`, `invoice_number_counters`, `fakturownia_integration_settings` (multi-account, `integration_id UNIQUE FK` -> `integrations`).
- `20260510_000105_add_invoice_requested_to_orders.sql` - `orders.invoice_requested TINYINT(1) NOT NULL DEFAULT 0` + index `idx_orders_invoice_requested`.
- `20260510_000106_seed_fakturownia_integration_type.sql` - no-op placeholder dokumentujacy uznanie `integrations.type='fakturownia'` jako oficjalnie wspieranego.
- `FakturowniaIntegrationRepository` - CRUD kont Fakturowni z resolved encryption (`integrations.api_key_encrypted` jako zrodlo prawdy, `settings.api_token_encrypted` jako cache). `findAll/findByIntegrationId/save/delete/getDecryptedToken`.
- `FakturowniaApiClient::testConnection()` - GET `https://{prefix}.fakturownia.pl/account.json?api_token=...` z cURL + `SslCertificateResolver`. `createInvoice`/`downloadPdf` jako STUB-y rzucajace `RuntimeException` (do implementacji w kolejnym planie).
- `IntegrationsRepository::updateTestResult()` - nowa publiczna metoda do zapisu `last_test_status / last_test_http_code / last_test_message / last_test_at`. Wykorzystywana przez `FakturowniaIntegrationController::test()`.
- `FakturowniaIntegrationController` - lista (`/settings/integrations/fakturownia`), edycja (`/edit`, `/new`), save, test, delete. CSRF via `_token`, flash `fakturownia.save/.test/.error`.
- Widoki `resources/views/settings/fakturownia.php` (lista z badge'ami) i `resources/views/settings/fakturownia-edit.php` (form: name, account_prefix, api_token, department_id, default_kind, default_payment_to_days, is_active).
- `IntegrationsHubController::buildFakturowniaRow()` - karta Fakturowni w hubie `/settings/integrations` z agregowanym statusem wszystkich kont.
- Routy w `routes/web.php` (`get /index`, `get /new`, `get /edit`, `post /save`, `post /test`, `post /delete`) + DI wiring (`$fakturowniaIntegrationRepository`, `$fakturowniaApiClient`, `$fakturowniaIntegrationController`).
- Dokumentacja: `db_schema.md` (sekcja "Invoices" + `fakturownia_integration_settings` + kolumna `orders.invoice_requested`, total tables 55 -> 59), `architecture.md` (sekcja "Phase 113").
**Dlaczego:**
- v3.7 Invoices wprowadza wystawianie faktur dla klientow wymagajacych dokumentu z NIP (clarifications: `orders.invoice_requested` z importera + manual override). Bez fundamentu DB i konfiguracji konta Fakturowni zaden kolejny plan v3.7 (CRUD configs, wystawianie, lista) nie ma sensu.
- Multi-account przez `integrations.type='fakturownia'` zachowuje spojnosc z Allegro/shopPRO (rozne instancje) i pozwala na rozne konta Fakturowni dla roznych marek/oddzialow.
- `is_delegated` flag w `invoice_configs` umozliwia w przyszlym planie dwa tryby: lokalna numeracja+PDF dompdf (default) lub delegacja do Fakturowni (numer+PDF z API).
- STUB-y `createInvoice/downloadPdf` celowo rzucaja exception zamiast "TODO" - kazda przedwczesna probaba uzycia rzuci jasny blad zamiast cichego no-op.
- `IntegrationsRepository::updateTestResult()` jest reusable - przyszle integracje (np. kolejne API) beda mogly korzystac z tej samej metody zamiast inline UPDATE.
**BREAKING:**
- Brak zmian breaking. `IntegrationsHubController` ma nowy parametr konstruktora (`FakturowniaIntegrationRepository`) - wszystkie miejsca wywolania zaktualizowane.
---
## 2026-05-07 - Phase 112 Plan 01: Re-import Data Protection
**Co zrobiono:**
- `OrderImportRepository::upsertOrderAggregate` - rozdzielono sciezke `created` (pierwszy import) od `else` (re-import). `replaceAddresses`, `replaceItems`, `replaceNotes`, `replacePayments`, `replaceShipments`, `replaceStatusHistory` wywolywane sa teraz wylacznie przy pierwszym imporcie. Logika `paymentTransition` / `statusOverwriteAllowed` (Phase 111) i preservacja `status_code` (Phase 62) bez zmian.
- `OrderImportRepository::updateOrderDelta()` - nowa prywatna metoda zastepujaca `updateOrder()`. Aktualizuje wylacznie `status_code`, `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Pozostale kolumny zamowienia nie sa nadpisywane przez re-import.
- Propagacja anulowania: gdy `is_canceled_by_buyer=1` ze zrodla LUB zmapowany pull `status_code='anulowane'` (Phase 75/83), wymuszone jest `orders.status_code='anulowane'` niezaleznie od `statusOverwriteAllowed`.
- Identical-payload guard: porownanie znormalizowanego `payload_json` (nowy vs aktualny w DB). Identyczny payload + brak `paymentTransition`/`statusOverwriteAllowed`/`cancelledBySource` -> commit transakcji bez wywolywania UPDATE; `fetched_at` i `updated_at` pozostaja niezmienione.
- `OrderImportRepository::getCurrentOrderState()` - rozszerzenie `getCurrentStatusAndPaymentStatus()` o kolumne `payload_json` (jeden SELECT zamiast dwoch).
- `OrderImportRepository::normalizePayloadJson()` - nowy helper deserializujacy/reserializujacy payload do porownywalnej formy (`JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES`).
**Dlaczego:**
- Bug case #882: po re-imporcie zamowienia produkty w `/orders/{id}` wyswietlaly badge "Brak projektu" mimo wczesniej wygenerowanych projektow PSD. Przyczyna: `replaceItems()` wykonywal DELETE+INSERT na `order_items` przy kazdym re-imporcie, co (a) zerowalo flagi `project_generated`/`project_generated_at` (Phase 97) na default 0/NULL i (b) zmienialo `order_items.id`, co lamie referencje skryptu batch `tools/generowanie/_batch_run.sh` (`UPDATE ... WHERE id IN (...)`).
- Phase 111 dodala emisje `payment.status_changed` przy re-imporcie z tranzycja platnosci, wiec re-import istniejacych zamowien stal sie regularny - problem #882 stal sie regresja systemowa, nie krawedziem.
- Zamowienia (orderPRO jako narzedzie zarzadzania) sa edytowane w aplikacji, nie w zrodle (Allegro/shopPRO). Re-import ze zrodla powinien aktualizowac wylacznie stan platnosci, anulowanie i znaczniki synchronizacji - reszta jest stanem lokalnym.
- Identical-payload guard eliminuje niepotrzebne write'y do binloga/replikacji oraz sztuczne odswiezanie `updated_at` przy cyklicznym imporcie tych samych zamowien.
**BREAKING:**
- Brak zmian breaking dla istniejacego API/UI/automatyzacji. Wewnetrznie usunieto `updateOrder()` i `getCurrentStatusAndPaymentStatus()` (oba private) - referencje zewnetrzne nie istnialy.
- Backfill zamowien z resetowanymi flagami `project_generated` (np. #882) wykonywany recznie przez operatora poza zakresem planu.
## 2026-05-05 - Phase 111 Plan 01: Payment Transition Event
**Co zrobiono:**
- `OrderImportRepository::upsertOrderAggregate` - rozszerzona detekcja `payment_transition`. Teraz porownuje poprzedni `payment_status` z nowym (warunek `0/1 -> 2`) zamiast polegac wylacznie na `status_code='nieoplacone'`. Logika preservacji status_code z Phase 62 (`statusOverwriteAllowed`) zostala wydzielona jako osobna decyzja.
- `OrderImportRepository::getCurrentStatusAndPaymentStatus()` - nowa metoda pomocnicza zastepujaca `getCurrentStatus()`, zwraca i status_code, i payment_status w jednym SELECT.
- `AllegroOrderImportService::importSingleOrder` - dodaje emit `payment.status_changed` gdy `payment_transition && !$wasCreated`.
- `ShopproOrdersSyncService::importOneOrder` - analogiczny emit `payment.status_changed`.
- `bin/backfill_payment_transition_111.php` - jednorazowy CLI dla zamowien z `payment_status=2 && status_code='nieoplacone'` (allegro + shoppro), idempotentny, wzorzec z Phase 98.
**Dlaczego:**
- Zamowienie #864 (Allegro) zaimportowane 10s po zlozeniu, gdy Allegro jeszcze nie potwierdzilo platnosci. Re-import 2 minuty pozniej zaktualizowal payment_status na 2, ale `order.imported` jest gated przez `$wasCreated` (Phase 98), wiec automatyzacja "Zmien status na w realizacji (allegro)" nigdy nie odpalila.
- Allegro nie mial odpowiednika `ShopproPaymentStatusSyncService`, wiec tranzycja platnosci znikala cicho. ShopPRO mial analogiczna luke w `ShopproOrdersSyncService` (flaga `payment_transition` byla wykrywana, ale nie emitowala eventu).
- Regula automatyzacji #7 (`payment.status_changed` -> `update_order_status` na `w_realizacji`) nie ma warunku integration_id, wiec po wyemitowaniu eventu obejmie zarowno Allegro jak i shopPRO.
- Idempotencja zalatwiona przez logike repo: po pierwszej tranzycji DB ma `payment_status=2`, kolejny re-import widzi old=2/new=2 i `payment_transition=false`. Brak duplikatow eventow.
## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
**Co zrobiono:**
- `/statistics/summary` - nowy widok podsumowania w menu `Statystyki -> Podsumowanie`.
- `OrdersStatisticsController::summary()` - buduje miesieczny view-model dla wykresow liczby i wartosci zamowien.
- `OrdersStatisticsRepository::aggregateByMonth()` - agreguje istniejace zamowienia po miesiacu i kanale/integracji.
- `public/assets/js/modules/statistics-summary-charts.js` - renderer dwoch interaktywnych wykresow liniowych oparty o Chart.js 4.4.8 CDN.
- `resources/views/statistics/summary.php` - filtry zgodne z raportem dziennym, dwa wykresy obok siebie na desktopie oraz dwie tabele fallback pod nimi.
- Domyslny poczatek historii ustawiony na `2026-04-01` (`04-2026`) mimo starszych danych.
**Dlaczego:**
- Operator potrzebuje szybkiego trendu miesiecznego przed przejsciem do szczegolowych dziennych statystyk.
- Wykresy uzywaja obecnych tabel `orders`, `integrations`, `order_status_groups` i `order_statuses`, wiec migracja DB nie jest potrzebna.
- Seria `Razem` jest liczona z tych samych danych co serie integracji, co ulatwia sprawdzenie sum miesiecznych.
## 2026-04-28 - Phase 109 Plan 01: Checkbox Multiselect Filters
**Co zrobiono:**
- `public/assets/js/modules/checkbox-multiselect.js` - nowy vanilla JS enhancer dla natywnych `<select multiple data-checkbox-multiselect>`.
- `resources/views/layouts/app.php` - globalne podpiecie modulu z cache busting przez `filemtime()`.
- `resources/views/statistics/orders.php` - filtry `channels[]` i `status_groups[]` oznaczone do progresywnego ulepszenia bez zmiany nazw pol formularza.
- `resources/scss/app.scss` - kompaktowe style dropdownu z checkboxami i opcja "Wszystkie".
**Dlaczego:**
- Natywne selecty multiple byly malo czytelne i zajmowaly za duzo miejsca w filtrach statystyk.
- Zachowanie oryginalnego selecta w DOM utrzymuje obecny kontrakt GET i fallback bez JavaScript.
- Brak zmian w schemacie DB i logice agregacji statystyk.
> Chronologiczny log zmian technicznych — co i dlaczego.
## 2026-04-27 — Phase 108 Plan 02: Automation Dropdowns z DB
**Co zrobiono:**
- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy)
- Dropdown statusów w warunku `shipment_status` i akcji `update_shipment_status` ładuje statusy z DB przez `DeliveryStatus::getAllOptions()`
- Walidacja w `parseConditionValue()` i `parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition()` porównuje klucze bezpośrednio
- `resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target (zamiast pierwszego z grupy)
**Dlaczego:**
- Zamknięcie integracji z Plan 01 — operator dodaje status w `/settings/delivery-statuses` i jest on od razu dostępny w dropdownach automatyzacji bez deploymentu
- Eliminacja kolizji semantycznej: stary klucz grupowy `picked_up` mapował na `delivered` (paczka odebrana przez klienta), nowy klucz DB `picked_up` to "Odebrana przez kuriera" (od nadawcy)
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`, oraz `picked_up`/`ready_for_pickup`/`cancelled` w starym znaczeniu) nie matchują — wymagają ręcznego odtworzenia z nowymi kluczami DB
## 2026-04-27 — Phase 108 Plan 01: Delivery Status Management
**Co zrobiono:**
- Tabela `delivery_statuses` z seedem 11 statusów (migracja `20260427_000103`)
- `DeliveryStatusRepository` — CRUD + per-request cache
- `DeliveryStatus.php` — dynamiczne ładowanie statusów z DB (`setRepository()`)
- Panel `/settings/delivery-statuses` z CRUD (zakładka "Statusy") i mapowaniem (zakładka "Mapowanie dostawy")
- Sidebar: "Statusy" → "Statusy zamówień", nowe "Statusy przesyłek" z badge niezmapowanych
- Badge przesyłek: inline CSS custom property `--status-color` dla niestandardowych statusów
**Dlaczego:**
- Dodanie nowego statusu wymagało zmiany kodu + deploymentu; teraz z UI
- Operator może definiować własne statusy znormalizowane bez ingerencji w kod

View File

@@ -1,114 +1,41 @@
# Testing # Testing
## Setup Last refresh: 2026-05-18.
- **Framework:** PHPUnit 11.5 ## Framework
- **Config:** `phpunit.xml`
- **Bootstrap:** `tests/bootstrap.php` (PSR-4 autoloader for `Tests\` namespace)
- **Run:** `composer test``vendor/bin/phpunit -c phpunit.xml --testdox`
- **Helper:** `dg/bypass-finals` — allows mocking `final` classes
## PHPUnit Configuration - Test framework: PHPUnit 11.5 through `phpunit.xml`.
- Bootstrap: `tests/bootstrap.php`.
- Test namespace: `Tests\\`.
- Test location: `tests/Unit/`.
- `DG\\BypassFinals::enable()` is enabled in `tests/bootstrap.php` for mocking final classes.
- PHPUnit fails on warnings and risky tests.
```xml ## Commands
<phpunit bootstrap="tests/bootstrap.php"
cacheDirectory="storage/cache/phpunit"
colors="true"
executionOrder="depends,defects"
failOnWarning="true"
failOnRisky="true">
```
## Test Files (`tests/Unit/`) - Full PHP suite: `composer test`.
- Direct PHPUnit: `vendor/bin/phpunit -c phpunit.xml --testdox`.
- Frontend assets: `npm run build:assets`.
- `npm test` is a placeholder that exits with an error.
| File | Subject | Coverage | ## Existing Coverage Shape
|------|---------|---------|
| `AllegroOrderImportServiceTest.php` | `AllegroOrderImportService` | Import, retry on 401, empty ID guard |
| `AllegroShipmentServiceTest.php` | `AllegroShipmentService` | Shipment creation |
| `AllegroStatusSyncServiceTest.php` | `AllegroStatusSyncService` | Status sync |
| `AllegroTokenManagerTest.php` | `AllegroTokenManager` | Token refresh |
| `ApaczkaShipmentServiceTest.php` | `ApaczkaShipmentService` | Apaczka API calls |
| `AutomationServiceTest.php` | `AutomationService` | Email-once guard, condition evaluation |
| `DeliveryStatusTest.php` | Delivery status mapping | Status translation |
**No repository, controller, or view tests exist** — only service-layer unit tests. - Services/mappers/repositories: `tests/Unit/ErliOrdersSyncServiceTest.php`, `tests/Unit/ErliOrderMapperTest.php`, `tests/Unit/FakturowniaInvoiceIdempotencyTest.php`, `tests/Unit/OrdersStatisticsRepositoryTest.php`.
- Shipping/integration services: `tests/Unit/AllegroShipmentServiceTest.php`, `tests/Unit/ApaczkaShipmentServiceTest.php`, `tests/Unit/PolkurierShipmentServiceTest.php`.
- Security/config: `tests/Unit/SmtpSecurityContextFactoryTest.php`, `tests/Unit/AllegroTokenManagerTest.php`.
- View-rendering style check: `tests/Unit/ShipmentPreparePolkurierMappingTest.php`.
## Test Patterns ## Gaps
### Mock Setup - No dedicated browser/e2e suite found.
- No frontend JS test suite found for `public/assets/js/modules/*.js`.
- No controller HTTP integration test suite found for `routes/web.php` and controllers.
- No migration integration test suite found for `database/migrations/`.
- Windows print client under `clients/windows/OrderPROPrint/` has no obvious automated tests.
```php ## Practical Verification Guidance
final class AllegroOrderImportServiceTest extends TestCase {
private AllegroIntegrationRepository&MockObject $integrationRepository;
private AllegroApiClient&MockObject $apiClient;
protected function setUp(): void { - For narrow service/repository changes, run the nearest `tests/Unit/*Test.php` first.
$this->apiClient = $this->createMock(AllegroApiClient::class); - For shared behavior, integration wiring, or risky changes, run `composer test`.
$this->service = new AllegroOrderImportService( - For UI/SCSS changes, run `npm run build:assets` and manually verify the affected view.
$this->integrationRepository, $this->tokenManager, $this->apiClient, ... - For cron/integration work, test the target service/repository and smoke the relevant handler in `src/Modules/Cron/`.
);
}
}
```
### Behavior Verification
```php
$this->emailOnceRepository
->expects($this->exactly(2))
->method('wasSent')
->willReturnOnConsecutiveCalls(false, true);
$this->emailService
->expects($this->once())
->method('send');
```
### Exception Testing
```php
$this->expectException(AllegroApiException::class);
$this->expectExceptionMessage('Podaj ID zamowienia');
$this->service->importSingleOrder('');
```
### Retry Logic Testing
```php
$callCount = 0;
$this->apiClient->method('getCheckoutForm')
->willReturnCallback(function () use (&$callCount): array {
if (++$callCount === 1) {
throw new RuntimeException('ALLEGRO_HTTP_401');
}
return $payload;
});
$this->service->importSingleOrder($id);
$this->assertSame(2, $callCount); // retry happened
```
### Test Data Builders
```php
private function buildMinimalPayload(string $id): array {
return [
'id' => $id,
'status' => 'READY_FOR_PROCESSING',
'payment' => ['id' => 'pay-1', 'type' => 'allegro', ...],
// all required nested fields
];
}
```
## What Is Not Tested
- Controllers (no HTTP integration tests)
- Repositories (no DB integration tests — no test database configured)
- Views (no rendering tests)
- Cron handlers
- Migration scripts
## Manual UAT
Phase summaries note manual UAT steps after feature implementation (e.g., Phase 104 — Apaczka weekend delivery tested via UI). No documented UAT scripts or Postman collections.

View File

@@ -1,98 +1,3 @@
# TODO — odlozone zadania techniczne # TODO — odlozone zadania techniczne
> Lista nieformalnych zadan do zrobienia pozniej. Kazdy wpis ma wlasny tag (np. `STAT-NET`) zeby mozna go bylo zlinkowac z komentarzy w kodzie. > Lista nieformalnych zadan do zrobienia pozniej.
## RECEIPT-NET-FIX — `receipts.total_net` powinno byc realnym netto (data: 2026-05-12)
### Status audytu Phase 134 (2026-05-16)
- **Active** - `ReceiptService::issue()` nadal zapisuje `total_net` jako kopie brutto, a `buildItemsSnapshot()` nie zwraca netto. Dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
### Status Phase 135 (2026-05-16)
- **Resolved for new receipts** - `ReceiptService::buildItemsSnapshot()` zwraca `total_net`, a `ReceiptService::issue()` zapisuje realne netto dla nowych paragonow. Historyczne paragony nie sa backfillowane decyzja operatora z planu 135-01.
### Kontekst
- Phase 123-01 — eksport paragonow XLSX z VAT breakdown.
- `ReceiptService::issue()` (linie 81-82) zapisuje `total_net = total_gross` (kopia, nie realne netto). To znany bug, ale nie poprawiany w 123 (poza zakresem).
- Phase 123 fallback dla legacy paragonow musi liczyc `net = brutto/1.23` zamiast brac z `total_net`, bo inaczej VAT = 0.
### Zadania
1. W `ReceiptService::buildItemsSnapshot()` agreguj `total_net` z pozycji (per stawka) — `lineTotal / (1 + vat/100)`.
2. Zwroc oba: `total_net` (suma netto per pozycja) i `total_gross` (suma brutto). Uzyj `total_net` w INSERT zamiast kopii brutto.
3. Po deploy mozna uproscic legacy fallback w `AccountingController::buildVatBreakdown()` zeby brak `vat` -> uzywal `total_net` z bazy.
4. Backfill historycznych paragonow opcjonalny (eksport teraz dziala bez tego).
## INVOICE-IDEMP-115 — idempotencja podwojnego POST do Fakturowni (data: 2026-05-10)
### Status audytu Phase 134 (2026-05-16)
- **Active / needs operator/API decision** - brak lokalnego `pending_external`, idempotency key i lookupu po referencji przed ponownym POST. W Phase 136 najpierw potwierdzic mozliwosci API Fakturowni. Dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
### Status Phase 136 (2026-05-17)
- **Resolved** - delegowane faktury Fakturownia uzywaja stabilnego `oid = orders.internal_order_number`, lokalnego stanu `pending_external`/`failed_retryable`, lookupu `GET /invoices.json?oid=...` przed ponownym POST oraz auto-podpiecia znalezionej faktury po timeoutcie. Fakturownia nie dokumentuje `Idempotency-Key`, wiec deduplikacja opiera sie na `oid`.
### Kontekst
- Phase 115-01 — wystawianie faktury z zamowienia (delegacja Fakturownia).
- Flow: `InvoiceService::issueDelegated()` -> POST do Fakturowni -> on success INSERT do `invoices`.
- Edge case: faktura zostala utworzona w Fakturowni, ale odpowiedz nie dotarla (timeout, network). Operator widzi blad, klika "Wystaw fakture" ponownie -> drugi POST -> Fakturownia tworzy DRUGA fakture.
### Zadania
1. Dorzucic idempotency-key (np. UUID per attempt zachowany w sesji albo w `invoices` ze statusem `pending_external`).
2. Sprawdzic czy Fakturownia API wspiera nag/lowek `Idempotency-Key` lub deduplikacje po referencji.
3. Alternatywa: po bledzie API, przed kolejnym POST, query Fakturowni `GET /invoices.json?q=<order_reference>` zeby sprawdzic czy faktura juz istnieje.
### Status
- Zamkniete w Phase 136 dla nowych delegowanych wystawien. Historyczne duplikaty, jesli istnieja, nie sa backfillowane ani automatycznie usuwane.
---
## STAT-NET — netto zamowien w statystykach (data: 2026-04-19)
### Status audytu Phase 134 (2026-05-16)
- **Active** - `OrdersStatisticsRepository::netAmountSql()` nadal ma fallback `gross / 1.23`; mapper potrafi przyjac czesc pol netto/VAT, ale statystyki nie licza netto po pozycjach. Dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
### Status Phase 135 (2026-05-16)
- **Resolved for runtime statistics** - `OrdersStatisticsRepository::netAmountSql()` preferuje source-level net, a przy braku net liczy fallback z `order_items` po realnym VAT i dolicza dostawe jako 23% VAT. Stale `gross / 1.23` zostaje tylko jako ostatni fallback dla legacy zamowien bez pozycji.
### Kontekst
- Statystyki `/statistics/orders` pokazuja `Netto` per dzien/kanal.
- shopPRO nie wysyla kwoty netto ani na poziomie zamowienia (`orders.total_without_tax`), ani produktow (`order_items.original_price_without_tax` — rowniez puste).
- Allegro: `orders.total_without_tax` rowniez moze byc puste.
- Obecnie dziala fallback: netto = `ROUND(total_with_tax / 1.23, 2)` gdy kolumna netto jest pusta/zerowa. Zaklada 23% VAT dla wszystkich.
### Zadania
1. **Ustalic zrodlo prawdy dla netto**:
- Sprawdzic, czy API shopPRO udostepnia `price_netto` lub `total_netto` (payload zawiera tylko `price_brutto` + `vat`).
- Jesli TAK → rozszerzyc mapping importu (`src/Modules/ShopPro/...`) i backfill migracja dla historycznych rekordow.
- Jesli NIE → liczyc netto deterministycznie z `order_items.original_price_with_tax` i `order_items.tax_rate` (wtedy nie zakladamy sztywno 23%).
2. **Backfill historycznych zamowien** po wdrozeniu zrodla netto (migracja SQL + idempotentny skrypt).
3. **Zastapic fallback /1.23** w `OrdersStatisticsRepository::netAmountSql()`:
- Preferuj `orders.total_without_tax`.
- Jesli brak — `SUM(order_items.original_price_with_tax / (1 + order_items.tax_rate / 100) * order_items.quantity)`.
- Stala 1.23 tylko jako ostateczny fallback przy braku item-levelu.
### Linki w kodzie
- `src/Modules/Statistics/OrdersStatisticsRepository.php` - metoda `netAmountSql()` (komentarz `TODO(STAT-NET)`).
## DELIVERY-STATUS-MGMT — zarzadzanie statusami znormalizowanymi z panelu (data: 2026-04-26)
### Status audytu Phase 134 (2026-05-16)
- **Implemented; operator verification remains** - core DB-driven statusy, CRUD i dropdowny automatyzacji sa wdrozone po Phase 108. Phase 137 powinien zweryfikowac tylko stare reguly automatyzacji/operator migration i zamknac nieaktualny wpis. Dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
### Status Phase 137 (2026-05-17)
- **Closed / verified** - runtime code nie zawiera juz `SHIPMENT_STATUS_OPTIONS` ani `SHIPMENT_STATUS_OPTION_MAP`; `AutomationController` i `AutomationService` uzywaja `DeliveryStatus::getAllStatuses()` / `getAllOptions()`. Read-only check przez `DB_HOST_REMOTE` wykazal 11 statusow w `delivery_statuses`, 3 warunki `shipment_status`, 0 akcji `update_shipment_status`, 0 starych kluczy (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) i 0 kluczy spoza `delivery_statuses`. Dowody: `.paul/phases/137-delivery-status-backlog-verification/137-01-SUMMARY.md`.
### Kontekst
- Aktualnie statusy znormalizowane (`created`, `confirmed`, `picked_up`, `in_transit`, itd.) sa stalymi w kodzie (`DeliveryStatus.php`).
- Dodanie nowego statusu wymaga zmiany kodu + deploymentu.
- Panel ustawien pozwala tylko mapowac surowe statusy kurierow na istniejace statusy znormalizowane - nie mozna dodac nowego znormalizowanego statusu z UI.
### Zadania
1. Wyniesc liste statusow znormalizowanych do tabeli DB (np. `delivery_statuses`) z kolumnami: `key`, `label_pl`, `color`, `sort_order`, `is_terminal`, `is_system`.
2. Statusy systemowe (`delivered`, `returned`, `cancelled`) oznaczac flaga `is_system = true` - nieedytowalne z UI (maja specjalne znaczenie w kodzie).
3. Panel `/settings/delivery-statuses` - CRUD dla statusow niebedacych systemowymi (dodaj, zmien etykiete/kolor, usun jesli nieuzywany).
4. `DeliveryStatus::ALL_STATUSES`, `LABEL_PL` i badge CSS zastapic dynamicznym ladowaniem z DB (cache per-request).
5. Automatyzacje i mapowania kurierow - dropdown statusow znormalizowanych pobierany z DB zamiast hardcoded.
### Uwagi
- `TERMINAL_STATUSES` musi zostac zachowane jako lista systemowych statusow koncowych - te nie powinny byc usuwalne.
- Przy usuwaniu statusu znormalizowanego - blokada jesli uzywany w `delivery_status_mappings` lub `shipment_packages.delivery_status`.

View File

@@ -0,0 +1,113 @@
# Tooling Status
Last refresh: 2026-05-19 Europe/Warsaw.
## Current Policy
- Quality Radar is lightweight by default.
- `codebase-memory-mcp` remains enabled for automatic plan/apply/unify impact scans.
- `jscpd` is disabled by policy for automatic runs; use it only on explicit duplicate-detection requests or if `quality_radar.tools.jscpd: true` is set in `.paul/config.md`.
- `ast-grep` is disabled by policy for automatic runs; use it only on explicit structural-pattern requests or if `quality_radar.tools.ast_grep: true` is set in `.paul/config.md`.
- Existing `jscpd` and `ast-grep` raw reports under `.paul/codebase/radar/` are historical artifacts and should not be loaded into context unless directly needed.
- Generated `.paul/codebase/*.md` map is reduced to: `stack.md`, `architecture.md`, `conventions.md`, `testing.md`, `integrations.md`, `db_schema.md`, `impact_map.md`, `quality_risks.md`, and `tooling_status.md`.
- `.paul/codebase/todo.md` is manual/user-owned and must not be generated or overwritten by PAUL workflows.
- Retired generated docs: `index.md`, `structure.md`, `concerns.md`, `domain_duplicates.md`, and `tech_changelog.md`.
- PAUL-generated Markdown documents should be written in Polish by default; paths, commands, config keys, identifiers, logs, and quoted source text stay unchanged.
## Scan
- Mode: policy update / previous full scan baseline.
- Scope: Quality Radar tooling configuration.
- Status: lightweight automatic radar enabled through `codebase-memory-mcp`; `jscpd` and `ast-grep` manual/on-demand by policy.
## Tools
- `codebase-memory-mcp --version`: ok, `codebase-memory-mcp 0.6.1`.
- `codex mcp list`: `codebase-memory-mcp` enabled with stdio transport.
- Fresh `codex exec` process used MCP tools successfully: `list_projects`, `index_repository`, `list_projects`, `get_architecture`.
- Initial MCP index: project `C-visual studio code-projekty-orderPRO`, 8165 nodes, 13610 edges.
- `codebase-memory-mcp --% cli index_repository {"repo_path":"C:\visual studio code\projekty\orderPRO"}`: exited 0; emitted only init logs in this shell mode.
- `codebase-memory-mcp --% cli index_status {"repo_path":"C:\visual studio code\projekty\orderPRO"}`: exited 0; emitted only init logs in this shell mode.
- `codebase-memory-mcp --% cli get_architecture {"repo_path":"C:\visual studio code\projekty\orderPRO"}`: exited 0; emitted only init logs in this shell mode.
- `jscpd --version`: failed, command not found globally.
- `npx --yes jscpd ...`: succeeded and wrote `.paul/codebase/radar/jscpd/jscpd-report.json`.
- `ast-grep --version`: ok after global install, `ast-grep 0.42.2`.
- `sg --version`: ok after global install, `ast-grep 0.42.2`.
- `npx --yes @ast-grep/cli ...`: failed historically, npm could not determine executable.
- `npx --yes --package @ast-grep/cli ast-grep ...`: failed historically with module resolution error under Node v24.15.0.
- Fix applied: installed `@ast-grep/cli` globally and repaired npm wrapper scripts to call native `ast-grep.exe` / `sg.exe`.
- Fallback: `rg` scans and explorer analysis remain available.
## Raw Outputs
- Codebase memory install/MCP/index note: `.paul/codebase/radar/codebase-memory-full.txt`.
- jscpd JSON report: `.paul/codebase/radar/jscpd/jscpd-report.json`.
- ast-grep refreshed output: `.paul/codebase/radar/ast-grep-full.txt`.
## Commands Attempted
- `codebase-memory-mcp --version`
- `codebase-memory-mcp --help`
- `codex mcp list`
- `codex mcp get codebase-memory-mcp`
- Fresh `codex exec` prompt that called MCP tools: `list_projects`, `index_repository`, `get_architecture`
- `codebase-memory-mcp --% cli index_repository {"repo_path":"C:\visual studio code\projekty\orderPRO"}`
- `codebase-memory-mcp --% cli index_status {"repo_path":"C:\visual studio code\projekty\orderPRO"}`
- `codebase-memory-mcp --% cli get_architecture {"repo_path":"C:\visual studio code\projekty\orderPRO"}`
- `jscpd --version`
- `ast-grep --version`
- `sg --version`
- `npx --yes jscpd . --reporters json,console --output .paul/codebase/radar/jscpd --threshold 100 --ignore "**/.git/**,**/node_modules/**,**/vendor/**,**/storage/**,**/.paul/**"`
- `npx --yes @ast-grep/cli -p ...`
- `npx --yes --package @ast-grep/cli ast-grep -p ...`
## Next Action
- Treat automatic radar as usable through `codebase-memory-mcp`.
- Do not run `jscpd` or `ast-grep` during routine PLAN/APPLY/UNIFY unless explicitly requested or enabled in `.paul/config.md`.
- `ast-grep` was previously working globally, while `jscpd` relied on `npx`; keep those details only as historical setup notes.
- If `npm install -g @ast-grep/cli` is rerun, verify the generated Windows wrappers still call the native `.exe` binaries.
- Use MCP tools through a fresh Codex session/process for structured codebase-memory output.
- Future jscpd runs should also ignore `.playwright-mcp/**`, `.scannerwork/**`, `.vscode/ftp-kr.diff.*`, generated assets, and cache folders.
## Targeted Plan Scan: Polish UI Copy
- Timestamp: 2026-05-18 23:05 Europe/Warsaw.
- Mode: plan.
- Scope: Polish translations/UI copy in `resources/lang`, `resources/views`, `public/assets/js/modules`, and `src/Modules`.
- Status: degraded, because `codebase-memory-mcp` and `npx jscpd` worked, while `ast-grep`/`sg` remain unavailable.
- Commands:
- `codebase-memory-mcp --version` -> ok, 0.6.1.
- MCP `list_projects`, `search_graph`, `get_code_snippet`, `search_code` -> ok.
- `jscpd --version` -> unavailable globally.
- `npx --yes jscpd resources/lang resources/views public/assets/js/modules src/Modules --reporters json,console --output .paul/codebase/radar/jscpd-i18n-plan --threshold 100 ...` -> ok.
- `ast-grep --version`, `sg --version` -> unavailable.
- Raw output: `.paul/codebase/radar/jscpd-i18n-plan/jscpd-report.json`.
## Targeted Apply Scan: Polish UI Copy
- Timestamp: 2026-05-18 23:25 Europe/Warsaw.
- Mode: apply.
- Scope: Polish translations/UI copy in `resources/lang`, `resources/views`, `public/assets/js/modules`, and `src/Modules`.
- Status: degraded, because `codebase-memory-mcp` and `npx jscpd` worked, while `ast-grep`/`sg` and `sonar-scanner` are unavailable in PATH.
- Commands:
- MCP `detect_changes` from `HEAD` for the affected scope -> ok, 111 changed files, no impacted graph symbols reported.
- `npx --yes jscpd resources/lang resources/views public/assets/js/modules src/Modules --reporters json,console --output .paul/codebase/radar/jscpd-i18n-post-apply --threshold 100 ...` -> ok.
- `sonar-scanner --version` -> failed, command not found.
- Raw output: `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`.
## Targeted UNIFY Scan: Polish UI Copy
- Timestamp: 2026-05-18 23:40 Europe/Warsaw.
- Mode: post-apply / unify.
- Scope: plan path plus modified UI copy, docs and radar files.
- Status: degraded, because `codebase-memory-mcp` and `npx jscpd` worked, while global `jscpd`, `ast-grep`, `sg` and `sonar-scanner` are unavailable in PATH.
- Commands:
- `codebase-memory-mcp --version` -> ok, 0.6.1.
- MCP `detect_changes` from `HEAD` for the affected scope -> ok, 110 changed files, no impacted graph symbols reported.
- `jscpd --version` -> failed, command not found globally.
- `ast-grep --version`, `sg --version` -> failed, command not found.
- `npx --yes jscpd resources/lang resources/views public/assets/js/modules src/Modules --reporters json,console --output .paul/codebase/radar/jscpd-i18n-post-apply --threshold 100 ...` -> ok.
- Raw outputs:
- `.paul/codebase/radar/codebase-memory-post-apply-polish-ui-copy.txt`
- `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`

47
.paul/config.md Normal file
View File

@@ -0,0 +1,47 @@
# Project Config
**Project:** orderPRO
**Updated:** 2026-05-19
## Project Settings
```yaml
project:
name: orderPRO
```
## Integrations
### Quality Radar
```yaml
quality_radar:
enabled: true
auto_install: true
tools:
codebase_memory_mcp: true
jscpd: false
ast_grep: false
```
### SonarQube
```yaml
sonarqube:
enabled: false
mode: manual-on-demand
project_key: orderPRO
```
## Preferences
```yaml
preferences:
auto_commit: false
verbose_output: false
state_autocompress: true
state_autocompress_max_lines: 500
```
---
*Config updated: 2026-05-19*

View File

@@ -1,76 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-13
**Status:** paused
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** orderPRO — aplikacja do zarządzania zamówieniami z wielu kanałów sprzedaży (Allegro, Erli, własne sklepy). Generowanie etykiet kurierskich.
**Core value:** Sprzedawca obsługuje wszystkie kanały i nadaje przesyłki bez przełączania platform.
---
## Current State
**Version:** v0.1.0 (In Progress)
**Phase:** 6 of TBD — 06-sonarqube-quality
**Plan:** 06-01 ✓ DONE, 06-02 ✓ DONE, 06-03 ✓ DONE, 06-04/05/06 awaiting
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [loop closed — ready for next plan: 06-06]
```
---
## What Was Done (this session)
- **APPLY + UNIFY 06-02** — S1142: redukcja return statements (commit 028c46c)
- AllegroIntegrationController: save 6→3, saveImportSettings 5→3, oauthCallback 4→3
- ShopproIntegrationsController: save 9→3, saveStatusMappings 4→3, syncStatuses 4→3
- Nowe wzorce: `validateXxxInput(): ?string` i `validateXxxAccess(): ?Response`
- Phase 6 progress: 3/6 plans complete (50%)
---
## What's In Progress
Nic — pętla 06-02 zamknięta, codebase stabilny.
---
## What's Next
**Immediate:** `/paul:apply .paul/phases/06-sonarqube-quality/06-06-PLAN.md`
**Kolejność pozostałych planów:** 06-06 → 06-04 → 06-05
- 06-05 (god classes) zależy od 06-04 i ma `checkpoint:human-verify` (nie autonomous)
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview |
| `.paul/phases/06-sonarqube-quality/06-06-PLAN.md` | Następny plan |
| `.paul/phases/06-sonarqube-quality/06-02-SUMMARY.md` | Ostatni UNIFY — context |
| `src/Modules/Settings/AllegroIntegrationController.php` | Zrefaktoryzowany w 06-02 |
| `src/Modules/Settings/ShopproIntegrationsController.php` | Zrefaktoryzowany w 06-02 |
---
## Resume Instructions
1. Przeczytaj `.paul/STATE.md` — potwierdź pozycję w loop
2. Uruchom `/paul:apply .paul/phases/06-sonarqube-quality/06-06-PLAN.md`
---
*Handoff created: 2026-03-13*

View File

@@ -1,74 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-13
**Status:** paused — session complete, plans ready for execution
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** orderPRO — aplikacja do zarządzania zamówieniami z wielu źródeł sprzedaży (Allegro, Erli, shopPRO) z generowaniem etykiet przewozowych.
**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 State
**Milestone:** v0.2 Pre-Expansion Fixes
**Phase:** 7 — pre-expansion-fixes
**Plan:** 07-01..07-05 CREATED, żaden nie wykonany
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ○ ○ [Plan gotowy — czeka na APPLY]
```
---
## What Was Done (ta sesja)
- UNIFY 06-05: god classes split — ShopproOrdersSyncService 39→9 metod, AllegroIntegrationController 35→25 metod
- Faza 06 zamknięta: 6/6 planów, SonarQube quality baseline
- /paul:complete-milestone: v0.1 Initial Release zamknięty, git tag v0.1.0
- CONCERNS.md skategoryzowany — "przed rozbudową" vs "odroczić"
- Faza 07 zaplanowana: 5 planów (07-01..07-05) gotowe do APPLY
---
## What's Next
**Immediate:** `/paul:apply .paul/phases/07-pre-expansion-fixes/07-01-PLAN.md`
Kolejność:
1. 07-01 — autonomiczny (Performance: N+1, static cache, DB indexes)
2. 07-02 — autonomiczny (SSL verify, cron→DB, migration 000014b)
3. 07-03 — ma checkpoint:human-verify (UX: disable orderpro_to_allegro, UI items 14-17)
4. 07-04 — autonomiczny (Tests: AllegroTokenManager + import)
5. 07-05 — ma checkpoint:decision (InPost ShipmentProviderInterface)
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/phases/07-pre-expansion-fixes/07-01-PLAN.md` | Performance fixes |
| `.paul/phases/07-pre-expansion-fixes/07-02-PLAN.md` | SSL + cron + migration |
| `.paul/phases/07-pre-expansion-fixes/07-03-PLAN.md` | UX fixes (checkpoint) |
| `.paul/phases/07-pre-expansion-fixes/07-04-PLAN.md` | Unit tests |
| `.paul/phases/07-pre-expansion-fixes/07-05-PLAN.md` | InPost provider (checkpoint:decision) |
---
## Resume Instructions
1. `/paul:resume` — odczyta STATE.md i pokaże aktualny stan
2. Zatwierdź: `/paul:apply .paul/phases/07-pre-expansion-fixes/07-01-PLAN.md`
---
*Handoff created: 2026-03-13*

View File

@@ -1,91 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-16
**Status:** paused — checkpoint human-verify in progress
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** orderPRO — aplikacja do zarządzania zamówieniami z wielu platform
**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 State
**Milestone:** v0.4 Moduł E-mail
**Phase:** 14 of 3 (in milestone) — Szablony wiadomości e-mail
**Plan:** 14-01 — APPLY in progress (checkpoint human-verify)
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [APPLY in progress — Task 1+2 done, Task 3 checkpoint awaiting approval]
```
---
## What Was Done
- Task 1: Created `EmailTemplateRepository` + `EmailTemplateController` + routes (6 endpoints)
- Task 2: Created view `email-templates.php` with Quill.js CDN editor, variable panel, preview modal, AJAX toggle
- Added sidebar link "Szablony e-mail" in `layouts/app.php`
- Compiled SCSS (modal-overlay, email-tpl-editor styles)
- Fixed 2 bugs discovered during deploy:
- `AuthService` namespace: `App\Core\Auth\AuthService``App\Modules\Auth\AuthService`
- `Flash` namespace: `App\Core\Session\Flash``App\Core\Support\Flash`
---
## What's In Progress
- **Task 3 checkpoint:human-verify** — user was testing the deployed page when session paused
- User reported 2 namespace errors which were fixed, needs to re-test
---
## What's Next
**Immediate:** User re-tests `/settings/email-templates` after namespace fixes. If approved → APPLY complete.
**After that:** Run `sonar-scanner` (required skill), then `/paul:unify .paul/phases/14-email-templates/14-01-PLAN.md`
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview |
| `.paul/phases/14-email-templates/14-01-PLAN.md` | Current plan |
| `src/Modules/Settings/EmailTemplateController.php` | Controller (CRUD + preview + variables) |
| `src/Modules/Settings/EmailTemplateRepository.php` | Repository (DB operations) |
| `resources/views/settings/email-templates.php` | View (list + form + Quill.js + variable panel) |
| `routes/web.php` | Routes (6 new endpoints) |
| `resources/views/layouts/app.php` | Sidebar link added |
| `resources/scss/app.scss` | Styles (modal-overlay, email-tpl-editor) |
---
## Namespace Fixes Applied
These were wrong in the initial controller and have been corrected:
- `use App\Core\Auth\AuthService``use App\Modules\Auth\AuthService`
- `use App\Core\Session\Flash``use App\Core\Support\Flash`
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. Check loop position — APPLY in progress, Task 3 checkpoint
3. Run `/paul:resume` or ask user to re-test `/settings/email-templates`
4. On approval → finalize APPLY → sonar-scanner → `/paul:unify`
---
*Handoff created: 2026-03-16*

View File

@@ -1,111 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-22
**Status:** paused — sesja w toku, checkpoint human-verify
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** orderPRO — aplikacja do zarządzania zamówieniami wielokanałowymi
**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 State
**Milestone:** v0.7 Zdalne drukowanie etykiet
**Phase:** 19 of 20 — UI Integration
**Plan:** 19-01 — APPLY w toku (checkpoint human-verify)
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [APPLY in progress — checkpoint verification]
```
---
## What Was Done
- Przycisk "Drukuj" w widoku przesyłki (prepare.php) i szczegółów zamówienia (show.php)
- Bulk endpoint POST /api/print/jobs/bulk (obsługuje package_ids i order_ids)
- Zbiorcze drukowanie z listy zamówień (checkboxy + "Drukuj etykiety" header action)
- Kolejka wydruku w Ustawienia > Drukowanie (lista zleceń z filtrami statusu + retry)
- Ochrona przed duplikatami (findPendingByPackageId)
- Auto-download etykiety przez ensureLabel() z ShipmentProviderRegistry
- Stan "W kolejce" (czerwony, disabled) od razu gdy pending job istnieje
- Sprawdzanie istnienia pliku etykiety na dysku (show.php + prepare.php) — ukrywa "Pobierz"/"Drukuj" gdy plik nie istnieje
- Redirect po utworzeniu przesyłki → szczegóły zamówienia z tabem "Przesyłki"
- Zapamiętywanie aktywnego taba w localStorage
- Apaczka zapisuje nazwę usługi w carrier_id (np. "Orlen Paczka")
- Kolumna "Przewoznik" pokazuje "Apaczka → Orlen Paczka"
- Usunięto "(WZA)" z tytułu sekcji
- Sekcja "Utworzone przesylki" przeniesiona pod formularz nowej przesyłki
- Naprawiony bug use statement w ApaczkaApiClient.php (brak backslashy)
- Wyłączono hook PreToolUse context-mode w settings.json
---
## What's In Progress
- Checkpoint human-verify — użytkownik testuje UI na produkcji (orderpro.projectpro.pl)
- Ostatni feedback: nazwa usługi w kolumnie Przewoznik działa dla nowych przesyłek (stare mają puste carrier_id)
---
## What's Next
**Immediate:** Kontynuacja checkpoint human-verify — użytkownik potwierdza lub zgłasza kolejne uwagi
**After that:**
1. Po "approved" → finalize APPLY
2. /paul:unify dla 19-01
3. sonar-scanner przed UNIFY (wymagane przez SPECIAL-FLOWS.md)
---
## Dodatkowe zmiany (poza planem 19-01)
- show.php: zapamiętywanie taba w localStorage
- show.php: sprawdzanie file_exists dla label_path
- show.php: przycisk "Drukuj" + JS handler
- show.php: kolumna Przewoznik z provider → carrier_id
- ShipmentController: redirect po sukcesie → /orders/{id} z flash
- ApaczkaShipmentService: carrier_id = service name
- ApaczkaApiClient: fix use statement
- OrdersController: storagePath + printJobRepo injection
- settings.json: wyłączony PreToolUse hook context-mode
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview |
| `.paul/phases/19-ui-integration/19-01-PLAN.md` | Current plan |
| `src/Modules/Printing/PrintApiController.php` | REST API + ensureLabel + bulkCreateJobs |
| `src/Modules/Printing/PrintJobRepository.php` | DB operations + pendingPackageIds |
| `src/Modules/Settings/PrintSettingsController.php` | Kolejka wydruku w ustawieniach |
| `resources/views/orders/show.php` | Przycisk Drukuj + tab persistence |
| `resources/views/shipments/prepare.php` | Przycisk Drukuj + sekcja przesyłek |
| `resources/views/orders/list.php` | Bulk print action |
| `resources/views/settings/printing.php` | Kolejka wydruku UI |
| `routes/web.php` | DI + nowe route'y |
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. Check loop position — APPLY in progress
3. Run `/paul:resume` or `/paul:progress`
4. Kontynuuj checkpoint human-verify z użytkownikiem
---
*Handoff created: 2026-03-22*

View File

@@ -1,125 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-23
**Status:** paused — mid-APPLY Phase 28
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** orderPRO — wielokanałowe zarządzanie zamówieniami i przesyłkami
**Core value:** Sprzedawca obsługuje zamówienia ze wszystkich kanałów i nadaje przesyłki bez przełączania się między platformami.
---
## Current State
**Milestone:** v1.2 Śledzenie przesyłek
**Phase:** [2] of [2] — Shipment Tracking UI + Settings
**Plan:** 28-01 — APPLY in progress (Task 2 checkpoint pending, Task 3 not started)
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [APPLY mid-execution, checkpoint pending]
```
---
## What Was Done This Session
### Phase 27 — Shipment Tracking Backend (COMPLETE ✓)
- Migracja DB: 3 kolumny (delivery_status, delivery_status_raw, delivery_status_updated_at) + indeks
- DeliveryStatus class z mapowaniami statusów (30+ InPost, 11 Apaczka, 7 Allegro)
- ShipmentTrackingInterface + 3 implementacje (InpostTrackingService, ApaczkaTrackingService, AllegroTrackingService)
- ShipmentTrackingRegistry + ShipmentTrackingHandler cron (15 min interwał)
- CronHandlerFactory rozszerzony o shipment_tracking_sync
- Commit: `228c0e9`
### Phase 28 — Shipment Tracking UI (IN PROGRESS)
- Task 1 DONE: SCSS badge'e statusów + DeliveryStatus::trackingUrl() z carrier detection
- Task 2 PARTIALLY DONE: Badge'e w show.php i prepare.php, link śledzenia, boks Płatność i wysyłka
- Task 3 NOT STARTED: Ustawienia interwału crona
### Dodatkowe poprawki poza planem:
- **Fix: Przycisk Pobierz etykietę w show.php** — zmieniony z linku do prepare na formularz POST z bezpośrednim downloadem PDF
- **Fix: delivery_status "delivered" → "confirmed"** — migracja błędnie ustawiała label_ready jako doręczona; naprawiono w DB (3 rows) i w pliku migracji
- **Fix: carrier_id dla Apaczka** — uzupełniono z tabeli carrier_delivery_method_mappings (13 rows); dodano fallback w ApaczkaShipmentService
- **Fix: Orlen Paczka URL** — poprawiony na `www.orlenpaczka.pl/sledz-paczke/?numer=`
- **Fix: Google fallback** — gdy carrier nieznany, link śledzenia kieruje do Google search
- **Nowa metoda: ShipmentPackageRepository::resolveCarrierName()** — lookup carrier z carrier_delivery_method_mappings
---
## What's In Progress
- **Checkpoint Task 2** — user testuje UI badge'ów i linków śledzenia, jeszcze nie zatwierdził "approved"
- **Task 3** — ustawienia interwału trackingu w cronie — nie rozpoczęty
---
## What's Next
**Immediate:** Uzyskać checkpoint approval dla Task 2 (UI badge'y), potem wykonać Task 3 (cron interval settings)
**After that:**
1. Sonar scan + UNIFY Phase 28
2. PLAN Phase 29 — UI zarządzania mapowaniem statusów (user request)
3. Dodać fazę 29 do ROADMAP
---
## Decisions Made
| Decision | Rationale |
|----------|-----------|
| Google search jako fallback tracking URL | Gdy carrier_id nieznany — uniwersalne, zawsze działa |
| carrier_delivery_method_mappings jako źródło carrier_id | API Apaczki nie zwraca usług; tabela mapowań jest konfigurowana przez usera |
| Usunięto pattern matching po numerze śledzenia | Zawodne — 13-cyfrowy numer może być DPD, Orlen lub inny |
| Przycisk Pobierz w show.php zmieniony na POST form | Pre-existing bug — link do prepare zamiast bezpośredniego downloadu |
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview |
| `.paul/phases/28-shipment-tracking-ui/28-01-PLAN.md` | Current plan |
| `.paul/phases/27-shipment-tracking-backend/27-01-SUMMARY.md` | Phase 27 summary |
| `src/Modules/Shipments/DeliveryStatus.php` | Status mapping + trackingUrl() |
| `resources/views/orders/show.php` | Badge'e + link śledzenia w zamówieniu |
| `resources/views/shipments/prepare.php` | Badge'e + link śledzenia w przygotowaniu |
| `DOCS/SHIPMENT_TRACKING_STATUSES.md` | Dokumentacja statusów API |
---
## Modified Files (uncommitted)
- `src/Modules/Shipments/DeliveryStatus.php` — trackingUrl() z carrier detection + Google fallback
- `src/Modules/Shipments/ShipmentPackageRepository.php` — resolveCarrierName()
- `src/Modules/Shipments/ApaczkaShipmentService.php` — fallback carrier_id z mappings
- `resources/views/orders/show.php` — kolumna Status dostawy, badge, link, fix Pobierz
- `resources/views/shipments/prepare.php` — kolumna Status dostawy, badge, link
- `resources/scss/modules/_delivery-status.scss` — style badge'ów
- `resources/scss/app.scss`@use delivery-status
- `public/assets/css/app.css` — rebuilt
- `database/migrations/20260323_000060_*` — fix initial status values
---
## Resume Instructions
1. Read `.paul/STATE.md` for latest position
2. Check this handoff file
3. Run `/paul:resume` or continue APPLY manually:
- Get checkpoint approval for Task 2 (badge'e UI)
- Execute Task 3 (cron interval settings in settings/cron.php)
- Then sonar + `/paul:unify`
---
*Handoff created: 2026-03-23*

View File

@@ -0,0 +1,253 @@
---
plan_id: 20260518-2305-polskie-tlumaczenia
title: Poprawne polskie tlumaczenia UI
storage: plan-first
legacy_phase: null
created: 2026-05-18T23:05:52+02:00
status: unified
type: execute
autonomous: true
delegation: auto
files_modified:
- resources/lang/pl.php
- resources/views/
- public/assets/js/modules/
- src/Modules/
- DOCS/ARCHITECTURE.md
- DOCS/TECH_CHANGELOG.md
- .paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md
quality_radar: degraded
---
<objective>
## Goal
Wdrozyc poprawne polskie znaki w tekstach interfejsu orderPRO, np. `Zamowienia` -> `Zamówienia`, bez zmiany logiki biznesowej.
## Purpose
Interfejs ma wygladac profesjonalnie i naturalnie po polsku. Obecnie duza czesc etykiet, komunikatow i opisow jest zapisana bez polskich znakow albo w niejednolitym kodowaniu.
## Output
- Skorygowany glowny slownik `resources/lang/pl.php`.
- Skorygowane najbardziej widoczne hardcoded teksty w widokach, modulach JS i komunikatach kontrolerow.
- Zaktualizowane `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md`.
- Raport/summary w `.paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md` po APPLY.
</objective>
<context>
## Project Docs
@AGENTS.md
@DOCS/ARCHITECTURE.md
@DOCS/DB_SCHEMA.md
@DOCS/TECH_CHANGELOG.md
@.paul/PROJECT.md
@.paul/STATE.md
@.paul/codebase/architecture.md
@.paul/codebase/impact_map.md
@.paul/codebase/quality_risks.md
@.paul/codebase/tooling_status.md
## Source Files
@resources/lang/pl.php
@src/Core/I18n/Translator.php
@src/Core/View/Template.php
@resources/views/layouts/app.php
@resources/views/auth/login.php
@resources/views/orders/list.php
@resources/views/orders/show.php
@resources/views/orders/partials/preview-content.php
@resources/views/orders/partials/email-send-modal.php
@resources/views/shipments/prepare.php
@resources/views/settings/*.php
@resources/views/accounting/*.php
@resources/views/automation/*.php
@public/assets/js/modules/*.js
@src/Modules/*/*Controller.php
@src/Modules/*/*Service.php
</context>
<clarifications>
- No clarifications needed.
- Zakres przyjety: poprawiamy teksty widoczne dla operatora w UI, flashach i komunikatach JS/PHP.
- Poza zakresem sa wartosci techniczne, klucze statusow, nazwy marek/providera (`orderPRO`, `shopPRO`, `SMSPLANET`, `Polkurier`, API keys), raw payloady i dane importowane z marketplace.
</clarifications>
<impact_scan>
## Quality Radar
**Status:** degraded
**Tools:** codebase-memory-mcp ok, jscpd ok przez `npx`, ast-grep degraded/unavailable
## Commands / Sources
- MCP: `list_projects`, `search_graph` dla `Translator`, `get_code_snippet` dla `src/Core/I18n/Translator.php`, `search_code` targeted.
- Fallback `rg`: wyszukiwanie tekstow bez polskich znakow oraz uzyc `$t()`/`Translator`.
- `npx --yes jscpd resources/lang resources/views public/assets/js/modules src/Modules --reporters json,console --output .paul/codebase/radar/jscpd-i18n-plan --threshold 100 ...`
## Affected Areas
- Translation layer: `resources/lang/pl.php`, `src/Core/I18n/Translator.php`, `src/Core/View/Template.php`.
- Views: `resources/views/orders/`, `resources/views/settings/`, `resources/views/accounting/`, `resources/views/automation/`, `resources/views/layouts/`.
- Frontend modules: `public/assets/js/modules/confirm-delete.js`, `order-notes.js`, `inline-status-change.js`, `invoice-requested-toggle.js`, `automation-form.js`, `sms-template-picker.js`, `email`/preview related modules where present.
- Backend operator messages: selected controllers/services under `src/Modules/*` where hardcoded Polish UI/flash/error strings are returned to the operator.
- Docs only: `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`; no DB schema change expected.
## Duplicate / Hardcoded Risks
- `resources/lang/pl.php` already centralizes many labels, but hardcoded UI text remains in views/JS/controllers. Handle by correcting visible hardcoded strings where small and low-risk; defer broad i18n refactor.
- jscpd targeted scan found existing view/controller duplication, especially settings/accounting forms and integration pages. Do not refactor duplicated markup in this plan; only adjust display text.
- Native prompt risks from `.paul/codebase/quality_risks.md` remain relevant but are not part of this translation-only work unless a touched string is inside an existing prompt.
## Encoding Risks
- Some files already contain proper UTF-8 Polish characters, while many strings are ASCII-transliterated. Preserve UTF-8 encoding and avoid introducing mojibake.
- Do not apply blind global replacements to source code identifiers, array keys, routes, CSS classes, status codes, provider codes or external API payload keys.
## Explicit Deferrals
- Full i18n architecture cleanup and migration of every hardcoded string to `resources/lang/pl.php` is deferred; this plan focuses on correcting Polish copy without broad structural churn.
- Database content cleanup is deferred. Existing DB rows, imported order data, template records and user-created values are not modified.
- Native `alert()`/`confirm()` cleanup is deferred except for text corrections in already-touched files.
</impact_scan>
<skills>
- Superseded after UNIFY: orderPRO now treats SonarQube / `sonar-scanner` as manual on-demand only, not as an automatic APPLY/UNIFY requirement.
</skills>
<acceptance_criteria>
## AC-1: Navigation And Core Labels Use Polish Characters
```gherkin
Given the operator opens the authenticated layout
When they view the sidebar, topbar and main order/statistics/settings pages
Then common labels such as "Zamówienia", "Użytkownicy", "Księgowość", "Przesyłki", "Płatności" and "Źródło" display with Polish diacritics
```
## AC-2: Translation File Is UTF-8 And Semantically Correct
```gherkin
Given resources/lang/pl.php is loaded by Translator
When PHP parses the file
Then it returns a valid array and corrected Polish strings without mojibake sequences like "zamĂ" or ASCII-only words like "Zamowienia" in user-facing values
```
## AC-3: Visible Hardcoded UI Copy Is Corrected
```gherkin
Given the operator uses orders, shipments, settings, accounting, automation and notification surfaces
When visible hardcoded PHP/JS messages are rendered
Then corrected Polish copy is used without changing routes, keys, form names, CSS classes or JS behavior
```
## AC-4: External And Technical Contracts Are Preserved
```gherkin
Given provider/API code, status codes, route paths and database keys exist
When translations are corrected
Then technical identifiers such as source keys, provider codes, route names, config keys and payload field names remain unchanged
```
## AC-5: Documentation And Verification Are Complete
```gherkin
Given the implementation is finished
When verification runs
Then PHP lint passes for touched PHP files, relevant asset/build checks are attempted, docs are updated, and any unavailable tools are recorded
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Build a targeted translation audit list</name>
<files>resources/lang/pl.php, resources/views/**/*.php, public/assets/js/modules/*.js, src/Modules/**/*.php</files>
<action>
Use targeted searches for common ASCII Polish words (`Zamow`, `Przesyl`, `platnos`, `zrodlo`, `uzytk`, `usun`, `blad`, `wysyl`, `ksiegow`, `dostep`, etc.) and mojibake markers (`Ă`, `Ĺ`, `—`, `â†`) in user-facing strings. Classify matches into: safe UI copy, technical identifiers to preserve, external/provider text to preserve, and uncertain cases.
</action>
<verify>Review the match list before editing; confirm no routes/keys/classes/status codes are queued for mutation.</verify>
<done>Supports AC-2, AC-3 and AC-4.</done>
</task>
<task type="auto">
<name>Task 2: Correct the central Polish translation file</name>
<files>resources/lang/pl.php</files>
<action>
Replace ASCII-transliterated and mojibake user-facing values with proper UTF-8 Polish text. Keep array keys, placeholder tokens (`:name`, `:count`) and product/provider names unchanged. Prefer natural Polish copy, not mechanical character replacement, especially for messages and descriptions.
</action>
<verify>C:\xampp\php\php.exe -l resources/lang/pl.php; run a PHP one-liner requiring the file and counting non-array/parse failures if useful.</verify>
<done>AC-1 and AC-2 pass for dictionary-backed UI.</done>
</task>
<task type="auto">
<name>Task 3: Correct visible hardcoded PHP view text</name>
<files>resources/views/**/*.php</files>
<action>
Correct user-facing text embedded directly in views, especially orders, shipments, settings, accounting, automation, layouts and partials. Do not move large blocks to the translator unless the surrounding file already uses `$t()` and the change is small. Preserve `e()` escaping, CSRF fields, form names, data attributes and component contracts.
</action>
<verify>C:\xampp\php\php.exe -l for every touched PHP view file; targeted `rg` check for remaining high-signal ASCII forms in touched files.</verify>
<done>AC-1, AC-3 and AC-4 pass for PHP-rendered UI.</done>
</task>
<task type="auto">
<name>Task 4: Correct visible JS and backend operator messages</name>
<files>public/assets/js/modules/*.js, src/Modules/**/*.php</files>
<action>
Correct user-facing JS alert/confirm/status text and backend flash/API messages returned to UI. Avoid changing exception class names, log/event type identifiers, API payload keys, database codes or source/status mapping keys.
</action>
<verify>C:\xampp\php\php.exe -l for touched PHP classes; if asset scripts are changed and package scripts exist, run the project asset build/check command. Otherwise run targeted `rg` and manual code review.</verify>
<done>AC-3 and AC-4 pass for JS/backend messages.</done>
</task>
<task type="auto">
<name>Task 5: Update technical documentation</name>
<files>DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, .paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md</files>
<action>
Document that the Polish UI copy was normalized to UTF-8 diacritics, note the affected layers, and state that no DB/schema/API contract changed. Do not modify `DOCS/DB_SCHEMA.md` except to explicitly skip it in the summary because there is no schema change.
</action>
<verify>Docs mention changed translation/UI behavior and no migration requirement.</verify>
<done>AC-5 passes.</done>
</task>
<task type="auto">
<name>Task 6: Final verification and residual scan</name>
<files>resources/lang/pl.php, touched PHP/JS/docs files</files>
<action>
Run lint on touched PHP files, targeted residual `rg` for common unaccented Polish words and mojibake markers, and `git diff --check`. Manually inspect the most visible pages if local app is available: `/orders/list`, `/orders/{id}`, `/settings/integrations`, `/settings/accounting`, `/settings/email-mailboxes`, `/statistics/orders`.
</action>
<verify>C:\xampp\php\php.exe -l touched PHP files; `git diff --check`; residual `rg`; SonarQube only on explicit operator request.</verify>
<done>All AC pass or gaps are explicitly documented in SUMMARY.md.</done>
</task>
</tasks>
<boundaries>
## Do Not Change
- No database migrations or data backfills.
- No changes to route paths, array keys, config keys, env names, status codes, provider codes, CSS class names, JS selectors, API payload fields or marketplace raw data.
- Do not rewrite product/order/customer data stored in DB.
- Do not introduce `DB_HOST_REMOTE` into runtime config.
- Do not add native `alert()`/`confirm()` usage.
## Scope Limits
- This is a copy/translation normalization plan, not a broad i18n refactor.
- Brand and product names stay as written: `orderPRO`, `shopPRO`, `SMSPLANET`, `HostedSMS`, `Fakturownia`, `Polkurier`, `InPost`, `Apaczka`, `Erli`, `Allegro`.
- Technical English labels may remain when they are intentional (`API Key`, `Base URL`, `OAuth`, `HTTP`, `JSON`, `SKU`, `EAN`).
</boundaries>
<verification>
- [ ] `C:\xampp\php\php.exe -l resources/lang/pl.php`
- [ ] `C:\xampp\php\php.exe -l` for every touched PHP file
- [ ] Targeted residual `rg` for mojibake markers and common ASCII Polish words in touched user-facing files
- [ ] `git diff --check`
- [ ] Asset build/check attempted if JS/SCSS build inputs require it
- [ ] Manual smoke of key UI pages if local app/browser session is available
- [ ] SonarQube skipped unless explicitly requested by operator
- [ ] Quality Radar relevant risks handled or deferred
</verification>
<success_criteria>
- [ ] All AC pass.
- [ ] The most visible UI surfaces show proper Polish diacritics.
- [ ] No technical identifiers/contracts were accidentally translated.
- [ ] Verification complete with gaps documented.
- [ ] `DOCS/ARCHITECTURE.md` and `DOCS/TECH_CHANGELOG.md` updated; `DOCS/DB_SCHEMA.md` unchanged because no schema change.
</success_criteria>
<output>
SUMMARY.md path: `.paul/plans/20260518-2305-polskie-tlumaczenia/SUMMARY.md`
</output>

View File

@@ -0,0 +1,95 @@
---
plan_id: 20260518-2305-polskie-tlumaczenia
title: Poprawne polskie tlumaczenia UI
completed: 2026-05-18T23:40:00+02:00
storage: plan-first
quality_radar: degraded
---
# Summary: Poprawne polskie tlumaczenia UI
## Objective
Normalize visible Polish UI copy in orderPRO to proper UTF-8 Polish diacritics, without changing business logic, database schema, routes, form contracts, API payloads or technical identifiers.
## What Was Built
| Area | Result |
|------|--------|
| Central translations | `resources/lang/pl.php` now uses natural Polish UI labels and messages with diacritics. |
| PHP views | Visible hardcoded copy in orders, shipments, settings, accounting, automation, receipt and shared component views was corrected. |
| JavaScript messages | Existing module alerts/confirms/status texts were corrected without adding native prompt usage. |
| Backend operator messages | Selected flash/API/error messages returned to the UI were corrected in module controllers/services. |
| Template variables | Human labels/descriptions were localized while preserving ASCII placeholder keys such as `{{zamowienie.numer}}` and `{{przesylka.numer}}`. |
| Documentation | `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md` and PAUL radar/status artifacts were updated. |
## Files Modified
- `resources/lang/pl.php` - corrected central Polish translation values.
- `resources/views/**` - corrected visible legacy hardcoded UI copy.
- `public/assets/js/modules/*.js` - corrected visible browser messages in existing modules.
- `src/Modules/**/*.php` - corrected selected operator-facing flash/API/error text.
- `src/Modules/Settings/TemplateVariableCatalog.php` - localized human labels/descriptions while preserving variable keys.
- `DOCS/ARCHITECTURE.md` - documented Polish UI copy conventions and contract boundaries.
- `DOCS/TECH_CHANGELOG.md` - recorded the technical change and no-schema-change scope.
- `.paul/codebase/*` and `.paul/codebase/radar/jscpd-i18n-post-apply/` - recorded post-apply radar results.
- `.paul/STATE.md` - updated loop state.
- `.paul/plans/20260518-2305-polskie-tlumaczenia/*` - stored audit, residual scan and summary artifacts.
## Acceptance Criteria Results
| Criterion | Status | Evidence |
|-----------|--------|----------|
| AC-1: Navigation and core labels use Polish characters | Pass | `resources/lang/pl.php` and visible views were normalized; residual scan no longer reports uncorrected high-signal UI labels such as `Zamowienia`, `Przesylki`, `Platnosci`, `Pokaz`. |
| AC-2: Translation file is UTF-8 and semantically correct | Pass | `C:\xampp\php\php.exe -l resources\lang\pl.php` passed; PHP require check returned a valid array with 16 top-level keys; mojibake scan found no runtime/docs matches in changed scope. |
| AC-3: Visible hardcoded UI copy is corrected | Pass | Orders, shipments, settings, accounting, automation, receipt, component and JS/backend user-facing strings were corrected; remaining scan hits are technical identifiers or external/provider URLs. |
| AC-4: External and technical contracts are preserved | Pass | Placeholder guard found no `{{...}}` keys with Polish characters; `zamowienie.*`, `przesylka.*`, provider status slugs, route/form/API keys and URLs were intentionally left ASCII. |
| AC-5: Documentation and verification are complete | Partial | Docs and PAUL artifacts were updated; PHP lint, residual scans, jscpd and `git diff --check` passed. Manual browser smoke remains a gap due local app/browser session availability. SonarQube was removed from the automatic PAUL workflow after this plan and is now manual on-demand only. |
## Verification Results
| Check | Result | Notes |
|-------|--------|-------|
| `C:\xampp\php\php.exe -l resources\lang\pl.php` | Pass | No syntax errors. |
| `C:\xampp\php\php.exe -l` for changed PHP files | Pass | 91 changed PHP files linted successfully. |
| PHP require check for `resources/lang/pl.php` | Pass | File returns an array with 16 top-level keys. |
| Residual ASCII Polish scan | Pass | Remaining matches classified as template keys, provider status slugs or tracking URLs. |
| Placeholder guard | Pass | No `{{...}}` placeholder with Polish characters found. |
| Mojibake guard | Pass | No mojibake markers found in changed runtime/docs scope. |
| `git diff --check` | Pass | Passed; Git printed only line-ending warnings. |
| `npx --yes jscpd ...` | Pass | 226 files analyzed, 397 clones, 4754 duplicated lines; report written to `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`. |
| `sonar-scanner --version` | Historical / no longer required | It failed during APPLY because `sonar-scanner` was not available in PATH. SonarQube is now manual on-demand only and is not required for future PAUL PLAN/APPLY/UNIFY loops. |
| Asset build/check | Skipped | No SCSS/source build inputs were changed; public JS modules were edited directly as existing runtime assets. |
| Manual browser smoke | Skipped/Gapped | Local app/browser session was not started during APPLY/UNIFY. |
## Quality Radar Results
**Status:** degraded
- New risks: none from the translation normalization itself.
- Resolved risks: high-signal Polish copy/encoding risk for targeted UI surfaces is reduced; residual scan artifacts document preserved technical identifiers.
- Deferred risks: existing jscpd duplication in settings/accounting/integration views remains baseline debt; native prompt cleanup remains separate future work; SonarQube is manual on-demand only.
- Raw outputs:
- `.paul/codebase/radar/jscpd-i18n-post-apply/jscpd-report.json`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/translation-audit.txt`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/hardcoded-ui-files.txt`
- `.paul/plans/20260518-2305-polskie-tlumaczenia/residual-ui-scan.txt`
## Deviations
- The plan asked to attempt relevant asset/build checks if JS/SCSS build inputs required it; no SCSS/source build inputs changed, so no asset build was run.
- Manual browser smoke was planned "if local app/browser session is available"; it was not run in this session.
- `sonar-scanner` was attempted during APPLY under the old policy, but SonarQube has since been removed from automatic PAUL verification and moved to manual on-demand use.
- The plan-first work did not modify `DOCS/DB_SCHEMA.md` because there was no schema change.
## Key Decisions / Patterns
- Correct visible copy directly where legacy views/modules already hardcode text; avoid a broad i18n refactor in this translation-only plan.
- Preserve ASCII technical contracts for placeholder keys, provider slugs, route paths, form names, CSS classes, JS selectors, config keys and API payload fields.
- Treat marketplace/imported/user-created data as out of scope; this change only touches source-controlled UI copy.
## Follow-up
- Run manual smoke on key pages: `/orders/list`, order detail, `/settings/integrations`, `/settings/accounting`, `/settings/email-mailboxes`, shipment prepare and statistics pages.
- Run SonarQube only on explicit operator request.
- Plan a separate cleanup for native prompt fallbacks and repeated settings/accounting/integration layout patterns if still valuable.

View File

@@ -0,0 +1,87 @@
src\Modules\Automation\OrderStatusAgedService.php
src\Modules\Email\EmailSendingService.php
src\Modules\Accounting\ReceiptService.php
public\assets\js\modules\order-notes.js
src\Modules\Automation\AutomationController.php
src\Modules\Automation\AutomationService.php
src\Modules\Settings\AllegroIntegrationController.php
src\Modules\Accounting\ReceiptController.php
src\Modules\Settings\AllegroApiClient.php
src\Modules\Settings\AllegroOAuthClient.php
public\assets\js\modules\invoice-requested-toggle.js
src\Modules\Accounting\InvoiceService.php
src\Modules\Settings\AllegroOrderImportService.php
public\assets\js\modules\inline-status-change.js
public\assets\js\modules\automation-form.js
public\assets\js\modules\confirm-delete.js
src\Modules\Printing\PrintApiController.php
resources\views\shipments\prepare.php
src\Modules\Accounting\InvoiceController.php
src\Modules\Shipments\AllegroShipmentService.php
src\Modules\Settings\AllegroTokenManager.php
src\Modules\Shipments\ApaczkaShipmentService.php
src\Modules\Settings\AllegroStatusSyncService.php
src\Modules\Sms\SmsVariableResolver.php
src\Modules\Settings\ApaczkaApiClient.php
resources\views\orders\show.php
resources\views\orders\receipt-create.php
src\Modules\Shipments\DeliveryStatus.php
src\Modules\Shipments\InpostShipmentService.php
src\Modules\Orders\OrderNotesService.php
src\Modules\Orders\OrdersController.php
src\Modules\Shipments\PolkurierShipmentService.php
src\Modules\Settings\DeliveryStatusesController.php
src\Modules\Shipments\PolkurierTrackingService.php
resources\views\orders\partials\preview-content.php
resources\views\accounting\invoices_issued_list.php
src\Modules\Orders\OrdersRepository.php
resources\views\accounting\invoice_form.php
resources\views\components\table-list.php
src\Modules\Shipments\ShipmentController.php
resources\views\accounting\invoice_pdf.php
src\Modules\Settings\EmailTemplateController.php
resources\views\orders\partials\email-send-modal.php
src\Modules\Shipments\ShipmentPackageRepository.php
resources\views\accounting\invoice_preview.php
src\Modules\Settings\EmailMailboxController.php
src\Modules\Settings\ErliApiClient.php
resources\views\settings\accounting-receipt-edit.php
src\Modules\Settings\ErliExternalShipmentService.php
resources\views\settings\accounting-invoice-edit.php
resources\views\settings\accounting-invoices.php
resources\views\settings\allegro.php
resources\views\settings\accounting-receipts.php
resources\views\settings\email-mailboxes.php
resources\views\settings\accounting.php
resources\views\automation\index.php
resources\views\settings\delivery-statuses.php
resources\views\automation\form.php
resources\views\settings\email-templates.php
src\Modules\Settings\ErliOrderMapper.php
resources\views\settings\email-templates-form.php
resources\views\settings\fakturownia-edit.php
resources\views\settings\fakturownia.php
src\Modules\Settings\ErliOrdersSyncService.php
resources\views\receipts\print.php
resources\views\receipts\show.php
src\Modules\Settings\ErliStatusSyncService.php
src\Modules\Settings\FakturowniaApiClient.php
resources\views\settings\printing.php
src\Modules\Settings\FakturowniaIntegrationController.php
src\Modules\Settings\FakturowniaIntegrationRepository.php
resources\views\settings\sms-templates-form.php
src\Modules\Settings\HostedSmsApiClient.php
resources\views\settings\sms-templates.php
src\Modules\Settings\InvoiceConfigController.php
src\Modules\Settings\InvoiceConfigRepository.php
src\Modules\Settings\PolkurierApiClient.php
src\Modules\Settings\PrintSettingsController.php
src\Modules\Settings\ReceiptConfigController.php
src\Modules\Settings\ShopproApiClient.php
src\Modules\Settings\ShopproIntegrationsRepository.php
src\Modules\Settings\ShopproOrdersSyncService.php
src\Modules\Settings\ShopproPaymentStatusSyncService.php
src\Modules\Settings\SmsplanetApiClient.php
src\Modules\Settings\ShopproStatusSyncService.php
src\Modules\Settings\TemplateVariableCatalog.php
src\Modules\Settings\SmsTemplateController.php

View File

@@ -0,0 +1,32 @@
# Residual UI Copy Scan
Timestamp: 2026-05-18 23:25 Europe/Warsaw
Scope: resources/lang/pl.php, resources/views, public/assets/js/modules, src/Modules
## Command
rg -n "Wysyl|wysyl|Przesyl|przesyl|Zamow|zamow|Platn|platn|notatke|cofnac|ladowania|podgladu|odebrala|przesylke|przesylek|statusówi|inform¹c|Pokaz" resources/lang/pl.php resources/views public/assets/js/modules src/Modules
## Classification
Remaining matches are intentional technical identifiers/placeholders or provider URLs/status codes:
- SMS/e-mail template variable keys: `zamowienie.*`, `przesylka.*`, `kupujacy.*`; these are public template contract keys and must stay ASCII.
- Delivery provider status codes and URL slugs: e.g. InPost/DHL tracking URLs and provider status slugs in `DeliveryStatus.php`; these are external/provider-facing identifiers and must stay ASCII.
- UI copy matches from the scan were corrected during APPLY: statusowi, synchronizacji, Poka¿, podgl¹du, cofn¹æ, wysy³ka/wysy³anie.
## Placeholder Guard
Command:
rg -n "\{\{[^}\r\n]*[¹æê³ñ󜟿¥ÆÊ£ÑÓŒ<C393>¯][^}\r\n]*\}\}" resources/views resources/lang src public/assets/js/modules
Result: no matches. Template placeholders still use ASCII keys.
## Mojibake Guard
Command:
rg -n "Ä|A|A|Â|Ã|â€|Å|¼" resources/lang/pl.php resources/views public/assets/js/modules src/Modules DOCS
Result: no runtime/documentation matches in changed product/docs scope.

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"public_html": { "public_html": {
"AGENTS.md": { "AGENTS.md": {
"type": "-", "type": "-",
"size": 3719, "size": 3729,
"lmtime": 1772652932723, "lmtime": 1772652932723,
"modified": true "modified": true
}, },
@@ -1017,27 +1017,27 @@
"DOCS": { "DOCS": {
"ARCHITECTURE.md": { "ARCHITECTURE.md": {
"type": "-", "type": "-",
"size": 34823, "size": 38673,
"lmtime": 1779040812347, "lmtime": 1779128071678,
"modified": false "modified": false
}, },
"DB_SCHEMA.md": { "DB_SCHEMA.md": {
"type": "-", "type": "-",
"size": 48789, "size": 50221,
"lmtime": 1779040840321, "lmtime": 1779128071679,
"modified": false "modified": false
}, },
"TECH_CHANGELOG.md": { "TECH_CHANGELOG.md": {
"type": "-", "type": "-",
"size": 29892, "size": 36923,
"lmtime": 1779040825129, "lmtime": 1779128071680,
"modified": false "modified": false
}, },
"todo.md": { "todo.md": {
"type": "-", "type": "-",
"size": 878, "size": 1119,
"lmtime": 1779040800859, "lmtime": 1779052968103,
"modified": false "modified": true
} }
}, },
".env": { ".env": {
@@ -1048,7 +1048,7 @@
}, },
".env.example": { ".env.example": {
"type": "-", "type": "-",
"size": 1026, "size": 1029,
"lmtime": 1775673316997, "lmtime": 1775673316997,
"modified": true "modified": true
}, },
@@ -2398,8 +2398,8 @@
"css": { "css": {
"app.css": { "app.css": {
"type": "-", "type": "-",
"size": 68999, "size": 70439,
"lmtime": 1778885937938, "lmtime": 1779128071682,
"modified": false "modified": false
}, },
"app.css.map": { "app.css.map": {
@@ -2531,8 +2531,8 @@
"lang": { "lang": {
"pl.php": { "pl.php": {
"type": "-", "type": "-",
"size": 84241, "size": 84519,
"lmtime": 1778886050212, "lmtime": 1779128071684,
"modified": false "modified": false
} }
}, },
@@ -2579,8 +2579,8 @@
}, },
"app.scss": { "app.scss": {
"type": "-", "type": "-",
"size": 57489, "size": 58639,
"lmtime": 1778885937942, "lmtime": 1779128071685,
"modified": false "modified": false
}, },
"components": { "components": {
@@ -2696,14 +2696,14 @@
"accounting": { "accounting": {
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 3547, "size": 3532,
"lmtime": 0, "lmtime": 0,
"modified": false "modified": true
}, },
"invoice_form.php": { "invoice_form.php": {
"type": "-", "type": "-",
"size": 13645, "size": 13609,
"lmtime": 1778617477767, "lmtime": 1779052663465,
"modified": false "modified": false
}, },
"invoice_pdf.php": { "invoice_pdf.php": {
@@ -2728,22 +2728,22 @@
"auth": { "auth": {
"login.php": { "login.php": {
"type": "-", "type": "-",
"size": 1784, "size": 1835,
"lmtime": 1775815268579, "lmtime": 1779052631802,
"modified": false "modified": false
} }
}, },
"automation": { "automation": {
"form.php": { "form.php": {
"type": "-", "type": "-",
"size": 18891, "size": 18966,
"lmtime": 1777132029418, "lmtime": 1779052631816,
"modified": false "modified": false
}, },
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 15396, "size": 15857,
"lmtime": 1775590768288, "lmtime": 1779052631813,
"modified": false "modified": false
} }
}, },
@@ -2772,20 +2772,20 @@
"layouts": { "layouts": {
"app.php": { "app.php": {
"type": "-", "type": "-",
"size": 17928, "size": 18389,
"lmtime": 1778940460710, "lmtime": 1779128071687,
"modified": false "modified": false
}, },
"auth.php": { "auth.php": {
"type": "-", "type": "-",
"size": 1549, "size": 1630,
"lmtime": 1772220107387, "lmtime": 1779052663470,
"modified": true "modified": true
}, },
"public.php": { "public.php": {
"type": "-", "type": "-",
"size": 1670, "size": 1751,
"lmtime": 1775673889830, "lmtime": 1779052663471,
"modified": true "modified": true
} }
}, },
@@ -2812,9 +2812,9 @@
}, },
"list.php": { "list.php": {
"type": "-", "type": "-",
"size": 4620, "size": 4393,
"lmtime": 1775816812032, "lmtime": 1779128071688,
"modified": true "modified": false
}, },
"partials": { "partials": {
"email-send-modal.php": { "email-send-modal.php": {
@@ -2838,15 +2838,15 @@
}, },
"receipt-create.php": { "receipt-create.php": {
"type": "-", "type": "-",
"size": 7380, "size": 7351,
"lmtime": 1775462801767, "lmtime": 1779052663466,
"modified": true "modified": true
}, },
"show.php": { "show.php": {
"type": "-", "type": "-",
"size": 79678, "size": 79630,
"lmtime": 1778885937946, "lmtime": 1779052631838,
"modified": false "modified": true
} }
}, },
"products": { "products": {
@@ -2898,75 +2898,75 @@
"settings": { "settings": {
"accounting-invoice-edit.php": { "accounting-invoice-edit.php": {
"type": "-", "type": "-",
"size": 7002, "size": 6990,
"lmtime": 1778444374477, "lmtime": 1779052631849,
"modified": true "modified": false
}, },
"accounting-invoices.php": { "accounting-invoices.php": {
"type": "-", "type": "-",
"size": 4273, "size": 4261,
"lmtime": 1778444348951, "lmtime": 1779052631846,
"modified": true "modified": false
}, },
"accounting.php": { "accounting.php": {
"type": "-", "type": "-",
"size": 1826, "size": 1814,
"lmtime": 1778446442249, "lmtime": 1779052631864,
"modified": true "modified": false
}, },
"accounting-receipt-edit.php": { "accounting-receipt-edit.php": {
"type": "-", "type": "-",
"size": 4861, "size": 4849,
"lmtime": 1778444530228, "lmtime": 1779052631854,
"modified": true "modified": false
}, },
"accounting-receipts.php": { "accounting-receipts.php": {
"type": "-", "type": "-",
"size": 3471, "size": 3459,
"lmtime": 1778444511447, "lmtime": 1779052631859,
"modified": true "modified": false
}, },
"allegro.php": { "allegro.php": {
"type": "-", "type": "-",
"size": 42315, "size": 42291,
"lmtime": 1775589647792, "lmtime": 1779052631889,
"modified": true "modified": false
}, },
"apaczka.php": { "apaczka.php": {
"type": "-", "type": "-",
"size": 2912, "size": 2900,
"lmtime": 1773001309374, "lmtime": 1779052631894,
"modified": true "modified": false
}, },
"company.php": { "company.php": {
"type": "-", "type": "-",
"size": 7745, "size": 7733,
"lmtime": 1773009480295, "lmtime": 1779052631899,
"modified": true "modified": false
}, },
"cron.php": { "cron.php": {
"type": "-", "type": "-",
"size": 8831, "size": 8819,
"lmtime": 1774302948459, "lmtime": 1779052631905,
"modified": true "modified": false
}, },
"database.php": { "database.php": {
"type": "-", "type": "-",
"size": 3409, "size": 3385,
"lmtime": 1772491513567, "lmtime": 1779052631909,
"modified": true "modified": false
}, },
"delivery-statuses.php": { "delivery-statuses.php": {
"type": "-", "type": "-",
"size": 4644, "size": 4655,
"lmtime": 1779039854324, "lmtime": 1779039854324,
"modified": false "modified": true
}, },
"delivery-status-form.php": { "delivery-status-form.php": {
"type": "-", "type": "-",
"size": 3189, "size": 3183,
"lmtime": 1777320073160, "lmtime": 1779052631915,
"modified": true "modified": false
}, },
"_delivery-status-mappings-content.php": { "_delivery-status-mappings-content.php": {
"type": "-", "type": "-",
@@ -2976,33 +2976,33 @@
}, },
"delivery-status-mappings.php": { "delivery-status-mappings.php": {
"type": "-", "type": "-",
"size": 124, "size": 126,
"lmtime": 1779039857211, "lmtime": 1779039857211,
"modified": false "modified": true
}, },
"email-mailboxes.php": { "email-mailboxes.php": {
"type": "-", "type": "-",
"size": 19326, "size": 19314,
"lmtime": 1774728209847, "lmtime": 1779052631919,
"modified": true "modified": false
}, },
"email-templates-form.php": { "email-templates-form.php": {
"type": "-", "type": "-",
"size": 9678, "size": 9666,
"lmtime": 1775318330251, "lmtime": 1779052631926,
"modified": true "modified": false
}, },
"email-templates.php": { "email-templates.php": {
"type": "-", "type": "-",
"size": 5682, "size": 5670,
"lmtime": 1775318475603, "lmtime": 1779052631931,
"modified": true "modified": false
}, },
"erli.php": { "erli.php": {
"type": "-", "type": "-",
"size": 25931, "size": 26427,
"lmtime": 1778885104601, "lmtime": 1779052663469,
"modified": false "modified": true
}, },
"fakturownia-edit.php": { "fakturownia-edit.php": {
"type": "-", "type": "-",
@@ -3012,9 +3012,9 @@
}, },
"fakturownia.php": { "fakturownia.php": {
"type": "-", "type": "-",
"size": 5539, "size": 5521,
"lmtime": 1778443573844, "lmtime": 1779052631943,
"modified": true "modified": false
}, },
"gs1.php": { "gs1.php": {
"type": "-", "type": "-",
@@ -3024,21 +3024,21 @@
}, },
"hostedsms.php": { "hostedsms.php": {
"type": "-", "type": "-",
"size": 6322, "size": 6304,
"lmtime": 0, "lmtime": 1779052631947,
"modified": false "modified": false
}, },
"inpost.php": { "inpost.php": {
"type": "-", "type": "-",
"size": 7360, "size": 7348,
"lmtime": 1772750606535, "lmtime": 1779052631952,
"modified": true "modified": false
}, },
"integrations.php": { "integrations.php": {
"type": "-", "type": "-",
"size": 2667, "size": 3056,
"lmtime": 1772986683757, "lmtime": 1779128071689,
"modified": true "modified": false
}, },
"order-statuses.php": { "order-statuses.php": {
"type": "-", "type": "-",
@@ -3048,15 +3048,15 @@
}, },
"polkurier.php": { "polkurier.php": {
"type": "-", "type": "-",
"size": 5370, "size": 5377,
"lmtime": 1778885937947, "lmtime": 1779052663470,
"modified": false "modified": true
}, },
"printing.php": { "printing.php": {
"type": "-", "type": "-",
"size": 11190, "size": 11151,
"lmtime": 1774475356232, "lmtime": 1779052631967,
"modified": true "modified": false
}, },
"products.php": { "products.php": {
"type": "-", "type": "-",
@@ -3066,54 +3066,54 @@
}, },
"project-mappings.php": { "project-mappings.php": {
"type": "-", "type": "-",
"size": 9752, "size": 9740,
"lmtime": 1776018345590, "lmtime": 1779052631973,
"modified": true "modified": false
}, },
"shoppro.php": { "shoppro.php": {
"type": "-", "type": "-",
"size": 48192, "size": 52281,
"lmtime": 1773003933110, "lmtime": 1779128071690,
"modified": true "modified": false
}, },
"smsplanet.php": { "smsplanet.php": {
"type": "-", "type": "-",
"size": 10493, "size": 10475,
"lmtime": 0, "lmtime": 1779052632006,
"modified": false "modified": false
}, },
"sms-templates-form.php": { "sms-templates-form.php": {
"type": "-", "type": "-",
"size": 5184, "size": 5172,
"lmtime": 0, "lmtime": 1779052631996,
"modified": false "modified": true
}, },
"sms-templates.php": { "sms-templates.php": {
"type": "-", "type": "-",
"size": 4937, "size": 4925,
"lmtime": 0, "lmtime": 1779052632001,
"modified": false "modified": true
}, },
"statuses.php": { "statuses.php": {
"type": "-", "type": "-",
"size": 18314, "size": 18296,
"lmtime": 1772493011660, "lmtime": 1779052632010,
"modified": true "modified": false
} }
}, },
"shipments": { "shipments": {
"prepare.php": { "prepare.php": {
"type": "-", "type": "-",
"size": 59177, "size": 59427,
"lmtime": 1778886050211, "lmtime": 1779128071692,
"modified": false "modified": false
} }
}, },
"users": { "users": {
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 1600, "size": 1697,
"lmtime": 1772491504461, "lmtime": 1779052631805,
"modified": false "modified": false
} }
}, },
@@ -3124,14 +3124,28 @@
"lmtime": 1775674197653, "lmtime": 1775674197653,
"modified": false "modified": false
} }
},
"statistics": {
"orders.php": {
"type": "-",
"size": 8320,
"lmtime": 1779052631831,
"modified": false
},
"summary.php": {
"type": "-",
"size": 8567,
"lmtime": 0,
"modified": false
}
} }
} }
}, },
"routes": { "routes": {
"web.php": { "web.php": {
"type": "-", "type": "-",
"size": 47936, "size": 48135,
"lmtime": 1778886050207, "lmtime": 1779128071693,
"modified": false "modified": false
} }
}, },
@@ -3422,8 +3436,8 @@
}, },
"AutomationRepository.php": { "AutomationRepository.php": {
"type": "-", "type": "-",
"size": 11262, "size": 11267,
"lmtime": 1778940460709, "lmtime": 1779052561910,
"modified": false "modified": false
}, },
"AutomationService.php": { "AutomationService.php": {
@@ -3437,6 +3451,12 @@
"size": 5005, "size": 5005,
"lmtime": 1775948049470, "lmtime": 1775948049470,
"modified": false "modified": false
},
"AutomationRuleException.php": {
"type": "-",
"size": 155,
"lmtime": 1779052561904,
"modified": false
} }
}, },
"Cron": { "Cron": {
@@ -3636,14 +3656,20 @@
}, },
"OrdersController.php": { "OrdersController.php": {
"type": "-", "type": "-",
"size": 65754, "size": 65729,
"lmtime": 1778940460708, "lmtime": 1779128071695,
"modified": false "modified": false
}, },
"OrderSourceRegistry.php": {
"type": "-",
"size": 1319,
"lmtime": 1778940460704,
"modified": true
},
"OrdersRepository.php": { "OrdersRepository.php": {
"type": "-", "type": "-",
"size": 49778, "size": 49731,
"lmtime": 1778940460706, "lmtime": 1779128071696,
"modified": false "modified": false
}, },
"OrderStatusSyncService.php": { "OrderStatusSyncService.php": {
@@ -3651,12 +3677,6 @@
"size": 17295, "size": 17295,
"lmtime": 1772489130897, "lmtime": 1772489130897,
"modified": false "modified": false
},
"OrderSourceRegistry.php": {
"type": "-",
"size": 1262,
"lmtime": 1778940460704,
"modified": false
} }
}, },
"Printing": { "Printing": {
@@ -3782,7 +3802,7 @@
}, },
"AllegroIntegrationController.php": { "AllegroIntegrationController.php": {
"type": "-", "type": "-",
"size": 27140, "size": 27152,
"lmtime": 1775589568656, "lmtime": 1775589568656,
"modified": true "modified": true
}, },
@@ -3878,9 +3898,9 @@
}, },
"CarrierDeliveryMethodMappingRepository.php": { "CarrierDeliveryMethodMappingRepository.php": {
"type": "-", "type": "-",
"size": 9034, "size": 9081,
"lmtime": 1778884980256, "lmtime": 1778884980256,
"modified": false "modified": true
}, },
"CompanySettingsController.php": { "CompanySettingsController.php": {
"type": "-", "type": "-",
@@ -3902,19 +3922,19 @@
}, },
"DeliveryStatusesController.php": { "DeliveryStatusesController.php": {
"type": "-", "type": "-",
"size": 11209, "size": 11277,
"lmtime": 1779040416085, "lmtime": 1779040416085,
"modified": false "modified": true
}, },
"DeliveryStatusMappingController.php": { "DeliveryStatusMappingController.php": {
"type": "-", "type": "-",
"size": 10106, "size": 10179,
"lmtime": 1779039843270, "lmtime": 1779039843270,
"modified": false "modified": true
}, },
"EmailMailboxController.php": { "EmailMailboxController.php": {
"type": "-", "type": "-",
"size": 12752, "size": 12827,
"lmtime": 1774725281395, "lmtime": 1774725281395,
"modified": true "modified": true
}, },
@@ -3926,86 +3946,104 @@
}, },
"EmailTemplateController.php": { "EmailTemplateController.php": {
"type": "-", "type": "-",
"size": 9273, "size": 9309,
"lmtime": 1778941087323, "lmtime": 1778941087323,
"modified": true "modified": true
}, },
"EmailTemplateException.php": {
"type": "-",
"size": 162,
"lmtime": 1779052561905,
"modified": true
},
"EmailTemplateRepository.php": { "EmailTemplateRepository.php": {
"type": "-", "type": "-",
"size": 4583, "size": 4588,
"lmtime": 1775246458049, "lmtime": 1779052561910,
"modified": false "modified": true
}, },
"ErliApiClient.php": { "ErliApiClient.php": {
"type": "-", "type": "-",
"size": 14681, "size": 15065,
"lmtime": 1778885466388, "lmtime": 1778885466388,
"modified": false "modified": true
}, },
"ErliDeliveryMappingController.php": { "ErliDeliveryMappingController.php": {
"type": "-", "type": "-",
"size": 7363, "size": 7557,
"lmtime": 1778885057325, "lmtime": 1778885057325,
"modified": false "modified": true
}, },
"ErliExternalShipmentService.php": { "ErliExternalShipmentService.php": {
"type": "-", "type": "-",
"size": 6386, "size": 6555,
"lmtime": 1778885170413, "lmtime": 1778885170413,
"modified": false "modified": true
}, },
"ErliIntegrationController.php": { "ErliIntegrationController.php": {
"type": "-", "type": "-",
"size": 15254, "size": 15611,
"lmtime": 1778885023709, "lmtime": 1778885023709,
"modified": false "modified": true
}, },
"ErliIntegrationRepository.php": { "ErliIntegrationRepository.php": {
"type": "-", "type": "-",
"size": 9274, "size": 9536,
"lmtime": 1778881085068, "lmtime": 1778881085068,
"modified": false "modified": true
}, },
"ErliOrderMapper.php": { "ErliOrderMapper.php": {
"type": "-", "type": "-",
"size": 19260, "size": 19733,
"lmtime": 1778882727736, "lmtime": 1779052561908,
"modified": false "modified": true
},
"ErliOrderMappingException.php": {
"type": "-",
"size": 165,
"lmtime": 1779052561903,
"modified": true
}, },
"ErliOrdersSyncService.php": { "ErliOrdersSyncService.php": {
"type": "-", "type": "-",
"size": 13118, "size": 13476,
"lmtime": 1778939105137, "lmtime": 1778939105137,
"modified": false "modified": true
}, },
"ErliOrderSyncStateRepository.php": { "ErliOrderSyncStateRepository.php": {
"type": "-", "type": "-",
"size": 6123, "size": 6304,
"lmtime": 1778882722443, "lmtime": 1778882722443,
"modified": false "modified": true
}, },
"ErliPullStatusMappingRepository.php": { "ErliPullStatusMappingRepository.php": {
"type": "-", "type": "-",
"size": 4993, "size": 5141,
"lmtime": 1778882692008, "lmtime": 1778882692008,
"modified": false "modified": true
}, },
"ErliStatusMappingRepository.php": { "ErliStatusMappingRepository.php": {
"type": "-", "type": "-",
"size": 3849, "size": 3966,
"lmtime": 1778882692007, "lmtime": 1778882692007,
"modified": false "modified": true
}, },
"ErliStatusSyncService.php": { "ErliStatusSyncService.php": {
"type": "-", "type": "-",
"size": 7860, "size": 8083,
"lmtime": 1778882820718, "lmtime": 1778882820718,
"modified": false "modified": true
}, },
"FakturowniaApiClient.php": { "FakturowniaApiClient.php": {
"type": "-", "type": "-",
"size": 12570, "size": 12992,
"lmtime": 1778448029992, "lmtime": 1779052561906,
"modified": true
},
"FakturowniaApiException.php": {
"type": "-",
"size": 163,
"lmtime": 1779052561901,
"modified": true "modified": true
}, },
"FakturowniaIntegrationController.php": { "FakturowniaIntegrationController.php": {
@@ -4064,8 +4102,8 @@
}, },
"IntegrationsHubController.php": { "IntegrationsHubController.php": {
"type": "-", "type": "-",
"size": 14674, "size": 15629,
"lmtime": 1778886050206, "lmtime": 1779128071698,
"modified": false "modified": false
}, },
"IntegrationsRepository.php": { "IntegrationsRepository.php": {
@@ -4100,9 +4138,15 @@
}, },
"PolkurierApiClient.php": { "PolkurierApiClient.php": {
"type": "-", "type": "-",
"size": 12990, "size": 12992,
"lmtime": 1778885937958, "lmtime": 1779052561907,
"modified": false "modified": true
},
"PolkurierApiException.php": {
"type": "-",
"size": 161,
"lmtime": 1779052561902,
"modified": true
}, },
"PolkurierIntegrationController.php": { "PolkurierIntegrationController.php": {
"type": "-", "type": "-",
@@ -4172,9 +4216,9 @@
}, },
"ShopproIntegrationsController.php": { "ShopproIntegrationsController.php": {
"type": "-", "type": "-",
"size": 41512, "size": 43731,
"lmtime": 1773408010714, "lmtime": 1779128071699,
"modified": true "modified": false
}, },
"ShopproIntegrationsRepository.php": { "ShopproIntegrationsRepository.php": {
"type": "-", "type": "-",
@@ -4250,21 +4294,21 @@
}, },
"SmsTemplateController.php": { "SmsTemplateController.php": {
"type": "-", "type": "-",
"size": 6282, "size": 6610,
"lmtime": 1778941087325, "lmtime": 1779052603932,
"modified": true "modified": true
}, },
"SmtpSecurityContextFactory.php": { "SmtpSecurityContextFactory.php": {
"type": "-", "type": "-",
"size": 1159, "size": 1208,
"lmtime": 0, "lmtime": 0,
"modified": false "modified": true
}, },
"TemplateVariableCatalog.php": { "TemplateVariableCatalog.php": {
"type": "-", "type": "-",
"size": 3846, "size": 3976,
"lmtime": 0, "lmtime": 0,
"modified": false "modified": true
} }
}, },
"Shipments": { "Shipments": {
@@ -4300,15 +4344,15 @@
}, },
"DeliveryStatus.php": { "DeliveryStatus.php": {
"type": "-", "type": "-",
"size": 25566, "size": 25812,
"lmtime": 1779040606945, "lmtime": 1779040606945,
"modified": false "modified": true
}, },
"DeliveryStatusRepository.php": { "DeliveryStatusRepository.php": {
"type": "-", "type": "-",
"size": 5416, "size": 5512,
"lmtime": 1779039679909, "lmtime": 1779039679909,
"modified": false "modified": true
}, },
"InpostShipmentService.php": { "InpostShipmentService.php": {
"type": "-", "type": "-",
@@ -4324,8 +4368,8 @@
}, },
"PolkurierShipmentService.php": { "PolkurierShipmentService.php": {
"type": "-", "type": "-",
"size": 28548, "size": 28976,
"lmtime": 1778885937963, "lmtime": 1779128071700,
"modified": false "modified": false
}, },
"PolkurierTrackingService.php": { "PolkurierTrackingService.php": {
@@ -4336,9 +4380,9 @@
}, },
"ShipmentController.php": { "ShipmentController.php": {
"type": "-", "type": "-",
"size": 25600, "size": 25644,
"lmtime": 1778938051366, "lmtime": 1778938051366,
"modified": false "modified": true
}, },
"ShipmentPackageRepository.php": { "ShipmentPackageRepository.php": {
"type": "-", "type": "-",
@@ -4392,8 +4436,8 @@
}, },
"UsersController.php": { "UsersController.php": {
"type": "-", "type": "-",
"size": 5179, "size": 5772,
"lmtime": 1772491357326, "lmtime": 1779052603935,
"modified": false "modified": false
} }
}, },
@@ -6858,13 +6902,13 @@
"email_photo_fetcher.cpython-312.pyc": { "email_photo_fetcher.cpython-312.pyc": {
"type": "-", "type": "-",
"size": 9439, "size": 9439,
"lmtime": 1778189100394, "lmtime": 1779128071704,
"modified": false "modified": false
}, },
"_pudelko_komunia_core.cpython-312.pyc": { "_pudelko_komunia_core.cpython-312.pyc": {
"type": "-", "type": "-",
"size": 5888, "size": 5888,
"lmtime": 1778885937966, "lmtime": 1779052069927,
"modified": false "modified": false
} }
}, },

View File

@@ -107,6 +107,12 @@ HTTP Request
## Frontend Enhancement Modules ## Frontend Enhancement Modules
### Polish UI Copy
- `resources/lang/pl.php` is the primary Polish translation source loaded by `src/Core/I18n/Translator.php` and exposed to views as `$t()`.
- Views and small browser modules may still contain legacy hardcoded UI copy, but visible operator-facing text is normalized to UTF-8 Polish diacritics.
- Template placeholder keys remain ASCII technical contracts, e.g. `{{zamowienie.numer}}` and `{{przesylka.numer}}`; only their human labels/descriptions are localized.
- This copy normalization does not change routes, form field names, status codes, API payload keys, database schema, or imported marketplace data.
### Orders List and Sidebar State ### Orders List and Sidebar State
- `/orders/list` renders the reusable order status panel and shared `components/table-list` directly; the previous descriptive intro card was removed so the operational table starts higher on the screen. - `/orders/list` renders the reusable order status panel and shared `components/table-list` directly; the previous descriptive intro card was removed so the operational table starts higher on the screen.
- The order number cell shows the notes badge from `OrdersRepository::notesCountSubquerySql()`, counting every `order_notes` row for the order. Imported source notes (`note_type <> 'user'`) and operator notes (`note_type='user'`) both contribute to the same `[N]` badge, while order details still render those groups separately. - The order number cell shows the notes badge from `OrdersRepository::notesCountSubquerySql()`, counting every `order_notes` row for the order. Imported source notes (`note_type <> 'user'`) and operator notes (`note_type='user'`) both contribute to the same `[N]` badge, while order details still render those groups separately.

View File

@@ -395,6 +395,17 @@
- SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora. - SMSPLANET wspiera dwa warianty autoryzacji, wiec konfiguracja przechowuje wszystkie sekrety w formie szyfrowanej i waliduje wymagania zalezne od wyboru operatora.
- Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API. - Test uzywa rzeczywistej wysylki, bo celem tej fazy jest potwierdzenie realnej sciezki API.
## 2026-05-18 - Polish UI Copy Normalization
**Co zrobiono:**
- Znormalizowano polskie teksty interfejsu do UTF-8 z polskimi znakami w `resources/lang/pl.php`.
- Poprawiono widoczne hardcoded komunikaty w wybranych widokach, modułach JS i komunikatach backendu.
- Zachowano techniczne kontrakty placeholderów, tras, kluczy formularzy, kodów statusów i payloadów API.
**Dlaczego:**
- Interfejs zawierał wiele tekstów bez polskich znaków, np. `Zamowienia`, `Przesylki`, `Platnosci`.
- Zmiana jest warstwą copy/UI; nie dodaje migracji, tabel ani nowych konfiguracji środowiskowych.
## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS ## 2026-05-12 - Phase 116 Plan 01: HostedSMS Integration Settings + Test SMS
**Co zrobiono:** **Co zrobiono:**

View File

@@ -108,7 +108,7 @@
html += '</select>'; html += '</select>';
html += '<label class="checkbox-label">' html += '<label class="checkbox-label">'
+ '<input type="checkbox" name="' + namePrefix + '[send_once_per_order]" value="1"> ' + '<input type="checkbox" name="' + namePrefix + '[send_once_per_order]" value="1"> '
+ 'Wyslij tylko raz dla tego zamowienia' + 'Wyślij tylko raz dla tego zamówienia'
+ '</label>'; + '</label>';
return html; return html;
@@ -145,7 +145,7 @@
function buildShipmentStatusActionConfig(namePrefix) { function buildShipmentStatusActionConfig(namePrefix) {
var html = '<select class="form-control" name="' + namePrefix + '[shipment_status_key]">' var html = '<select class="form-control" name="' + namePrefix + '[shipment_status_key]">'
+ '<option value="">-- Wybierz docelowy status przesylki --</option>'; + '<option value="">-- Wybierz docelowy status przesyłki --</option>';
Object.keys(data.shipmentStatusOptions || {}).forEach(function(statusKey) { Object.keys(data.shipmentStatusOptions || {}).forEach(function(statusKey) {
var config = data.shipmentStatusOptions[statusKey] || {}; var config = data.shipmentStatusOptions[statusKey] || {};
@@ -159,7 +159,7 @@
function buildOrderStatusActionConfig(namePrefix) { function buildOrderStatusActionConfig(namePrefix) {
var html = '<select class="form-control" name="' + namePrefix + '[order_status_code]">' var html = '<select class="form-control" name="' + namePrefix + '[order_status_code]">'
+ '<option value="">-- Wybierz docelowy status zamowienia --</option>'; + '<option value="">-- Wybierz docelowy status zamówienia --</option>';
(data.orderStatusOptions || []).forEach(function(statusOption) { (data.orderStatusOptions || []).forEach(function(statusOption) {
html += '<option value="' + escapeHtml(statusOption.code || '') + '">' html += '<option value="' + escapeHtml(statusOption.code || '') + '">'
@@ -182,10 +182,10 @@
row.innerHTML = '<div class="automation-row__fields">' row.innerHTML = '<div class="automation-row__fields">'
+ '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onConditionTypeChange(this)">' + '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onConditionTypeChange(this)">'
+ '<option value="integration" selected>Integracja (kanal sprzedazy)</option>' + '<option value="integration" selected>Integracja (kanal sprzedazy)</option>'
+ '<option value="shipment_status">Status przesylki</option>' + '<option value="shipment_status">Status przesyłki</option>'
+ '<option value="payment_status">Status platnosci</option>' + '<option value="payment_status">Status płatności</option>'
+ '<option value="payment_method">Metoda platnosci</option>' + '<option value="payment_method">Metoda płatności</option>'
+ '<option value="order_status">Status zamowienia</option>' + '<option value="order_status">Status zamówienia</option>'
+ '<option value="days_in_status">Liczba dni w statusie</option>' + '<option value="days_in_status">Liczba dni w statusie</option>'
+ '</select>' + '</select>'
+ '<div class="automation-row__config">' + '<div class="automation-row__config">'
@@ -207,10 +207,10 @@
row.innerHTML = '<div class="automation-row__fields">' row.innerHTML = '<div class="automation-row__fields">'
+ '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onActionTypeChange(this)">' + '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onActionTypeChange(this)">'
+ '<option value="send_email" selected>Wyslij e-mail</option>' + '<option value="send_email" selected>Wyślij e-mail</option>'
+ '<option value="issue_receipt">Wystaw paragon</option>' + '<option value="issue_receipt">Wystaw paragon</option>'
+ '<option value="update_shipment_status">Zmiana statusu przesylki</option>' + '<option value="update_shipment_status">Zmiana statusu przesyłki</option>'
+ '<option value="update_order_status">Zmiana statusu zamowienia</option>' + '<option value="update_order_status">Zmiana statusu zamówienia</option>'
+ '</select>' + '</select>'
+ '<div class="automation-row__config">' + '<div class="automation-row__config">'
+ buildEmailActionConfig(namePrefix) + buildEmailActionConfig(namePrefix)

View File

@@ -9,8 +9,8 @@
var form = btn.closest('form'); var form = btn.closest('form');
if (!form) return; if (!form) return;
var title = form.getAttribute('data-confirm-title') || 'Usun pozycje'; var title = form.getAttribute('data-confirm-title') || 'Usuń pozycje';
var message = form.getAttribute('data-confirm-message') || 'Czy na pewno chcesz usunac ten wpis?'; var message = form.getAttribute('data-confirm-message') || 'Czy na pewno chcesz usuńąć ten wpis?';
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
var submitted = false; var submitted = false;
@@ -23,7 +23,7 @@
title: title, title: title,
message: message, message: message,
danger: true, danger: true,
confirmLabel: 'Usun', confirmLabel: 'Usuń',
onConfirm: doSubmit onConfirm: doSubmit
}); });
if (result && typeof result.then === 'function') { if (result && typeof result.then === 'function') {

View File

@@ -175,9 +175,9 @@
if (!result.ok || !result.data.success) { if (!result.ok || !result.data.success) {
wrap.innerHTML = prevHtml; wrap.innerHTML = prevHtml;
wrap.setAttribute('data-current-status', prevStatus || ''); wrap.setAttribute('data-current-status', prevStatus || '');
var msg = (result.data && result.data.error) ? result.data.error : 'Nie udalo sie zmienic statusu'; var msg = (result.data && result.data.error) ? result.data.error : 'Nie udało się zmienić statusu';
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({ title: 'Blad', message: msg }); window.OrderProAlerts.alert({ title: 'Błąd', message: msg });
} }
return; return;
} }
@@ -191,7 +191,7 @@
wrap.innerHTML = prevHtml; wrap.innerHTML = prevHtml;
wrap.setAttribute('data-current-status', prevStatus || ''); wrap.setAttribute('data-current-status', prevStatus || '');
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({ title: 'Blad', message: 'Blad polaczenia z serwerem' }); window.OrderProAlerts.alert({ title: 'Błąd', message: 'Błąd połączenia z serwerem' });
} }
}); });
} }

View File

@@ -34,7 +34,7 @@
}) })
.then(function (data) { .then(function (data) {
if (!data || !data.success) { if (!data || !data.success) {
throw new Error(data && data.error ? data.error : 'Blad serwera'); throw new Error(data && data.error ? data.error : 'Błąd serwera');
} }
if (buttonWrap) { if (buttonWrap) {
buttonWrap.style.display = newValue ? '' : 'none'; buttonWrap.style.display = newValue ? '' : 'none';
@@ -43,7 +43,7 @@
.catch(function (err) { .catch(function (err) {
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
if (window.OrderProAlerts && typeof window.OrderProAlerts.error === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.error === 'function') {
window.OrderProAlerts.error('Nie udalo sie zmienic flagi faktury: ' + (err && err.message ? err.message : '')); window.OrderProAlerts.error('Nie udało się zmienić flagi faktury: ' + (err && err.message ? err.message : ''));
} else { } else {
console.error('invoice-requested toggle failed', err); console.error('invoice-requested toggle failed', err);
} }

View File

@@ -50,10 +50,10 @@
event.preventDefault(); event.preventDefault();
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
title: 'Usunac notatke?', title: 'Usunąć notatkę?',
message: 'Tej operacji nie mozna cofnac.', message: 'Tej operacji nie można cofnąć.',
danger: true, danger: true,
confirmLabel: 'Usun', confirmLabel: 'Usuń',
onConfirm: function () { onConfirm: function () {
form.dataset.bound = '2'; form.dataset.bound = '2';
form.submit(); form.submit();

View File

@@ -44,8 +44,8 @@
applyBody(textarea, data.body); applyBody(textarea, data.body);
} else if (window.OrderProAlerts && window.OrderProAlerts.alert) { } else if (window.OrderProAlerts && window.OrderProAlerts.alert) {
window.OrderProAlerts.alert({ window.OrderProAlerts.alert({
title: 'Nie udalo sie wczytac szablonu', title: 'Nie udało się wczytać szablonu',
message: (data && data.error) ? String(data.error) : 'Sprobuj ponownie.' message: (data && data.error) ? String(data.error) : 'Spróbuj ponownie.'
}); });
} }
}) })
@@ -66,8 +66,8 @@
var triggered = false; var triggered = false;
var run = function () { if (!triggered) { triggered = true; doFetch(); } }; var run = function () { if (!triggered) { triggered = true; doFetch(); } };
var result = window.OrderProAlerts.confirm({ var result = window.OrderProAlerts.confirm({
title: 'Zamiana tresci', title: 'Zamiana treśći',
message: 'Tekst w polu wiadomosci zostanie nadpisany trescia szablonu. Kontynuowac?', message: 'Tekst w polu wiadomości zostanie nadpisany treśćia szablonu. Kontynuowac?',
confirmLabel: 'Wstaw szablon', confirmLabel: 'Wstaw szablon',
danger: false, danger: false,
onConfirm: run, onConfirm: run,
@@ -76,7 +76,7 @@
if (result && typeof result.then === 'function') { if (result && typeof result.then === 'function') {
result.then(function (ok) { if (ok) run(); else picker.value = ''; }); result.then(function (ok) { if (ok) run(); else picker.value = ''; });
} }
} else if (window.confirm('Tekst w polu wiadomosci zostanie nadpisany. Kontynuowac?')) { } else if (window.confirm('Tekst w polu wiadomości zostanie nadpisany. Kontynuowac?')) {
doFetch(); doFetch();
} else { } else {
picker.value = ''; picker.value = '';

File diff suppressed because it is too large Load Diff

View File

@@ -21,10 +21,10 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
<section class="card"> <section class="card">
<div class="order-details-head"> <div class="order-details-head">
<div> <div>
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">&larr; Powrot do zamowienia</a> <a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">&larr; Powrót do zamówienia</a>
<h2 class="section-title mt-12">Wystaw fakture</h2> <h2 class="section-title mt-12">Wystaw fakturę</h2>
<div class="order-details-sub mt-4"> <div class="order-details-sub mt-4">
Zamowienie <?= $e((string) ($orderRow['internal_order_number'] ?? ('#' . $orderIdVal))) ?> Zamówienie <?= $e((string) ($orderRow['internal_order_number'] ?? ('#' . $orderIdVal))) ?>
</div> </div>
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
<?php <?php
ob_start(); ob_start();
?> ?>
<strong>Uwaga!</strong> Do tego zamowienia wystawiono juz <?= $e((string) count($existingInvoicesList)) ?> fakture/y: <strong>Uwaga!</strong> Do tego zamówienia wystawiono już <?= $e((string) count($existingInvoicesList)) ?> fakturę/y:
<ul class="mt-4"> <ul class="mt-4">
<?php foreach ($existingInvoicesList as $ei): ?> <?php foreach ($existingInvoicesList as $ei): ?>
<li> <li>
@@ -97,14 +97,14 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
</button> </button>
</div> </div>
<?php if ($autoNip !== ''): ?> <?php if ($autoNip !== ''): ?>
<small class="muted">Auto-wykryty z payload zamowienia. Mozesz nadpisac lub kliknac "Pobierz z GUS".</small> <small class="muted">Auto-wykryty z payload zamówienia. Możesz nadpisać lub kliknąć "Pobierz z GUS".</small>
<?php else: ?> <?php else: ?>
<small class="muted">Wpisz NIP i kliknij "Pobierz z GUS" — dane firmy zostana wypelnione automatycznie.</small> <small class="muted">Wpisz NIP i kliknij "Pobierz z GUS" — dane firmy zostana wypelnione automatycznie.</small>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="buyer_name">Imie i nazwisko</label> <label class="form-label" for="buyer_name">Imię i nazwisko</label>
<input type="text" name="buyer_name" id="buyer_name" class="form-control" <input type="text" name="buyer_name" id="buyer_name" class="form-control"
value="<?= $e($buyerNameDefault) ?>"> value="<?= $e($buyerNameDefault) ?>">
</div> </div>
@@ -140,14 +140,14 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
</div> </div>
</div> </div>
<h3 class="section-title mt-16">Pozycje zamowienia</h3> <h3 class="section-title mt-16">Pozycje zamówienia</h3>
<div class="table-wrap mt-8"> <div class="table-wrap mt-8">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
<tr> <tr>
<th>Lp.</th> <th>Lp.</th>
<th>Nazwa</th> <th>Nazwa</th>
<th>Ilosc</th> <th>Ilość</th>
<th>Cena brutto</th> <th>Cena brutto</th>
<th>VAT</th> <th>VAT</th>
<th>Suma brutto</th> <th>Suma brutto</th>
@@ -182,7 +182,7 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
?> ?>
<tr> <tr>
<td><?= $e((string) (count($itemsList) + 1)) ?></td> <td><?= $e((string) (count($itemsList) + 1)) ?></td>
<td>Koszt wysylki</td> <td>Koszt wysyłki</td>
<td>1</td> <td>1</td>
<td><?= $e(number_format($deliveryPrice, 2, '.', ' ')) ?></td> <td><?= $e(number_format($deliveryPrice, 2, '.', ' ')) ?></td>
<td>23%</td> <td>23%</td>
@@ -210,9 +210,9 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
<div class="mt-16"> <div class="mt-16">
<?php if ($hasExistingInvoices): ?> <?php if ($hasExistingInvoices): ?>
<button type="button" id="invoice-submit-btn" class="btn btn--primary">Wystaw fakture</button> <button type="button" id="invoice-submit-btn" class="btn btn--primary">Wystaw fakturę</button>
<?php else: ?> <?php else: ?>
<button type="submit" class="btn btn--primary">Wystaw fakture</button> <button type="submit" class="btn btn--primary">Wystaw fakturę</button>
<?php endif; ?> <?php endif; ?>
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary ml-8">Anuluj</a> <a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary ml-8">Anuluj</a>
</div> </div>
@@ -249,7 +249,7 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, data: j }; }); }) .then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, data: j }; }); })
.then(function (res) { .then(function (res) {
if (!res.ok || !res.data || !res.data.success) { if (!res.ok || !res.data || !res.data.success) {
var msg = res.data && res.data.error ? res.data.error : 'Blad pobierania danych z GUS.'; var msg = res.data && res.data.error ? res.data.error : 'Błąd pobierania danych z GUS.';
throw new Error(msg); throw new Error(msg);
} }
var d = res.data.data || {}; var d = res.data.data || {};
@@ -271,9 +271,9 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
}) })
.catch(function (err) { .catch(function (err) {
if (window.OrderProAlerts && window.OrderProAlerts.error) { if (window.OrderProAlerts && window.OrderProAlerts.error) {
window.OrderProAlerts.error(err && err.message ? err.message : 'Blad GUS.'); window.OrderProAlerts.error(err && err.message ? err.message : 'Błąd GUS.');
} else { } else {
alert(err && err.message ? err.message : 'Blad GUS.'); alert(err && err.message ? err.message : 'Błąd GUS.');
} }
}) })
.finally(function () { .finally(function () {
@@ -288,8 +288,8 @@ $buyerEmailDefault = trim((string) ($buyerAddr['email'] ?? $orderRow['buyer_emai
<script> <script>
document.getElementById('invoice-submit-btn').addEventListener('click', function() { document.getElementById('invoice-submit-btn').addEventListener('click', function() {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
title: 'Wystawic kolejna fakture?', title: 'Wystawic kolejną fakturę?',
message: 'Do tego zamowienia wystawiono juz fakture. Czy na pewno chcesz wystawic kolejna?', message: 'Do tego zamówienia wystawiono już fakturę. Czy na pewno chcesz wystawić kolejną?',
confirmLabel: 'Wystaw', confirmLabel: 'Wystaw',
danger: false, danger: false,
onConfirm: function() { onConfirm: function() {

View File

@@ -100,7 +100,7 @@ $totalVat = max(0.0, $totalGross - $totalNet);
<tr> <tr>
<th>Lp.</th> <th>Lp.</th>
<th>Nazwa</th> <th>Nazwa</th>
<th class="text-right">Ilosc</th> <th class="text-right">Ilość</th>
<th class="text-right">Cena netto</th> <th class="text-right">Cena netto</th>
<th class="text-right">VAT</th> <th class="text-right">VAT</th>
<th class="text-right">Cena brutto</th> <th class="text-right">Cena brutto</th>
@@ -138,7 +138,7 @@ $totalVat = max(0.0, $totalGross - $totalNet);
<?php if (($invoiceData['payment_due_date'] ?? null) !== null): ?> <?php if (($invoiceData['payment_due_date'] ?? null) !== null): ?>
<div class="invoice-payment"> <div class="invoice-payment">
<strong>Termin platnosci:</strong> <?= $e(substr((string) $invoiceData['payment_due_date'], 0, 10)) ?> <strong>Termin płatności:</strong> <?= $e(substr((string) $invoiceData['payment_due_date'], 0, 10)) ?>
<?php if (trim((string) ($sellerData['bank_account'] ?? '')) !== ''): ?> <?php if (trim((string) ($sellerData['bank_account'] ?? '')) !== ''): ?>
| <strong>Konto:</strong> <?= $e((string) $sellerData['bank_account']) ?> | <strong>Konto:</strong> <?= $e((string) $sellerData['bank_account']) ?>
<?php endif; ?> <?php endif; ?>

View File

@@ -16,12 +16,12 @@ $externalPdfVal = trim((string) ($invoiceData['external_pdf_url'] ?? ''));
<section class="card"> <section class="card">
<div class="order-details-head"> <div class="order-details-head">
<div> <div>
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">&larr; Powrot do zamowienia</a> <a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">&larr; Powrót do zamówienia</a>
<h2 class="section-title mt-12">Faktura <?= $e($invoiceNumber) ?></h2> <h2 class="section-title mt-12">Faktura <?= $e($invoiceNumber) ?></h2>
<div class="order-details-sub mt-4"> <div class="order-details-sub mt-4">
<?php if ($isDelegatedFlag): ?> <?php if ($isDelegatedFlag): ?>
<span class="badge badge--success">Wystawione w Fakturowni<?php if (trim((string) ($accountPrefix ?? '')) !== ''): ?>: <?= $e((string) $accountPrefix) ?><?php endif; ?></span> <span class="badge badge--success">Wystawione w Fakturowni<?php if (trim((string) ($accountPrefix ?? '')) !== ''): ?>: <?= $e((string) $accountPrefix) ?><?php endif; ?></span>
<?php if ($externalIdVal !== ''): ?> <span class="muted">(id zewnetrzne: <?= $e($externalIdVal) ?>)</span><?php endif; ?> <?php if ($externalIdVal !== ''): ?> <span class="muted">(id zewnętrzne: <?= $e($externalIdVal) ?>)</span><?php endif; ?>
<?php else: ?> <?php else: ?>
<span class="badge badge--muted">Wystawione lokalnie</span> <span class="badge badge--muted">Wystawione lokalnie</span>
<?php endif; ?> <?php endif; ?>
@@ -33,7 +33,7 @@ $externalPdfVal = trim((string) ($invoiceData['external_pdf_url'] ?? ''));
<?php else: ?> <?php else: ?>
<a href="/orders/<?= $e((string) $orderIdVal) ?>/invoice/<?= $e((string) ($invoiceData['id'] ?? '')) ?>/pdf" class="btn btn--primary">Pobierz PDF</a> <a href="/orders/<?= $e((string) $orderIdVal) ?>/invoice/<?= $e((string) ($invoiceData['id'] ?? '')) ?>/pdf" class="btn btn--primary">Pobierz PDF</a>
<?php endif; ?> <?php endif; ?>
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary">Powrot</a> <a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary">Powrót</a>
</div> </div>
</div> </div>
@@ -80,7 +80,7 @@ $externalPdfVal = trim((string) ($invoiceData['external_pdf_url'] ?? ''));
<tr> <tr>
<th>Lp.</th> <th>Lp.</th>
<th>Nazwa</th> <th>Nazwa</th>
<th>Ilosc</th> <th>Ilość</th>
<th>Cena netto</th> <th>Cena netto</th>
<th>VAT</th> <th>VAT</th>
<th>Cena brutto</th> <th>Cena brutto</th>
@@ -119,7 +119,7 @@ $externalPdfVal = trim((string) ($invoiceData['external_pdf_url'] ?? ''));
<dt>Data wystawienia</dt><dd><?= $e(strlen($issueDateShow) >= 16 ? substr($issueDateShow, 0, 16) : $issueDateShow) ?></dd> <dt>Data wystawienia</dt><dd><?= $e(strlen($issueDateShow) >= 16 ? substr($issueDateShow, 0, 16) : $issueDateShow) ?></dd>
<dt>Data sprzedazy</dt><dd><?= $e((string) ($invoiceData['sale_date'] ?? '-')) ?></dd> <dt>Data sprzedazy</dt><dd><?= $e((string) ($invoiceData['sale_date'] ?? '-')) ?></dd>
<?php if (($invoiceData['payment_due_date'] ?? null) !== null): ?> <?php if (($invoiceData['payment_due_date'] ?? null) !== null): ?>
<dt>Termin platnosci</dt><dd><?= $e(substr((string) $invoiceData['payment_due_date'], 0, 10)) ?></dd> <dt>Termin płatności</dt><dd><?= $e(substr((string) $invoiceData['payment_due_date'], 0, 10)) ?></dd>
<?php endif; ?> <?php endif; ?>
<dt>Konfiguracja</dt><dd><?= $e($configNameVal !== '' ? $configNameVal : '-') ?></dd> <dt>Konfiguracja</dt><dd><?= $e($configNameVal !== '' ? $configNameVal : '-') ?></dd>
<dt>Typ</dt><dd><?= $e((string) ($invoiceData['kind'] ?? 'vat')) ?></dd> <dt>Typ</dt><dd><?= $e((string) ($invoiceData['kind'] ?? 'vat')) ?></dd>

View File

@@ -11,16 +11,16 @@ $perPageNum = (int) ($perPage ?? 50);
<section class="card"> <section class="card">
<div class="orders-head"> <div class="orders-head">
<div> <div>
<a href="/settings/accounting" class="order-back-link">&larr; Ksiegowosc</a> <a href="/settings/accounting" class="order-back-link">&larr; Księgowość</a>
<h2 class="section-title mt-12">Wystawione faktury</h2> <h2 class="section-title mt-12">Wystawione faktury</h2>
<div class="muted mt-4">Lacznie: <?= $e((string) $totalCount) ?></div> <div class="muted mt-4">Łącznie: <?= $e((string) $totalCount) ?></div>
</div> </div>
</div> </div>
<form method="get" action="/settings/accounting/invoices/issued" class="mt-12"> <form method="get" action="/settings/accounting/invoices/issued" class="mt-12">
<div class="form-grid-2"> <div class="form-grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label" for="search">Szukaj (numer, zamowienie)</label> <label class="form-label" for="search">Szukaj (numer, zamówienie)</label>
<input type="text" name="search" id="search" class="form-control" value="<?= $e((string) ($filtersData['search'] ?? '')) ?>"> <input type="text" name="search" id="search" class="form-control" value="<?= $e((string) ($filtersData['search'] ?? '')) ?>">
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -68,7 +68,7 @@ $perPageNum = (int) ($perPage ?? 50);
<th>Brutto</th> <th>Brutto</th>
<th>Tryb</th> <th>Tryb</th>
<th>Konfiguracja</th> <th>Konfiguracja</th>
<th>Zamowienie</th> <th>Zamówienie</th>
<th>Akcje</th> <th>Akcje</th>
</tr> </tr>
</thead> </thead>

View File

@@ -8,12 +8,12 @@ $actions = $rule !== null && is_array($rule['actions'] ?? null) ? $rule['actions
$eventLabels = [ $eventLabels = [
'receipt.created' => 'Utworzono paragon', 'receipt.created' => 'Utworzono paragon',
'shipment.created' => 'Utworzenie przesylki', 'shipment.created' => 'Utworzenie przesyłki',
'shipment.status_changed' => 'Zmiana statusu przesylki', 'shipment.status_changed' => 'Zmiana statusu przesyłki',
'payment.status_changed' => 'Zmiana statusu platnosci', 'payment.status_changed' => 'Zmiana statusu płatności',
'order.status_changed' => 'Zmiana statusu zamowienia', 'order.status_changed' => 'Zmiana statusu zamówienia',
'order.status_aged' => 'Minelo X dni od zmiany statusu', 'order.status_aged' => 'Minelo X dni od zmiany statusu',
'order.imported' => 'Pobranie zamowienia', 'order.imported' => 'Pobranie zamówienia',
]; ];
$recipientLabels = [ $recipientLabels = [
@@ -26,11 +26,11 @@ $receiptIssueDateModes = is_array($receiptIssueDateModes ?? null) ? $receiptIssu
$receiptDuplicatePolicies = is_array($receiptDuplicatePolicies ?? null) ? $receiptDuplicatePolicies : []; $receiptDuplicatePolicies = is_array($receiptDuplicatePolicies ?? null) ? $receiptDuplicatePolicies : [];
$receiptIssueDateModeLabels = [ $receiptIssueDateModeLabels = [
'today' => 'Data dzisiejsza', 'today' => 'Data dzisiejsza',
'order_date' => 'Data zamowienia', 'order_date' => 'Data zamówienia',
'payment_date' => 'Data platnosci (fallback: dzisiaj)', 'payment_date' => 'Data płatności (fallback: dzisiaj)',
]; ];
$receiptDuplicatePolicyLabels = [ $receiptDuplicatePolicyLabels = [
'skip_if_exists' => 'Pomin jesli paragon juz istnieje', 'skip_if_exists' => 'Pomin jesli paragon już istnieje',
'allow_duplicates' => 'Wystawiaj kolejne paragony', 'allow_duplicates' => 'Wystawiaj kolejne paragony',
]; ];
$shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentStatusOptions : []; $shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentStatusOptions : [];
@@ -53,7 +53,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
<div class="form-grid-2"> <div class="form-grid-2">
<label class="form-field"> <label class="form-field">
<span class="field-label">Nazwa zadania *</span> <span class="field-label">Nazwa zadania *</span>
<input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e((string) ($rule['name'] ?? '')) ?>" placeholder="np. Paragon Allegro - wyslij e-mail"> <input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e((string) ($rule['name'] ?? '')) ?>" placeholder="np. Paragon Allegro - wyślij e-mail">
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Zdarzenie *</span> <span class="field-label">Zdarzenie *</span>
@@ -82,10 +82,10 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
<div class="automation-row__fields"> <div class="automation-row__fields">
<select class="form-control automation-row__type" name="conditions[<?= $idx ?>][type]" onchange="window.AutomationForm.onConditionTypeChange(this)"> <select class="form-control automation-row__type" name="conditions[<?= $idx ?>][type]" onchange="window.AutomationForm.onConditionTypeChange(this)">
<option value="integration"<?= ((string) ($cond['condition_type'] ?? '')) === 'integration' ? ' selected' : '' ?>>Integracja (kanal sprzedazy)</option> <option value="integration"<?= ((string) ($cond['condition_type'] ?? '')) === 'integration' ? ' selected' : '' ?>>Integracja (kanal sprzedazy)</option>
<option value="shipment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'shipment_status' ? ' selected' : '' ?>>Status przesylki</option> <option value="shipment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'shipment_status' ? ' selected' : '' ?>>Status przesyłki</option>
<option value="payment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_status' ? ' selected' : '' ?>>Status platnosci</option> <option value="payment_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_status' ? ' selected' : '' ?>>Status płatności</option>
<option value="payment_method"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_method' ? ' selected' : '' ?>>Metoda platnosci</option> <option value="payment_method"<?= ((string) ($cond['condition_type'] ?? '')) === 'payment_method' ? ' selected' : '' ?>>Metoda płatności</option>
<option value="order_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'order_status' ? ' selected' : '' ?>>Status zamowienia</option> <option value="order_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'order_status' ? ' selected' : '' ?>>Status zamówienia</option>
<option value="days_in_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'days_in_status' ? ' selected' : '' ?>>Liczba dni w statusie</option> <option value="days_in_status"<?= ((string) ($cond['condition_type'] ?? '')) === 'days_in_status' ? ' selected' : '' ?>>Liczba dni w statusie</option>
</select> </select>
<div class="automation-row__config"> <div class="automation-row__config">
@@ -165,10 +165,10 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
<div class="automation-row mt-8" data-index="<?= $idx ?>"> <div class="automation-row mt-8" data-index="<?= $idx ?>">
<div class="automation-row__fields"> <div class="automation-row__fields">
<select class="form-control automation-row__type" name="actions[<?= $idx ?>][type]" onchange="window.AutomationForm.onActionTypeChange(this)"> <select class="form-control automation-row__type" name="actions[<?= $idx ?>][type]" onchange="window.AutomationForm.onActionTypeChange(this)">
<option value="send_email"<?= ((string) ($act['action_type'] ?? '')) === 'send_email' ? ' selected' : '' ?>>Wyslij e-mail</option> <option value="send_email"<?= ((string) ($act['action_type'] ?? '')) === 'send_email' ? ' selected' : '' ?>>Wyślij e-mail</option>
<option value="issue_receipt"<?= ((string) ($act['action_type'] ?? '')) === 'issue_receipt' ? ' selected' : '' ?>>Wystaw paragon</option> <option value="issue_receipt"<?= ((string) ($act['action_type'] ?? '')) === 'issue_receipt' ? ' selected' : '' ?>>Wystaw paragon</option>
<option value="update_shipment_status"<?= ((string) ($act['action_type'] ?? '')) === 'update_shipment_status' ? ' selected' : '' ?>>Zmiana statusu przesylki</option> <option value="update_shipment_status"<?= ((string) ($act['action_type'] ?? '')) === 'update_shipment_status' ? ' selected' : '' ?>>Zmiana statusu przesyłki</option>
<option value="update_order_status"<?= ((string) ($act['action_type'] ?? '')) === 'update_order_status' ? ' selected' : '' ?>>Zmiana statusu zamowienia</option> <option value="update_order_status"<?= ((string) ($act['action_type'] ?? '')) === 'update_order_status' ? ' selected' : '' ?>>Zmiana statusu zamówienia</option>
</select> </select>
<div class="automation-row__config"> <div class="automation-row__config">
<?php <?php
@@ -202,7 +202,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
</select> </select>
<?php elseif ($actionType === 'update_shipment_status'): ?> <?php elseif ($actionType === 'update_shipment_status'): ?>
<select class="form-control" name="actions[<?= $idx ?>][shipment_status_key]"> <select class="form-control" name="actions[<?= $idx ?>][shipment_status_key]">
<option value="">-- Wybierz docelowy status przesylki --</option> <option value="">-- Wybierz docelowy status przesyłki --</option>
<?php foreach ($shipmentStatusOptions as $statusKey => $statusConfig): ?> <?php foreach ($shipmentStatusOptions as $statusKey => $statusConfig): ?>
<?php $statusLabel = (string) ($statusConfig['label'] ?? $statusKey); ?> <?php $statusLabel = (string) ($statusConfig['label'] ?? $statusKey); ?>
<option value="<?= $e((string) $statusKey) ?>"<?= ((string) ($actConfig['status_key'] ?? '')) === (string) $statusKey ? ' selected' : '' ?>> <option value="<?= $e((string) $statusKey) ?>"<?= ((string) ($actConfig['status_key'] ?? '')) === (string) $statusKey ? ' selected' : '' ?>>
@@ -212,7 +212,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
</select> </select>
<?php elseif ($actionType === 'update_order_status'): ?> <?php elseif ($actionType === 'update_order_status'): ?>
<select class="form-control" name="actions[<?= $idx ?>][order_status_code]"> <select class="form-control" name="actions[<?= $idx ?>][order_status_code]">
<option value="">-- Wybierz docelowy status zamowienia --</option> <option value="">-- Wybierz docelowy status zamówienia --</option>
<?php foreach ($orderStatusOptions as $statusOption): ?> <?php foreach ($orderStatusOptions as $statusOption): ?>
<?php <?php
$statusCode = (string) ($statusOption['code'] ?? ''); $statusCode = (string) ($statusOption['code'] ?? '');
@@ -237,7 +237,7 @@ $orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOption
</select> </select>
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" name="actions[<?= $idx ?>][send_once_per_order]" value="1"<?= ((int) ($actConfig['send_once_per_order'] ?? 0)) === 1 ? ' checked' : '' ?>> <input type="checkbox" name="actions[<?= $idx ?>][send_once_per_order]" value="1"<?= ((int) ($actConfig['send_once_per_order'] ?? 0)) === 1 ? ' checked' : '' ?>>
Wyslij tylko raz dla tego zamowienia Wyślij tylko raz dla tego zamówienia
</label> </label>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -13,17 +13,17 @@ $historyTotal = max(0, (int) ($historyPagination['total'] ?? 0));
$eventLabels = [ $eventLabels = [
'receipt.created' => 'Utworzono paragon', 'receipt.created' => 'Utworzono paragon',
'shipment.created' => 'Utworzenie przesylki', 'shipment.created' => 'Utworzenie przesyłki',
'shipment.status_changed' => 'Zmiana statusu przesylki', 'shipment.status_changed' => 'Zmiana statusu przesyłki',
'payment.status_changed' => 'Zmiana statusu platnosci', 'payment.status_changed' => 'Zmiana statusu płatności',
'order.status_changed' => 'Zmiana statusu zamowienia', 'order.status_changed' => 'Zmiana statusu zamówienia',
'order.status_aged' => 'Minelo X dni od zmiany statusu', 'order.status_aged' => 'Minelo X dni od zmiany statusu',
'order.imported' => 'Pobranie zamowienia', 'order.imported' => 'Pobranie zamówienia',
]; ];
$statusLabels = [ $statusLabels = [
'success' => 'Sukces', 'success' => 'Sukces',
'failed' => 'Blad', 'failed' => 'Błąd',
]; ];
$historyFiltersDefault = [ $historyFiltersDefault = [
@@ -114,7 +114,7 @@ $buildHistoryUrl = static function (array $overrides = []) use ($historyFiltersD
<form action="/settings/automation/delete" method="post" class="automation-inline-form js-confirm-delete"> <form action="/settings/automation/delete" method="post" class="automation-inline-form js-confirm-delete">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= (int) ($rule['id'] ?? 0) ?>"> <input type="hidden" name="id" value="<?= (int) ($rule['id'] ?? 0) ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -168,7 +168,7 @@ $buildHistoryUrl = static function (array $overrides = []) use ($historyFiltersD
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">ID zamowienia</span> <span class="field-label">ID zamówienia</span>
<input class="form-control" type="number" min="1" step="1" name="history_order_id" value="<?= (int) ($historyFilters['order_id'] ?? 0) > 0 ? $e((string) (int) ($historyFilters['order_id'] ?? 0)) : '' ?>"> <input class="form-control" type="number" min="1" step="1" name="history_order_id" value="<?= (int) ($historyFilters['order_id'] ?? 0) > 0 ? $e((string) (int) ($historyFilters['order_id'] ?? 0)) : '' ?>">
</label> </label>
@@ -195,7 +195,7 @@ $buildHistoryUrl = static function (array $overrides = []) use ($historyFiltersD
<th>Kiedy</th> <th>Kiedy</th>
<th>Zdarzenie</th> <th>Zdarzenie</th>
<th>Regula</th> <th>Regula</th>
<th>Zamowienie</th> <th>Zamówienie</th>
<th>Status</th> <th>Status</th>
<th>Wynik</th> <th>Wynik</th>
</tr> </tr>
@@ -226,7 +226,7 @@ $buildHistoryUrl = static function (array $overrides = []) use ($historyFiltersD
<?php if ($entryStatus === 'success'): ?> <?php if ($entryStatus === 'success'): ?>
<span class="badge badge--success">Sukces</span> <span class="badge badge--success">Sukces</span>
<?php elseif ($entryStatus === 'failed'): ?> <?php elseif ($entryStatus === 'failed'): ?>
<span class="badge badge--danger">Blad</span> <span class="badge badge--danger">Błąd</span>
<?php else: ?> <?php else: ?>
<span class="badge badge--muted"><?= $e($entryStatus) ?></span> <span class="badge badge--muted"><?= $e($entryStatus) ?></span>
<?php endif; ?> <?php endif; ?>
@@ -290,7 +290,7 @@ document.addEventListener('DOMContentLoaded', function() {
try { try {
window.localStorage.setItem(tabStorageKey, target); window.localStorage.setItem(tabStorageKey, target);
} catch (error) { } catch (error) {
// Ignorujemy brak dostepu do localStorage. // Ignorujemy brak dostępu do localStorage.
} }
} }
} }
@@ -317,7 +317,7 @@ document.addEventListener('DOMContentLoaded', function() {
activateTab(savedTab, false); activateTab(savedTab, false);
} }
} catch (error) { } catch (error) {
// Ignorujemy brak dostepu do localStorage. // Ignorujemy brak dostępu do localStorage.
} }
} else if (explicitTab === 'history') { } else if (explicitTab === 'history') {
activateTab('automation-tab-history', true); activateTab('automation-tab-history', true);
@@ -338,7 +338,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') { if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
window.OrderProAlerts.confirm( window.OrderProAlerts.confirm(
'Usuwanie zadania', 'Usuwanie zadania',
'Czy na pewno chcesz usunac to zadanie automatyczne?', 'Czy na pewno chcesz usuńąć to zadanie automatyczne?',
function() { form.submit(); } function() { form.submit(); }
); );
return; return;

View File

@@ -114,7 +114,7 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string
</label> </label>
<?php endforeach; ?> <?php endforeach; ?>
<div class="table-col-toggle-footer"> <div class="table-col-toggle-footer">
<button type="button" class="btn btn--secondary js-col-toggle-reset">Pokaz wszystkie</button> <button type="button" class="btn btn--secondary js-col-toggle-reset">Pokaż wszystkie</button>
</div> </div>
</div> </div>
</div> </div>
@@ -316,7 +316,7 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
<input type="hidden" name="page" value="1"> <input type="hidden" name="page" value="1">
<span>Wyswietlaj</span> <span>Wyświetlaj</span>
<select class="form-control js-per-page-select" name="per_page"> <select class="form-control js-per-page-select" name="per_page">
<?php foreach ($perPageOptions as $opt): ?> <?php foreach ($perPageOptions as $opt): ?>
<option value="<?= $e((string) $opt) ?>"<?= (int) $opt === $perPage ? ' selected' : '' ?>><?= $e((string) $opt) ?></option> <option value="<?= $e((string) $opt) ?>"<?= (int) $opt === $perPage ? ' selected' : '' ?>><?= $e((string) $opt) ?></option>

View File

@@ -76,7 +76,7 @@
body.innerHTML = html; body.innerHTML = html;
}) })
.catch(function () { .catch(function () {
body.innerHTML = '<div class="order-preview-loading">Nie udalo sie zaladowac podgladu.</div>'; body.innerHTML = '<div class="order-preview-loading">Nie udało się załadować podglądu.</div>';
}); });
} }

View File

@@ -24,7 +24,7 @@ $emailEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $buy
<div class="email-send-overlay" id="emailSendOverlay" style="display:none"> <div class="email-send-overlay" id="emailSendOverlay" style="display:none">
<div class="email-send-modal"> <div class="email-send-modal">
<div class="email-send-modal__header"> <div class="email-send-modal__header">
<h3>Wyslij e-mail</h3> <h3>Wyślij e-mail</h3>
<button type="button" class="email-send-modal__close" id="emailSendClose">&times;</button> <button type="button" class="email-send-modal__close" id="emailSendClose">&times;</button>
</div> </div>
<div class="email-send-modal__body"> <div class="email-send-modal__body">
@@ -64,7 +64,7 @@ $emailEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $buy
</div> </div>
<div class="email-send-modal__footer"> <div class="email-send-modal__footer">
<button type="button" class="btn btn--secondary" id="emailSendCancel">Anuluj</button> <button type="button" class="btn btn--secondary" id="emailSendCancel">Anuluj</button>
<button type="button" class="btn btn--primary" id="emailSendBtn" disabled>Wyslij</button> <button type="button" class="btn btn--primary" id="emailSendBtn" disabled>Wyślij</button>
</div> </div>
</div> </div>
</div> </div>
@@ -128,14 +128,14 @@ $emailEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $buy
previewBody.innerHTML = data.body_html || ''; previewBody.innerHTML = data.body_html || '';
if (data.attachments && data.attachments.length > 0) { if (data.attachments && data.attachments.length > 0) {
previewAttachments.style.display = 'block'; previewAttachments.style.display = 'block';
previewAttachments.textContent = 'Zalaczniki: ' + data.attachments.join(', '); previewAttachments.textContent = 'Załączniki: ' + data.attachments.join(', ');
} else { } else {
previewAttachments.style.display = 'none'; previewAttachments.style.display = 'none';
} }
previewArea.style.display = 'block'; previewArea.style.display = 'block';
}) })
.catch(function () { .catch(function () {
previewBody.textContent = 'Blad ladowania podgladu'; previewBody.textContent = 'Błąd ładowania podglądu';
previewArea.style.display = 'block'; previewArea.style.display = 'block';
}) })
.finally(function () { .finally(function () {
@@ -149,7 +149,7 @@ $emailEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $buy
if (!tplId) return; if (!tplId) return;
sendBtn.disabled = true; sendBtn.disabled = true;
sendBtn.textContent = 'Wysylanie...'; sendBtn.textContent = 'Wysyłanie...';
var formData = new FormData(); var formData = new FormData();
formData.append('_token', csrfToken); formData.append('_token', csrfToken);
@@ -163,23 +163,23 @@ $emailEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $buy
if (data.success) { if (data.success) {
closeModal(); closeModal();
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.success(data.message || 'E-mail wyslany'); window.OrderProAlerts.success(data.message || 'E-mail wysłany');
} }
setTimeout(function () { location.reload(); }, 1500); setTimeout(function () { location.reload(); }, 1500);
} else { } else {
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.error(data.message || 'Blad wysylki'); window.OrderProAlerts.error(data.message || 'Błąd wysyłki');
} }
} }
}) })
.catch(function () { .catch(function () {
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.error('Blad polaczenia z serwerem'); window.OrderProAlerts.error('Błąd połączenia z serwerem');
} }
}) })
.finally(function () { .finally(function () {
sendBtn.disabled = false; sendBtn.disabled = false;
sendBtn.textContent = 'Wyslij'; sendBtn.textContent = 'Wyślij';
}); });
}); });
})(); })();

View File

@@ -114,7 +114,7 @@ $copyBtn = static function (string $value) use ($e, $copyIcon, $checkIcon): stri
<thead> <thead>
<tr> <tr>
<th>Produkt</th> <th>Produkt</th>
<th>Ilosc</th> <th>Ilość</th>
<th>Cena</th> <th>Cena</th>
</tr> </tr>
</thead> </thead>
@@ -182,7 +182,7 @@ $copyBtn = static function (string $value) use ($e, $copyIcon, $checkIcon): stri
$paymentType = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? ''))); $paymentType = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
if ($paymentType !== ''): if ($paymentType !== ''):
?> ?>
<dt>Platnosc:</dt> <dt>Płatność:</dt>
<dd><?= $e($paymentType) ?></dd> <dd><?= $e($paymentType) ?></dd>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -103,7 +103,7 @@ $hasExistingReceipts = $existingReceiptsList !== [];
<?php if ($deliveryPriceVal > 0): ?> <?php if ($deliveryPriceVal > 0): ?>
<tr> <tr>
<td><?= $e((string) (count($itemsList) + 1)) ?></td> <td><?= $e((string) (count($itemsList) + 1)) ?></td>
<td>Koszt wysylki</td> <td>Koszt wysyłki</td>
<td><div>-</div><div class="muted">-</div></td> <td><div>-</div><div class="muted">-</div></td>
<td>1</td> <td>1</td>
<td><?= $e(number_format($deliveryPriceVal, 2, '.', ' ')) ?></td> <td><?= $e(number_format($deliveryPriceVal, 2, '.', ' ')) ?></td>

View File

@@ -73,15 +73,15 @@ foreach ($addressesList as $address) {
</div> </div>
<div class="order-details-actions"> <div class="order-details-actions">
<button type="button" class="btn btn--secondary btn--disabled">Strefa klienta</button> <button type="button" class="btn btn--secondary btn--disabled">Strefa klienta</button>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--primary">Przygotuj przesylke</a> <a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--primary">Przygotuj przesyłkę</a>
<?php if ($receiptConfigsList !== []): ?> <?php if ($receiptConfigsList !== []): ?>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/create" class="btn btn--secondary">Wystaw paragon</a> <a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/create" class="btn btn--secondary">Wystaw paragon</a>
<?php endif; ?> <?php endif; ?>
<span data-invoice-button-wrap style="<?= $invoiceRequestedFlag ? '' : 'display:none;' ?>"> <span data-invoice-button-wrap style="<?= $invoiceRequestedFlag ? '' : 'display:none;' ?>">
<?php if ($invoiceConfigsList !== []): ?> <?php if ($invoiceConfigsList !== []): ?>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/invoice/create" class="btn btn--secondary">Wystaw fakture</a> <a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/invoice/create" class="btn btn--secondary">Wystaw fakturę</a>
<?php else: ?> <?php else: ?>
<button type="button" class="btn btn--secondary btn--disabled" title="Brak aktywnych konfiguracji faktur">Wystaw fakture</button> <button type="button" class="btn btn--secondary btn--disabled" title="Brak aktywnych konfiguracji faktur">Wystaw fakturę</button>
<?php endif; ?> <?php endif; ?>
</span> </span>
<?php <?php
@@ -92,11 +92,11 @@ foreach ($addressesList as $address) {
$emailBtnEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $emailBuyerAddr !== ''; $emailBtnEnabled = $emailTemplatesList !== [] && $emailMailboxesList !== [] && $emailBuyerAddr !== '';
?> ?>
<?php if ($emailBtnEnabled): ?> <?php if ($emailBtnEnabled): ?>
<button type="button" class="btn btn--secondary" id="btn-send-email">Wyslij e-mail</button> <button type="button" class="btn btn--secondary" id="btn-send-email">Wyślij e-mail</button>
<?php else: ?> <?php else: ?>
<button type="button" class="btn btn--secondary btn--disabled" title="Skonfiguruj skrzynke i szablony w Ustawieniach">Wyslij e-mail</button> <button type="button" class="btn btn--secondary btn--disabled" title="Skonfiguruj skrzynke i szablony w Ustawieniach">Wyślij e-mail</button>
<?php endif; ?> <?php endif; ?>
<button type="button" class="btn btn--secondary" id="btn-header-payment">Platnosc</button> <button type="button" class="btn btn--secondary" id="btn-header-payment">Płatność</button>
<button type="button" class="btn btn--secondary btn--disabled">Drukuj</button> <button type="button" class="btn btn--secondary btn--disabled">Drukuj</button>
<button type="button" class="btn btn--primary btn--disabled">Pakuj</button> <button type="button" class="btn btn--primary btn--disabled">Pakuj</button>
<button type="button" class="btn btn--secondary btn--disabled">Edytuj</button> <button type="button" class="btn btn--secondary btn--disabled">Edytuj</button>
@@ -111,13 +111,13 @@ foreach ($addressesList as $address) {
<?php $riskOrders = is_array($riskInfo['orders'] ?? null) ? $riskInfo['orders'] : []; ?> <?php $riskOrders = is_array($riskInfo['orders'] ?? null) ? $riskInfo['orders'] : []; ?>
<?php if ($riskOrders !== []): ?> <?php if ($riskOrders !== []): ?>
<details class="customer-risk-banner__list"> <details class="customer-risk-banner__list">
<summary>Pokaz liste zamowien ze zwrotem (<?= count($riskOrders) ?>)</summary> <summary>Pokaż listę zamówień ze zwrotem (<?= count($riskOrders) ?>)</summary>
<table class="customer-risk-banner__table"> <table class="customer-risk-banner__table">
<thead> <thead>
<tr> <tr>
<th>Nr zamowienia</th> <th>Nr zamówienia</th>
<th>Data</th> <th>Data</th>
<th>Nr przesylki</th> <th>Nr przesyłki</th>
<th>Przewoznik</th> <th>Przewoznik</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
@@ -276,7 +276,7 @@ foreach ($addressesList as $address) {
<h3 class="section-title"><?= $e($t('orders.details.order_info')) ?></h3> <h3 class="section-title"><?= $e($t('orders.details.order_info')) ?></h3>
<dl class="order-kv mt-12"> <dl class="order-kv mt-12">
<dt><?= $e($t('orders.details.fields.status')) ?></dt><dd><?= $e((string) ($statusLabel ?? '-')) ?></dd> <dt><?= $e($t('orders.details.fields.status')) ?></dt><dd><?= $e((string) ($statusLabel ?? '-')) ?></dd>
<dt>Nr zamowienia</dt><dd><strong><?= $e((string) ($orderRow['internal_order_number'] ?? '-')) ?></strong></dd> <dt>Nr zamówienia</dt><dd><strong><?= $e((string) ($orderRow['internal_order_number'] ?? '-')) ?></strong></dd>
<dt><?= $e($t('orders.details.fields.external_order_id')) ?></dt><dd><?= $e((string) ($orderRow['external_order_id'] ?? '-')) ?></dd> <dt><?= $e($t('orders.details.fields.external_order_id')) ?></dt><dd><?= $e((string) ($orderRow['external_order_id'] ?? '-')) ?></dd>
<dt><?= $e($t('orders.details.fields.ordered_at')) ?></dt><dd><?= $e((string) ($orderRow['ordered_at'] ?? '-')) ?></dd> <dt><?= $e($t('orders.details.fields.ordered_at')) ?></dt><dd><?= $e((string) ($orderRow['ordered_at'] ?? '-')) ?></dd>
<dt><?= $e($t('orders.details.fields.customer_login')) ?></dt><dd><?= $e((string) ($orderRow['customer_login'] ?? '-')) ?></dd> <dt><?= $e($t('orders.details.fields.customer_login')) ?></dt><dd><?= $e((string) ($orderRow['customer_login'] ?? '-')) ?></dd>
@@ -351,7 +351,7 @@ foreach ($addressesList as $address) {
<dt><?= $e($t('orders.details.fields.payment_status')) ?></dt><dd><?= $e((string) ($orderRow['payment_status'] ?? '-')) ?></dd> <dt><?= $e($t('orders.details.fields.payment_status')) ?></dt><dd><?= $e((string) ($orderRow['payment_status'] ?? '-')) ?></dd>
<dt>Forma dostawy</dt><dd><?= $e($deliveryMethodValue !== '' ? $deliveryMethodValue : '-') ?></dd> <dt>Forma dostawy</dt><dd><?= $e($deliveryMethodValue !== '' ? $deliveryMethodValue : '-') ?></dd>
<dt>Forma płatności</dt><dd><?= $e($paymentMethodValue !== '' ? $paymentMethodValue : '-') ?></dd> <dt>Forma płatności</dt><dd><?= $e($paymentMethodValue !== '' ? $paymentMethodValue : '-') ?></dd>
<dt>Typ platnosci</dt> <dt>Typ płatności</dt>
<dd> <dd>
<?php <?php
$paymentTypeRaw = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? ''))); $paymentTypeRaw = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
@@ -361,7 +361,7 @@ foreach ($addressesList as $address) {
'POBRANIE' => 'Za pobraniem', 'POBRANIE' => 'Za pobraniem',
'ZA POBRANIEM' => 'Za pobraniem', 'ZA POBRANIEM' => 'Za pobraniem',
'PŁATNOŚĆ PRZY ODBIORZE' => 'Za pobraniem', 'PŁATNOŚĆ PRZY ODBIORZE' => 'Za pobraniem',
'ONLINE' => 'Platnosc online', 'ONLINE' => 'Płatność online',
'TRANSFER' => 'Przelew', 'TRANSFER' => 'Przelew',
]; ];
$paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-'); $paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-');
@@ -618,7 +618,7 @@ foreach ($addressesList as $address) {
<div class="order-tab-panel" data-order-tab-panel="shipments"> <div class="order-tab-panel" data-order-tab-panel="shipments">
<?php if ($packagesList !== []): ?> <?php if ($packagesList !== []): ?>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Wygenerowane przesylki</h3> <h3 class="section-title">Wygenerowane przesyłki</h3>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
@@ -656,7 +656,7 @@ foreach ($addressesList as $address) {
<td><?= $e((string) ($pkg['id'] ?? '')) ?></td> <td><?= $e((string) ($pkg['id'] ?? '')) ?></td>
<td> <td>
<?php if ($isManual): ?> <?php if ($isManual): ?>
<span class="order-tag is-neutral">Dodana recznie</span> <span class="order-tag is-neutral">Dodana ręcznie</span>
<?php else: ?> <?php else: ?>
<span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>" <span class="order-tag <?= $pkgStatus === 'label_ready' || $pkgStatus === 'created' ? 'is-success' : ($pkgStatus === 'error' ? 'is-danger' : 'is-warn') ?>"
data-pkg-status-tag="<?= $e((string) ($pkg['id'] ?? 0)) ?>"> data-pkg-status-tag="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
@@ -667,7 +667,7 @@ foreach ($addressesList as $address) {
data-check-pkg-status="<?= $e((string) ($pkg['id'] ?? 0)) ?>" data-check-pkg-status="<?= $e((string) ($pkg['id'] ?? 0)) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
data-auto-poll="1" data-auto-poll="1"
style="font-size:0.7rem">Sprawdz status</button> style="font-size:0.7rem">Sprawdź status</button>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<?php if ($pkgError !== ''): ?> <?php if ($pkgError !== ''): ?>
@@ -691,7 +691,7 @@ foreach ($addressesList as $address) {
<td style="white-space:nowrap" data-pkg-tracking-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>"> <td style="white-space:nowrap" data-pkg-tracking-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
<?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php <?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php
$pkgTrackUrl = DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId); $pkgTrackUrl = DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId);
if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesylke">&#128279;</a><?php endif; ?> if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesyłkę">&#128279;</a><?php endif; ?>
</td> </td>
<td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td> <td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
<td data-pkg-label-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>"> <td data-pkg-label-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
@@ -718,7 +718,7 @@ foreach ($addressesList as $address) {
class="btn btn--sm btn--secondary btn-print-label" class="btn btn--sm btn--secondary btn-print-label"
data-package-id="<?= $e((string) ($pkg['id'] ?? 0)) ?>" data-package-id="<?= $e((string) ($pkg['id'] ?? 0)) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
title="Wyslij do drukarki">Drukuj</button> title="Wyślij do drukarki">Drukuj</button>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
</span> </span>
@@ -728,7 +728,7 @@ foreach ($addressesList as $address) {
<td> <td>
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) ($pkg['id'] ?? 0)) ?>/delete" class="form-delete-package" style="display:inline"> <form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) ($pkg['id'] ?? 0)) ?>/delete" class="form-delete-package" style="display:inline">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<button type="submit" class="btn btn--sm btn--danger btn-delete-package">Usun</button> <button type="submit" class="btn btn--sm btn--danger btn-delete-package">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -741,7 +741,7 @@ foreach ($addressesList as $address) {
<?php if ($shipmentsList !== []): ?> <?php if ($shipmentsList !== []): ?>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Wysylki z Allegro</h3> <h3 class="section-title">Wysyłki z Allegro</h3>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
@@ -768,15 +768,15 @@ foreach ($addressesList as $address) {
<?php if ($packagesList === [] && $shipmentsList === []): ?> <?php if ($packagesList === [] && $shipmentsList === []): ?>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title"><?= $e($t('orders.details.tabs.shipments')) ?></h3> <h3 class="section-title"><?= $e($t('orders.details.tabs.shipments')) ?></h3>
<p class="muted mt-12">Brak przesylek dla tego zamowienia.</p> <p class="muted mt-12">Brak przesyłek dla tego zamówienia.</p>
</section> </section>
<?php endif; ?> <?php endif; ?>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Dodaj reczny numer przesylki</h3> <h3 class="section-title">Dodaj reczny numer przesyłki</h3>
<form method="POST" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/manual" class="manual-tracking-form mt-12"> <form method="POST" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/manual" class="manual-tracking-form mt-12">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="text" name="tracking_number" placeholder="Nr przesylki" required class="form-control"> <input type="text" name="tracking_number" placeholder="Nr przesyłki" required class="form-control">
<input type="text" name="carrier_name" placeholder="Przewoznik (opcjonalnie)" class="form-control"> <input type="text" name="carrier_name" placeholder="Przewoznik (opcjonalnie)" class="form-control">
<button type="submit" class="btn btn--primary">Dodaj</button> <button type="submit" class="btn btn--primary">Dodaj</button>
</form> </form>
@@ -794,9 +794,9 @@ foreach ($addressesList as $address) {
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
data-csrf-token="<?= $e((string) ($csrfToken ?? '')) ?>" data-csrf-token="<?= $e((string) ($csrfToken ?? '')) ?>"
<?= $invoiceRequestedFlag ? 'checked' : '' ?>> <?= $invoiceRequestedFlag ? 'checked' : '' ?>>
<span><strong>Klient prosi o fakture</strong></span> <span><strong>Klient prosi o fakturę</strong></span>
</label> </label>
<span class="muted" style="font-size:12px;">Po zaznaczeniu pojawi sie przycisk "Wystaw fakture" w naglowku zamowienia.</span> <span class="muted" style="font-size:12px;">Po zaznaczeniu pojawi się przycisk "Wystaw fakturę" w nagłówku zamówienia.</span>
</div> </div>
<?php <?php
@@ -1012,7 +1012,7 @@ foreach ($addressesList as $address) {
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if ($documentsList !== []): ?> <?php if ($documentsList !== []): ?>
<h4 class="section-title mt-12">Dokumenty zewnetrzne</h4> <h4 class="section-title mt-12">Dokumenty zewnętrzne</h4>
<div class="table-wrap mt-8"> <div class="table-wrap mt-8">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
@@ -1136,12 +1136,12 @@ foreach ($addressesList as $address) {
var queryTab = ''; var queryTab = '';
try { queryTab = new URLSearchParams(window.location.search).get('tab') || ''; } catch (e) {} try { queryTab = new URLSearchParams(window.location.search).get('tab') || ''; } catch (e) {}
var forceTab = queryTab || <?= json_encode($flashSuccessMsg !== '' && strpos($flashSuccessMsg, 'Przesylka') !== false ? 'shipments' : '') ?>; var forceTab = queryTab || <?= json_encode($flashSuccessMsg !== '' && strpos($flashSuccessMsg, 'Przesyłka') !== false ? 'shipments' : '') ?>;
var savedTab = null; var savedTab = null;
try { savedTab = localStorage.getItem(storageKey); } catch (e) {} try { savedTab = localStorage.getItem(storageKey); } catch (e) {}
setActiveTab(forceTab || savedTab || 'details'); setActiveTab(forceTab || savedTab || 'details');
// Header "Platnosc" button — switch to payments tab and open form // Header "Płatność" button — switch to payments tab and open form
var btnHeaderPayment = document.getElementById('btn-header-payment'); var btnHeaderPayment = document.getElementById('btn-header-payment');
if (btnHeaderPayment) { if (btnHeaderPayment) {
btnHeaderPayment.addEventListener('click', function () { btnHeaderPayment.addEventListener('click', function () {
@@ -1166,7 +1166,7 @@ foreach ($addressesList as $address) {
btn.classList.remove('js-print-queue-pending'); btn.classList.remove('js-print-queue-pending');
btn.classList.add('btn--secondary'); btn.classList.add('btn--secondary');
btn.classList.add('btn-print-label'); btn.classList.add('btn-print-label');
btn.setAttribute('title', 'Wyslij do drukarki'); btn.setAttribute('title', 'Wyślij do drukarki');
} }
function stopPrintQueuePoll() { function stopPrintQueuePoll() {
@@ -1212,7 +1212,7 @@ foreach ($addressesList as $address) {
if (!packageId) return; if (!packageId) return;
btn.disabled = true; btn.disabled = true;
var originalText = btn.innerHTML; var originalText = btn.innerHTML;
btn.innerHTML = 'Wysylam...'; btn.innerHTML = 'Wysyłam...';
var csrfInput = document.querySelector('input[name="_token"]'); var csrfInput = document.querySelector('input[name="_token"]');
var csrf = csrfInput ? csrfInput.value : '<?= $e($csrfToken ?? '') ?>'; var csrf = csrfInput ? csrfInput.value : '<?= $e($csrfToken ?? '') ?>';
@@ -1232,25 +1232,25 @@ foreach ($addressesList as $address) {
btn.classList.add('js-print-queue-pending'); btn.classList.add('js-print-queue-pending');
watchPrintQueueButton(btn); watchPrintQueueButton(btn);
} else { } else {
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad'; var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany błąd';
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: msg, type: 'error' }); } if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: msg, type: 'error' }); }
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false; btn.disabled = false;
} }
}) })
.catch(function () { .catch(function () {
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: 'Blad sieci.', type: 'error' }); } if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: 'Błąd sieci.', type: 'error' }); }
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false; btn.disabled = false;
}); });
}); });
// Przy zaladowaniu strony: uruchom polling dla przyciskow juz w kolejce // Przy zaladowaniu strony: uruchom polling dla przyciskow już w kolejce
document.querySelectorAll('.js-print-queue-pending').forEach(function (btn) { document.querySelectorAll('.js-print-queue-pending').forEach(function (btn) {
watchPrintQueueButton(btn); watchPrintQueueButton(btn);
}); });
// Auto-click najnowszej etykiety po utworzeniu przesylki (?printLast=1) // Auto-click najnowszej etykiety po utworzeniu przesyłki (?printLast=1)
(function autoClickLastLabel() { (function autoClickLastLabel() {
if (!/[?&]printLast=1\b/.test(location.search)) return; if (!/[?&]printLast=1\b/.test(location.search)) return;
var attempts = 0; var attempts = 0;
@@ -1338,7 +1338,7 @@ foreach ($addressesList as $address) {
+ '<button type="submit" class="btn btn--sm btn--secondary">Pobierz</button></form>'; + '<button type="submit" class="btn btn--sm btn--secondary">Pobierz</button></form>';
} }
html += '<button type="button" class="btn btn--sm btn--secondary btn-print-label"' html += '<button type="button" class="btn btn--sm btn--secondary btn-print-label"'
+ ' data-package-id="' + pkgId + '" data-order-id="' + orderId + '" title="Wyslij do drukarki">Drukuj</button>'; + ' data-package-id="' + pkgId + '" data-order-id="' + orderId + '" title="Wyślij do drukarki">Drukuj</button>';
html += '</span>'; html += '</span>';
labelCell.innerHTML = html; labelCell.innerHTML = html;
} }
@@ -1362,7 +1362,7 @@ foreach ($addressesList as $address) {
errEl.setAttribute('data-pkg-error', pkgId); errEl.setAttribute('data-pkg-error', pkgId);
tag.parentNode.appendChild(errEl); tag.parentNode.appendChild(errEl);
} }
if (errEl) errEl.textContent = errorMsg || 'Blad tworzenia przesylki'; if (errEl) errEl.textContent = errorMsg || 'Błąd tworzenia przesyłki';
} }
function pollPackageStatus(pkgId, orderId, btn, attempt) { function pollPackageStatus(pkgId, orderId, btn, attempt) {
@@ -1382,7 +1382,7 @@ foreach ($addressesList as $address) {
pollPackageStatus(pkgId, orderId, btn, attempt + 1); pollPackageStatus(pkgId, orderId, btn, attempt + 1);
}, delay); }, delay);
} else if (btn) { } else if (btn) {
btn.textContent = 'Sprawdz ponownie'; btn.textContent = 'Sprawdź ponownie';
btn.disabled = false; btn.disabled = false;
} }
} else if (data.status === 'error') { } else if (data.status === 'error') {
@@ -1391,7 +1391,7 @@ foreach ($addressesList as $address) {
}) })
.catch(function () { .catch(function () {
if (btn) { if (btn) {
btn.textContent = 'Blad sieci'; btn.textContent = 'Błąd sieci';
btn.disabled = false; btn.disabled = false;
} }
}); });
@@ -1494,11 +1494,11 @@ document.querySelectorAll('.form-delete-package').forEach(function(form) {
e.preventDefault(); e.preventDefault();
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
title: 'Usuwanie przesylki', title: 'Usuwanie przesyłki',
message: 'Czy na pewno chcesz usunac te przesylke?', message: 'Czy na pewno chcesz usuńąć te przesyłkę?',
onConfirm: function() { form.submit(); } onConfirm: function() { form.submit(); }
}); });
} else if (confirm('Czy na pewno chcesz usunac te przesylke?')) { } else if (confirm('Czy na pewno chcesz usuńąć te przesyłkę?')) {
form.submit(); form.submit();
} }
}); });

View File

@@ -78,9 +78,9 @@ $totalGross = (float) ($receiptData['total_gross'] ?? 0);
<tr> <tr>
<th>Lp.</th> <th>Lp.</th>
<th>Nazwa</th> <th>Nazwa</th>
<th class="text-right">Ilosc</th> <th class="text-right">Ilość</th>
<th class="text-right">Cena</th> <th class="text-right">Cena</th>
<th class="text-right">Wartosc</th> <th class="text-right">Wartość</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -65,9 +65,9 @@ $totalGross = (float) ($receiptData['total_gross'] ?? 0);
<tr> <tr>
<th>Lp.</th> <th>Lp.</th>
<th>Nazwa</th> <th>Nazwa</th>
<th>Ilosc</th> <th>Ilość</th>
<th>Cena</th> <th>Cena</th>
<th>Wartosc</th> <th>Wartość</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -23,7 +23,7 @@ $error = trim((string) ($errorMessage ?? ''));
?> ?>
<section class="card"> <section class="card">
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Ksiegowosc</a> &raquo; <a href="/settings/accounting/invoices">Faktury</a></p> <p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Księgowość</a> &raquo; <a href="/settings/accounting/invoices">Faktury</a></p>
<h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji faktury' : 'Nowa konfiguracja faktury' ?></h2> <h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji faktury' : 'Nowa konfiguracja faktury' ?></h2>
<?php if ($error !== ''): ?> <?php if ($error !== ''): ?>
@@ -49,7 +49,7 @@ $error = trim((string) ($errorMessage ?? ''));
<label class="form-field"> <label class="form-field">
<span class="field-label">Format numeru *</span> <span class="field-label">Format numeru *</span>
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="FV/%N/%M/%Y" value="<?= $e($numberFormat) ?>"> <input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="FV/%N/%M/%Y" value="<?= $e($numberFormat) ?>">
<small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiac (01-12), <code>%Y</code> = rok (4 cyfry)</small> <small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiąc (01-12), <code>%Y</code> = rok (4 cyfry)</small>
</label> </label>
</div> </div>
@@ -65,30 +65,30 @@ $error = trim((string) ($errorMessage ?? ''));
<span class="field-label">Data sprzedazy</span> <span class="field-label">Data sprzedazy</span>
<select class="form-control" name="sale_date_source"> <select class="form-control" name="sale_date_source">
<option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option> <option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option>
<option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamowienia</option> <option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamówienia</option>
<option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data platnosci</option> <option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data płatności</option>
</select> </select>
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Numer referencyjny zamowienia</span> <span class="field-label">Numer referencyjny zamówienia</span>
<select class="form-control" name="order_reference"> <select class="form-control" name="order_reference">
<option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option> <option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option>
<option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option> <option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option>
<option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnetrzny</option> <option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnętrzny</option>
</select> </select>
</label> </label>
</div> </div>
<div class="form-grid-3 mt-0"> <div class="form-grid-3 mt-0">
<label class="form-field"> <label class="form-field">
<span class="field-label">Termin platnosci (dni)</span> <span class="field-label">Termin płatności (dni)</span>
<input class="form-control" type="number" name="payment_to_days" min="0" max="365" value="<?= $paymentToDays ?>"> <input class="form-control" type="number" name="payment_to_days" min="0" max="365" value="<?= $paymentToDays ?>">
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Typ dokumentu</span> <span class="field-label">Typ dokumentu</span>
<select class="form-control" name="default_kind"> <select class="form-control" name="default_kind">
<option value="vat"<?= $defaultKind === 'vat' ? ' selected' : '' ?>>Faktura VAT</option> <option value="vat"<?= $defaultKind === 'vat' ? ' selected' : '' ?>>Faktura VAT</option>
<option value="proforma"<?= $defaultKind === 'proforma' ? ' selected' : '' ?>>Proforma</option> <option value="proformą"<?= $defaultKind === 'proformą' ? ' selected' : '' ?>>Proformą</option>
<option value="invoice_other"<?= $defaultKind === 'invoice_other' ? ' selected' : '' ?>>Inna</option> <option value="invoice_other"<?= $defaultKind === 'invoice_other' ? ' selected' : '' ?>>Inna</option>
</select> </select>
</label> </label>

View File

@@ -6,9 +6,9 @@ $error = trim((string) ($errorMessage ?? ''));
?> ?>
<section class="card"> <section class="card">
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">&larr; Ksiegowosc</a></p> <p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">&larr; Księgowość</a></p>
<h2 class="section-title">Konfiguracje faktur</h2> <h2 class="section-title">Konfiguracje faktur</h2>
<p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania faktur. Mozesz dodac wiele konfiguracji (np. dla roznych dzialalnosci) i opcjonalnie delegowac wystawianie do Fakturowni.</p> <p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania faktur. Możesz dodać wiele konfiguracji (np. dla roznych dzialalnosci) i opcjonalnie delegowac wystawianie do Fakturowni.</p>
<?php if ($error !== ''): ?> <?php if ($error !== ''): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div>
@@ -84,7 +84,7 @@ $error = trim((string) ($errorMessage ?? ''));
<form action="/settings/accounting/invoices/delete" method="post" style="display:inline" class="js-confirm-delete"> <form action="/settings/accounting/invoices/delete" method="post" style="display:inline" class="js-confirm-delete">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $cid ?>"> <input type="hidden" name="id" value="<?= $cid ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>

View File

@@ -17,7 +17,7 @@ $error = trim((string) ($errorMessage ?? ''));
?> ?>
<section class="card"> <section class="card">
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Ksiegowosc</a> &raquo; <a href="/settings/accounting/receipts">Paragony</a></p> <p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Księgowość</a> &raquo; <a href="/settings/accounting/receipts">Paragony</a></p>
<h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji paragonu' : 'Nowa konfiguracja paragonu' ?></h2> <h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji paragonu' : 'Nowa konfiguracja paragonu' ?></h2>
<?php if ($error !== ''): ?> <?php if ($error !== ''): ?>
@@ -43,7 +43,7 @@ $error = trim((string) ($errorMessage ?? ''));
<label class="form-field"> <label class="form-field">
<span class="field-label">Format numeru *</span> <span class="field-label">Format numeru *</span>
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="PAR/%N/%M/%Y" value="<?= $e($numberFormat) ?>"> <input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="PAR/%N/%M/%Y" value="<?= $e($numberFormat) ?>">
<small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiac, <code>%Y</code> = rok</small> <small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiąc, <code>%Y</code> = rok</small>
</label> </label>
</div> </div>
@@ -59,16 +59,16 @@ $error = trim((string) ($errorMessage ?? ''));
<span class="field-label">Data sprzedazy</span> <span class="field-label">Data sprzedazy</span>
<select class="form-control" name="sale_date_source"> <select class="form-control" name="sale_date_source">
<option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option> <option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option>
<option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamowienia</option> <option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamówienia</option>
<option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data platnosci</option> <option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data płatności</option>
</select> </select>
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Numer referencyjny zamowienia</span> <span class="field-label">Numer referencyjny zamówienia</span>
<select class="form-control" name="order_reference"> <select class="form-control" name="order_reference">
<option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option> <option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option>
<option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option> <option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option>
<option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnetrzny</option> <option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnętrzny</option>
</select> </select>
</label> </label>
</div> </div>
@@ -76,7 +76,7 @@ $error = trim((string) ($errorMessage ?? ''));
<div class="form-grid-2 mt-0"> <div class="form-grid-2 mt-0">
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row"> <label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
<input type="checkbox" name="is_named" value="1"<?= $isNamed ? ' checked' : '' ?>> <input type="checkbox" name="is_named" value="1"<?= $isNamed ? ' checked' : '' ?>>
<span class="field-label" style="margin:0">Paragon imienny (z danymi kupujacego)</span> <span class="field-label" style="margin:0">Paragon imienny (z danymi kupującego)</span>
</label> </label>
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row"> <label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>> <input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>

View File

@@ -6,9 +6,9 @@ $error = trim((string) ($errorMessage ?? ''));
?> ?>
<section class="card"> <section class="card">
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">&larr; Ksiegowosc</a></p> <p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">&larr; Księgowość</a></p>
<h2 class="section-title">Konfiguracje paragonow</h2> <h2 class="section-title">Konfiguracje paragonów</h2>
<p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania paragonow.</p> <p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania paragonów.</p>
<?php if ($error !== ''): ?> <?php if ($error !== ''): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div>
@@ -26,7 +26,7 @@ $error = trim((string) ($errorMessage ?? ''));
</div> </div>
<?php if ($configs === []): ?> <?php if ($configs === []): ?>
<p class="muted mt-12">Brak konfiguracji paragonow. Dodaj pierwsza powyzej.</p> <p class="muted mt-12">Brak konfiguracji paragonów. Dodaj pierwsza powyzej.</p>
<?php else: ?> <?php else: ?>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table"> <table class="table">
@@ -67,7 +67,7 @@ $error = trim((string) ($errorMessage ?? ''));
<form action="/settings/accounting/receipts/delete" method="post" style="display:inline" class="js-confirm-delete"> <form action="/settings/accounting/receipts/delete" method="post" style="display:inline" class="js-confirm-delete">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $cid ?>"> <input type="hidden" name="id" value="<?= $cid ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>

View File

@@ -4,8 +4,8 @@ $error = trim((string) ($errorMessage ?? ''));
?> ?>
<section class="card"> <section class="card">
<h2 class="section-title">Ksiegowosc</h2> <h2 class="section-title">Księgowość</h2>
<p class="muted mt-12">Wybierz typ dokumentu ktorego konfiguracje chcesz zarzadzac.</p> <p class="muted mt-12">Wybierz typ dokumentu którego konfiguracje chcesz zarzadzac.</p>
<?php if ($error !== ''): ?> <?php if ($error !== ''): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $error, 'dismissible' => true]); ?></div>
@@ -19,14 +19,14 @@ $error = trim((string) ($errorMessage ?? ''));
<div class="form-grid-2"> <div class="form-grid-2">
<div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px"> <div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px">
<h3 class="section-title" style="margin-top:0">Paragony</h3> <h3 class="section-title" style="margin-top:0">Paragony</h3>
<p class="muted mt-12">Konfiguracje wystawiania paragonow: format numeracji, sposob numerowania, oznaczenie zamowienia.</p> <p class="muted mt-12">Konfiguracje wystawiania paragonów: format numeracji, sposób numerowania, oznaczenie zamówienia.</p>
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<a class="btn btn--primary" href="/settings/accounting/receipts">Zarzadzaj paragonami</a> <a class="btn btn--primary" href="/settings/accounting/receipts">Zarzadzaj paragonami</a>
</div> </div>
</div> </div>
<div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px"> <div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px">
<h3 class="section-title" style="margin-top:0">Faktury</h3> <h3 class="section-title" style="margin-top:0">Faktury</h3>
<p class="muted mt-12">Konfiguracje wystawiania faktur: numeracja lokalna lub delegacja do Fakturowni, termin platnosci, typ dokumentu.</p> <p class="muted mt-12">Konfiguracje wystawiania faktur: numeracja lokalna lub delegacja do Fakturowni, termin płatności, typ dokumentu.</p>
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<a class="btn btn--primary" href="/settings/accounting/invoices">Zarzadzaj fakturami</a> <a class="btn btn--primary" href="/settings/accounting/invoices">Zarzadzaj fakturami</a>
<a class="btn btn--secondary ml-8" href="/settings/accounting/invoices/issued">Faktury wystawione</a> <a class="btn btn--secondary ml-8" href="/settings/accounting/invoices/issued">Faktury wystawione</a>

View File

@@ -443,7 +443,7 @@ foreach ($pullStatusMappings as $pm) {
<div class="dm-apaczka-panel" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>"> <div class="dm-apaczka-panel" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>">
<?php if ($dmApaczkaServices === []): ?> <?php if ($dmApaczkaServices === []): ?>
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div> <div class="muted">Brak usług Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
<?php else: ?> <?php else: ?>
<select class="form-control dm-apaczka-select"> <select class="form-control dm-apaczka-select">
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option> <option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>
@@ -467,7 +467,7 @@ foreach ($pullStatusMappings as $pm) {
<?php // InPost simple select ?> <?php // InPost simple select ?>
<div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>"> <div class="dm-inpost-panel" style="<?= $currentCarrier !== 'inpost' ? 'display:none' : '' ?>">
<?php if ($dmInpostServices === []): ?> <?php if ($dmInpostServices === []): ?>
<div class="muted">Brak uslug InPost (sprawdz polaczenie z Allegro).</div> <div class="muted">Brak usług InPost (sprawdz połączenie z Allegro).</div>
<?php else: ?> <?php else: ?>
<select class="form-control dm-inpost-select"> <select class="form-control dm-inpost-select">
<option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option> <option value="">-- <?= $e($t('settings.allegro.delivery.fields.no_mapping')) ?> --</option>

View File

@@ -31,7 +31,7 @@ $action = $isEdit
<span class="field-label">Klucz *</span> <span class="field-label">Klucz *</span>
<?php if ($isEdit): ?> <?php if ($isEdit): ?>
<input class="form-control" type="text" value="<?= $e($rowKey) ?>" disabled> <input class="form-control" type="text" value="<?= $e($rowKey) ?>" disabled>
<small class="muted">Klucz nie może być zmieniany po utworzeniu.</small> <small class="muted">Klucz nie może być zmieńiany po utworzeniu.</small>
<?php else: ?> <?php else: ?>
<input class="form-control" type="text" name="key" maxlength="50" required <input class="form-control" type="text" name="key" maxlength="50" required
pattern="[a-z][a-z0-9_]{0,49}" pattern="[a-z][a-z0-9_]{0,49}"

View File

@@ -14,7 +14,7 @@ $isEdit = $em !== null;
<section class="card"> <section class="card">
<h2 class="section-title">Skrzynki pocztowe</h2> <h2 class="section-title">Skrzynki pocztowe</h2>
<p class="muted mt-12">Konfiguracja skrzynek SMTP do wysylki wiadomosci e-mail.</p> <p class="muted mt-12">Konfiguracja skrzynek SMTP do wysyłki wiadomości e-mail.</p>
<?php if (!empty($errorMessage)): ?> <?php if (!empty($errorMessage)): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div>
@@ -28,7 +28,7 @@ $isEdit = $em !== null;
<h3 class="section-title">Lista skrzynek</h3> <h3 class="section-title">Lista skrzynek</h3>
<?php if (count($mailboxes) === 0): ?> <?php if (count($mailboxes) === 0): ?>
<p class="muted mt-12">Brak skrzynek pocztowych. Dodaj pierwsza skrzynke ponizej.</p> <p class="muted mt-12">Brak skrzynek pocztowych. Dodaj pierwsza skrzynke poniżej.</p>
<?php else: ?> <?php else: ?>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table"> <table class="table">
@@ -73,7 +73,7 @@ $isEdit = $em !== null;
<form action="/settings/email-mailboxes/delete" method="post" style="display:inline" class="js-confirm-delete"> <form action="/settings/email-mailboxes/delete" method="post" style="display:inline" class="js-confirm-delete">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= (int) ($mb['id'] ?? 0) ?>"> <input type="hidden" name="id" value="<?= (int) ($mb['id'] ?? 0) ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -96,7 +96,7 @@ $isEdit = $em !== null;
<div class="form-grid-2"> <div class="form-grid-2">
<label class="form-field"> <label class="form-field">
<span class="field-label">Nazwa *</span> <span class="field-label">Nazwa *</span>
<input class="form-control" type="text" name="name" maxlength="100" required value="<?= $e((string) ($em['name'] ?? '')) ?>" placeholder="np. Glowna skrzynka"> <input class="form-control" type="text" name="name" maxlength="100" required value="<?= $e((string) ($em['name'] ?? '')) ?>" placeholder="np. Główna skrzynka">
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">E-mail nadawcy *</span> <span class="field-label">E-mail nadawcy *</span>
@@ -135,14 +135,14 @@ $isEdit = $em !== null;
<div class="form-grid-2 mt-0"> <div class="form-grid-2 mt-0">
<label class="form-field"> <label class="form-field">
<span class="field-label">Uzytkownik SMTP *</span> <span class="field-label">Użytkownik SMTP *</span>
<input class="form-control" type="text" name="smtp_username" maxlength="255" required value="<?= $e((string) ($em['smtp_username'] ?? '')) ?>"> <input class="form-control" type="text" name="smtp_username" maxlength="255" required value="<?= $e((string) ($em['smtp_username'] ?? '')) ?>">
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Haslo SMTP <?= $isEdit ? '' : '*' ?></span> <span class="field-label">Hasło SMTP <?= $isEdit ? '' : '*' ?></span>
<input class="form-control" type="password" name="smtp_password" maxlength="255" <?= $isEdit ? '' : 'required' ?> placeholder="<?= $isEdit ? '(bez zmian)' : '' ?>"> <input class="form-control" type="password" name="smtp_password" maxlength="255" <?= $isEdit ? '' : 'required' ?> placeholder="<?= $isEdit ? '(bez zmian)' : '' ?>">
<?php if ($isEdit): ?> <?php if ($isEdit): ?>
<small class="field-hint">Pozostaw puste, aby zachowac aktualne haslo</small> <small class="field-hint">Pozostaw puste, aby zachowac aktualne hasło</small>
<?php endif; ?> <?php endif; ?>
</label> </label>
</div> </div>
@@ -158,11 +158,11 @@ $isEdit = $em !== null;
</label> </label>
</div> </div>
<h4 class="section-title mt-16">Szablon wiadomosci</h4> <h4 class="section-title mt-16">Szablon wiadomości</h4>
<p class="muted mt-4" style="font-size:12px">Opcjonalnie. Naglowek i stopka beda dolaczane do kazdego e-maila wysylanego z tej skrzynki.</p> <p class="muted mt-4" style="font-size:12px">Opcjonalnie. Nagłówek i stopka beda dolaczane do kazdego e-maila wysyłanego z tej skrzynki.</p>
<div class="form-field mt-12"> <div class="form-field mt-12">
<span class="field-label">Naglowek (header)</span> <span class="field-label">Nagłówek (header)</span>
<div id="js-header-editor" style="min-height:80px"></div> <div id="js-header-editor" style="min-height:80px"></div>
<textarea id="js-header-source" class="html-source-area"></textarea> <textarea id="js-header-source" class="html-source-area"></textarea>
<div class="html-source-toggle"><button type="button" class="js-toggle-html" data-editor="header">&lt;/&gt; HTML</button> <button type="button" class="js-preview-html" data-editor="header">Podglad</button></div> <div class="html-source-toggle"><button type="button" class="js-toggle-html" data-editor="header">&lt;/&gt; HTML</button> <button type="button" class="js-preview-html" data-editor="header">Podglad</button></div>
@@ -179,7 +179,7 @@ $isEdit = $em !== null;
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj skrzynke' ?></button> <button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj skrzynke' ?></button>
<button type="button" class="btn btn--secondary" id="js-test-connection">Testuj polaczenie</button> <button type="button" class="btn btn--secondary" id="js-test-connection">Testuj połączenie</button>
<?php if ($isEdit): ?> <?php if ($isEdit): ?>
<a href="/settings/email-mailboxes" class="btn btn--secondary">Anuluj</a> <a href="/settings/email-mailboxes" class="btn btn--secondary">Anuluj</a>
<?php endif; ?> <?php endif; ?>
@@ -218,13 +218,13 @@ document.addEventListener('DOMContentLoaded', function() {
var headerEditor = new Quill('#js-header-editor', { var headerEditor = new Quill('#js-header-editor', {
theme: 'snow', theme: 'snow',
modules: { toolbar: quillToolbar }, modules: { toolbar: quillToolbar },
placeholder: 'Naglowek wiadomosci (np. logo, nazwa firmy)...' placeholder: 'Nagłówek wiadomości (np. logo, nazwa firmy)...'
}); });
var footerEditor = new Quill('#js-footer-editor', { var footerEditor = new Quill('#js-footer-editor', {
theme: 'snow', theme: 'snow',
modules: { toolbar: quillToolbar }, modules: { toolbar: quillToolbar },
placeholder: 'Stopka wiadomosci (np. dane kontaktowe, adres)...' placeholder: 'Stopka wiadomości (np. dane kontaktowe, adres)...'
}); });
// --- HTML source toggle --- // --- HTML source toggle ---
@@ -312,11 +312,11 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.js-preview-html').forEach(function(btn) { document.querySelectorAll('.js-preview-html').forEach(function(btn) {
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var key = this.getAttribute('data-editor'); var key = this.getAttribute('data-editor');
var label = key === 'header' ? 'Naglowek (header)' : 'Stopka (footer)'; var label = key === 'header' ? 'Nagłówek (header)' : 'Stopka (footer)';
var html = getEditorHtml(key); var html = getEditorHtml(key);
if (!html) { if (!html) {
if (window.OrderProAlerts && window.OrderProAlerts.warning) { if (window.OrderProAlerts && window.OrderProAlerts.warning) {
window.OrderProAlerts.warning('Podglad', 'Brak tresci do wyswietlenia.'); window.OrderProAlerts.warning('Podglad', 'Brak treśći do wyświetlenia.');
} }
return; return;
} }
@@ -365,11 +365,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm( window.OrderProAlerts.confirm(
'Usuwanie skrzynki', 'Usuwanie skrzynki',
'Czy na pewno chcesz usunac te skrzynke pocztowa?', 'Czy na pewno chcesz usuńąć te skrzynke pocztowa?',
function() { form.submit(); } function() { form.submit(); }
); );
} else { } else {
if (confirm('Czy na pewno chcesz usunac te skrzynke pocztowa?')) { if (confirm('Czy na pewno chcesz usuńąć te skrzynke pocztowa?')) {
form.submit(); form.submit();
} }
} }
@@ -399,16 +399,16 @@ document.addEventListener('DOMContentLoaded', function() {
} else { } else {
resultDiv.className = 'mt-12 alert alert--danger'; resultDiv.className = 'mt-12 alert alert--danger';
} }
resultDiv.textContent = data.message || 'Brak odpowiedzi'; resultDiv.textContent = data.message || 'Brak odpowiedźi';
}) })
.catch(function(err) { .catch(function(err) {
resultDiv.style.display = 'block'; resultDiv.style.display = 'block';
resultDiv.className = 'mt-12 alert alert--danger'; resultDiv.className = 'mt-12 alert alert--danger';
resultDiv.textContent = 'Blad polaczenia: ' + err.message; resultDiv.textContent = 'Błąd połączenia: ' + err.message;
}) })
.finally(function() { .finally(function() {
testBtn.disabled = false; testBtn.disabled = false;
testBtn.textContent = 'Testuj polaczenie'; testBtn.textContent = 'Testuj połączenie';
}); });
}); });
} }

View File

@@ -8,7 +8,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<section class="card"> <section class="card">
<h2 class="section-title"><?= $isEdit ? 'Edytuj szablon e-mail' : 'Dodaj szablon e-mail' ?></h2> <h2 class="section-title"><?= $isEdit ? 'Edytuj szablon e-mail' : 'Dodaj szablon e-mail' ?></h2>
<p class="muted mt-12">Skonfiguruj temat, tresc i zmienne, ktore beda podstawiane podczas wysylki.</p> <p class="muted mt-12">Skonfiguruj temat, treść i zmienne, które beda podstawiane podczas wysyłki.</p>
<?php if (!empty($errorMessage)): ?> <?php if (!empty($errorMessage)): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div>
@@ -29,7 +29,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<div class="form-grid-2"> <div class="form-grid-2">
<label class="form-field"> <label class="form-field">
<span class="field-label">Nazwa szablonu *</span> <span class="field-label">Nazwa szablonu *</span>
<input class="form-control" type="text" name="name" maxlength="200" required value="<?= $e((string) ($template['name'] ?? '')) ?>" placeholder="np. Potwierdzenie zamowienia"> <input class="form-control" type="text" name="name" maxlength="200" required value="<?= $e((string) ($template['name'] ?? '')) ?>" placeholder="np. Potwierdzenie zamówienia">
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Skrzynka nadawcza</span> <span class="field-label">Skrzynka nadawcza</span>
@@ -47,8 +47,8 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<div class="form-grid-2 mt-0"> <div class="form-grid-2 mt-0">
<label class="form-field"> <label class="form-field">
<span class="field-label">Temat wiadomosci *</span> <span class="field-label">Temat wiadomości *</span>
<input class="form-control" type="text" name="subject" maxlength="500" required value="<?= $e((string) ($template['subject'] ?? '')) ?>" placeholder="np. Potwierdzenie zamowienia {{zamowienie.numer}}"> <input class="form-control" type="text" name="subject" maxlength="500" required value="<?= $e((string) ($template['subject'] ?? '')) ?>" placeholder="np. Potwierdzenie zamówienia {{zamowienie.numer}}">
</label> </label>
<div class="form-field" style="display:flex;align-items:flex-end;gap:8px"> <div class="form-field" style="display:flex;align-items:flex-end;gap:8px">
<label style="display:flex;align-items:center;gap:6px;flex-direction:row"> <label style="display:flex;align-items:center;gap:6px;flex-direction:row">
@@ -60,7 +60,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<div class="form-grid-2 mt-0"> <div class="form-grid-2 mt-0">
<label class="form-field"> <label class="form-field">
<span class="field-label">Zalacznik nr 1</span> <span class="field-label">Załącznik nr 1</span>
<select class="form-control" name="attachment_1"> <select class="form-control" name="attachment_1">
<option value="">- brak -</option> <option value="">- brak -</option>
<?php foreach ($attachmentTypes as $attachmentKey => $attachmentLabel): ?> <?php foreach ($attachmentTypes as $attachmentKey => $attachmentLabel): ?>
@@ -72,12 +72,12 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
</div> </div>
<div class="mt-12"> <div class="mt-12">
<span class="field-label">Tresc wiadomosci *</span> <span class="field-label">Treść wiadomości *</span>
<p class="muted mt-4">Dostepne sa zmienne przesylki: <code>{{przesylka.numer}}</code> oraz <code>{{przesylka.link_sledzenia}}</code>.</p> <p class="muted mt-4">Dostępne są zmienne przesyłki: <code>{{przesylka.numer}}</code> oraz <code>{{przesylka.link_sledzenia}}</code>.</p>
<div class="email-tpl-editor-wrap mt-4"> <div class="email-tpl-editor-wrap mt-4">
<div class="email-tpl-toolbar"> <div class="email-tpl-toolbar">
<div class="email-tpl-var-dropdown"> <div class="email-tpl-var-dropdown">
<button type="button" class="btn btn--sm btn--secondary" id="js-var-toggle">Wstaw zmienna</button> <button type="button" class="btn btn--sm btn--secondary" id="js-var-toggle">Wstaw zmienną</button>
<div class="email-tpl-var-panel" id="js-var-panel" style="display:none"> <div class="email-tpl-var-panel" id="js-var-panel" style="display:none">
<?php foreach ($variableGroups as $groupKey => $group): ?> <?php foreach ($variableGroups as $groupKey => $group): ?>
<div class="email-var-group"> <div class="email-var-group">
@@ -99,7 +99,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj szablon' ?></button> <button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj szablon' ?></button>
<a href="/settings/email-templates" class="btn btn--secondary">Powrot do listy</a> <a href="/settings/email-templates" class="btn btn--secondary">Powrót do listy</a>
</div> </div>
</form> </form>
</section> </section>
@@ -137,7 +137,7 @@ document.addEventListener('DOMContentLoaded', function() {
['clean'] ['clean']
] ]
}, },
placeholder: 'Wpisz tresc wiadomosci...' placeholder: 'Wpisz treść wiadomości...'
}); });
var existingHtml = document.getElementById('js-body-html').value; var existingHtml = document.getElementById('js-body-html').value;

View File

@@ -8,7 +8,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<h2 class="section-title">Szablony e-mail</h2> <h2 class="section-title">Szablony e-mail</h2>
<a href="/settings/email-templates/create" class="btn btn--primary btn--sm">Dodaj szablon</a> <a href="/settings/email-templates/create" class="btn btn--primary btn--sm">Dodaj szablon</a>
</div> </div>
<p class="muted mt-12">Szablony wiadomosci e-mail z edytorem i systemem zmiennych.</p> <p class="muted mt-12">Szablony wiadomości e-mail z edytorem i systemem zmiennych.</p>
<?php if (!empty($errorMessage)): ?> <?php if (!empty($errorMessage)): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div>
@@ -31,7 +31,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<th>Nazwa</th> <th>Nazwa</th>
<th>Temat</th> <th>Temat</th>
<th>Skrzynka</th> <th>Skrzynka</th>
<th>Zalacznik</th> <th>Załącznik</th>
<th>Status</th> <th>Status</th>
<th>Akcje</th> <th>Akcje</th>
</tr> </tr>
@@ -66,7 +66,7 @@ $attachmentTypes = is_array($attachmentTypes ?? null) ? $attachmentTypes : [];
<form action="/settings/email-templates/delete" method="post" style="display:inline" class="js-confirm-delete"> <form action="/settings/email-templates/delete" method="post" style="display:inline" class="js-confirm-delete">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $templateId ?>"> <input type="hidden" name="id" value="<?= $templateId ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -117,7 +117,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm( window.OrderProAlerts.confirm(
'Usuwanie szablonu', 'Usuwanie szablonu',
'Czy na pewno chcesz usunac ten szablon e-mail?', 'Czy na pewno chcesz usuńąć ten szablon e-mail?',
function() { delForm.submit(); } function() { delForm.submit(); }
); );
} else { } else {

View File

@@ -2,7 +2,7 @@
?> ?>
<section class="card"> <section class="card">
<h2 class="section-title">Integracja Fakturownia</h2> <h2 class="section-title">Integracja Fakturownia</h2>
<p class="muted mt-12">Fakturownia ma jedna globalna konfiguracje. Wroc do strony konfiguracji.</p> <p class="muted mt-12">Fakturownia ma jedna globalna konfiguracje. Wróć do strony konfiguracji.</p>
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<a class="btn btn--primary" href="/settings/integrations/fakturownia">Otworz konfiguracje</a> <a class="btn btn--primary" href="/settings/integrations/fakturownia">Otworz konfiguracje</a>
</div> </div>

View File

@@ -53,7 +53,7 @@ $flashError = trim((string) ($flashError ?? ''));
<label class="form-field"> <label class="form-field">
<span class="field-label">Token API <?= $hasToken ? '' : '*' ?></span> <span class="field-label">Token API <?= $hasToken ? '' : '*' ?></span>
<input class="form-control" type="password" name="api_token" autocomplete="new-password" placeholder="<?= $hasToken ? '********' : '' ?>" <?= $hasToken ? '' : 'required' ?>> <input class="form-control" type="password" name="api_token" autocomplete="new-password" placeholder="<?= $hasToken ? '********' : '' ?>" <?= $hasToken ? '' : 'required' ?>>
<span class="muted"><?= $hasToken ? 'Token jest zapisany. Wpisz nowy, aby go nadpisac.' : 'Token API z Fakturowni (Ustawienia > Konta uzytkownikow > API).' ?></span> <span class="muted"><?= $hasToken ? 'Token jest zapisany. Wpisz nowy, aby go nadpisać.' : 'Token API z Fakturowni (Ustawienia > Konta użytkowników > API).' ?></span>
</label> </label>
<label class="form-field"> <label class="form-field">
@@ -63,16 +63,16 @@ $flashError = trim((string) ($flashError ?? ''));
<div class="form-grid-2 mt-0"> <div class="form-grid-2 mt-0">
<label class="form-field"> <label class="form-field">
<span class="field-label">Domyslny typ dokumentu</span> <span class="field-label">Domyślny typ dokumentu</span>
<select class="form-control" name="default_kind"> <select class="form-control" name="default_kind">
<option value="vat" <?= $defaultKind === 'vat' ? 'selected' : '' ?>>Faktura VAT</option> <option value="vat" <?= $defaultKind === 'vat' ? 'selected' : '' ?>>Faktura VAT</option>
<option value="proforma" <?= $defaultKind === 'proforma' ? 'selected' : '' ?>>Proforma</option> <option value="proformą" <?= $defaultKind === 'proformą' ? 'selected' : '' ?>>Proformą</option>
<option value="invoice_other" <?= $defaultKind === 'invoice_other' ? 'selected' : '' ?>>Inna</option> <option value="invoice_other" <?= $defaultKind === 'invoice_other' ? 'selected' : '' ?>>Inna</option>
</select> </select>
</label> </label>
<label class="form-field"> <label class="form-field">
<span class="field-label">Domyslny termin platnosci (dni)</span> <span class="field-label">Domyślny termin płatności (dni)</span>
<input class="form-control" type="number" name="default_payment_to_days" min="0" max="120" value="<?= $defaultPaymentDays ?>"> <input class="form-control" type="number" name="default_payment_to_days" min="0" max="120" value="<?= $defaultPaymentDays ?>">
</label> </label>
</div> </div>
@@ -92,13 +92,13 @@ $flashError = trim((string) ($flashError ?? ''));
</section> </section>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Test polaczenia</h3> <h3 class="section-title">Test połączenia</h3>
<p class="muted mt-12">Wykonuje GET <code><?= $e('https://' . ($prefix !== '' ? $prefix : '{prefix}') . '.fakturownia.pl/account.json') ?></code> z zapisanym tokenem.</p> <p class="muted mt-12">Wykonuje GET <code><?= $e('https://' . ($prefix !== '' ? $prefix : '{prefix}') . '.fakturownia.pl/account.json') ?></code> z zapisanym tokenem.</p>
<form action="/settings/integrations/fakturownia/test" method="post" class="mt-12"> <form action="/settings/integrations/fakturownia/test" method="post" class="mt-12">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $integrationId ?>"> <input type="hidden" name="id" value="<?= $integrationId ?>">
<button type="submit" class="btn btn--secondary">Testuj polaczenie</button> <button type="submit" class="btn btn--secondary">Testuj połączenie</button>
</form> </form>
<?php if ($lastTestAt !== ''): ?> <?php if ($lastTestAt !== ''): ?>

View File

@@ -23,7 +23,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
<strong>Nowy klucz API:</strong> <strong>Nowy klucz API:</strong>
<code id="new-api-key" style="display:inline-block;padding:4px 8px;background:#f5f5f5;border:1px solid #ddd;border-radius:3px;font-size:13px;word-break:break-all;user-select:all"><?= $e($newKey) ?></code> <code id="new-api-key" style="display:inline-block;padding:4px 8px;background:#f5f5f5;border:1px solid #ddd;border-radius:3px;font-size:13px;word-break:break-all;user-select:all"><?= $e($newKey) ?></code>
<button type="button" class="btn btn--secondary btn--sm ml-8" onclick="navigator.clipboard.writeText(document.getElementById('new-api-key').textContent)">Kopiuj</button> <button type="button" class="btn btn--secondary btn--sm ml-8" onclick="navigator.clipboard.writeText(document.getElementById('new-api-key').textContent)">Kopiuj</button>
<br><small class="muted">Ten klucz nie bedzie ponownie wyswietlony. Skopiuj go teraz.</small> <br><small class="muted">Ten klucz nie bedzie ponownie wyświetlony. Skopiuj go teraz.</small>
<?php <?php
$newKeyAlertHtml = ob_get_clean(); $newKeyAlertHtml = ob_get_clean();
?> ?>
@@ -35,7 +35,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
<h3 class="section-title">Klucze API</h3> <h3 class="section-title">Klucze API</h3>
<?php if (count($keysList) === 0): ?> <?php if (count($keysList) === 0): ?>
<p class="muted mt-12">Brak kluczy API. Utworz pierwszy klucz ponizej.</p> <p class="muted mt-12">Brak kluczy API. Utworz pierwszy klucz poniżej.</p>
<?php else: ?> <?php else: ?>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table"> <table class="table">
@@ -66,7 +66,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
<td> <td>
<form method="post" action="/settings/printing/keys/<?= $e((string) ($key['id'] ?? '')) ?>/delete" style="display:inline" class="js-delete-api-key-form"> <form method="post" action="/settings/printing/keys/<?= $e((string) ($key['id'] ?? '')) ?>/delete" style="display:inline" class="js-delete-api-key-form">
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>"> <input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
<button type="button" class="btn btn--danger btn--sm js-delete-api-key-btn">Usun</button> <button type="button" class="btn btn--danger btn--sm js-delete-api-key-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -107,7 +107,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
<thead> <thead>
<tr> <tr>
<th>Data</th> <th>Data</th>
<th>Zamowienie</th> <th>Zamówienie</th>
<th>Tracking</th> <th>Tracking</th>
<th>Status</th> <th>Status</th>
<th>Wydrukowano</th> <th>Wydrukowano</th>
@@ -133,7 +133,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
$badgeLabel = 'Wydrukowano'; $badgeLabel = 'Wydrukowano';
} elseif ($jobStatus === 'failed') { } elseif ($jobStatus === 'failed') {
$badgeClass = 'print-status-badge--failed'; $badgeClass = 'print-status-badge--failed';
$badgeLabel = 'Blad'; $badgeLabel = 'Błąd';
} }
?> ?>
<tr> <tr>
@@ -163,7 +163,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
type="button" type="button"
class="btn btn--sm btn--danger js-delete-print-job" class="btn btn--sm btn--danger js-delete-print-job"
data-job-id="<?= $e((string) $jobId) ?>" data-job-id="<?= $e((string) $jobId) ?>"
>Usun</button> >Usuń</button>
</form> </form>
</div> </div>
</td> </td>
@@ -184,8 +184,8 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
title: 'Usuwanie wpisu z kolejki', title: 'Usuwanie wpisu z kolejki',
message: 'Czy na pewno chcesz usunac ten wpis kolejki wydruku?', message: 'Czy na pewno chcesz usuńąć ten wpis kolejki wydruku?',
confirmLabel: 'Usun', confirmLabel: 'Usuń',
cancelLabel: 'Anuluj', cancelLabel: 'Anuluj',
danger: true danger: true
}).then(function (confirmed) { }).then(function (confirmed) {
@@ -206,8 +206,8 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
title: 'Usuwanie klucza API', title: 'Usuwanie klucza API',
message: 'Czy na pewno chcesz usunac ten klucz API?', message: 'Czy na pewno chcesz usuńąć ten klucz API?',
confirmLabel: 'Usun', confirmLabel: 'Usuń',
cancelLabel: 'Anuluj', cancelLabel: 'Anuluj',
danger: true danger: true
}).then(function (confirmed) { }).then(function (confirmed) {
@@ -228,7 +228,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
if (!packageId || !csrf) return; if (!packageId || !csrf) return;
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Wysylam...'; btn.textContent = 'Wysyłam...';
fetch('/api/print/jobs', { fetch('/api/print/jobs', {
method: 'POST', method: 'POST',
@@ -240,7 +240,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
if (res.status === 201) { if (res.status === 201) {
window.location.reload(); window.location.reload();
} else { } else {
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad'; var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany błąd';
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: msg, type: 'error' }); window.OrderProAlerts.show({ message: msg, type: 'error' });
} }
@@ -250,7 +250,7 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
}) })
.catch(function () { .catch(function () {
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: 'Blad sieci.', type: 'error' }); window.OrderProAlerts.show({ message: 'Błąd sieci.', type: 'error' });
} }
btn.disabled = false; btn.disabled = false;
btn.textContent = 'Ponow'; btn.textContent = 'Ponow';

View File

@@ -546,7 +546,7 @@ foreach ($dmMappings as $dm) {
<div class="dm-apaczka-panel dm-searchable-select" data-provider="apaczka" data-current-id="<?= $e($currentCarrier === 'apaczka' ? $currentMethodId : '') ?>" data-current-name="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>"> <div class="dm-apaczka-panel dm-searchable-select" data-provider="apaczka" data-current-id="<?= $e($currentCarrier === 'apaczka' ? $currentMethodId : '') ?>" data-current-name="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" style="<?= $currentCarrier !== 'apaczka' ? 'display:none' : '' ?>">
<?php if ($dmApaczkaServices === []): ?> <?php if ($dmApaczkaServices === []): ?>
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div> <div class="muted">Brak usług Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
<?php else: ?> <?php else: ?>
<input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" autocomplete="off"> <input type="text" class="form-control dm-search-input" placeholder="<?= $e($t('settings.integrations.delivery.fields.search_placeholder')) ?>" value="<?= $e($currentCarrier === 'apaczka' ? $currentServiceName : '') ?>" autocomplete="off">
<div class="searchable-select__dropdown dm-dropdown"> <div class="searchable-select__dropdown dm-dropdown">

View File

@@ -6,7 +6,7 @@ $variableGroups = is_array($variableGroups ?? null) ? $variableGroups : [];
<section class="card"> <section class="card">
<h2 class="section-title"><?= $isEdit ? 'Edytuj szablon SMS' : 'Dodaj szablon SMS' ?></h2> <h2 class="section-title"><?= $isEdit ? 'Edytuj szablon SMS' : 'Dodaj szablon SMS' ?></h2>
<p class="muted mt-12">Wpisz tresc wiadomosci ze zmiennymi typu <code>{{zamowienie.numer}}</code>. Stopka SMSPLANET jest doklejana automatycznie przy wysylce, nie dopisuj jej w szablonie.</p> <p class="muted mt-12">Wpisz treść wiadomości ze zmiennymi typu <code>{{zamowienie.numer}}</code>. Stopka SMSPLANET jest doklejana automatycznie przy wysyłce, nie dopisuj jej w szablonie.</p>
<?php if (!empty($errorMessage)): ?> <?php if (!empty($errorMessage)): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div>
@@ -38,19 +38,19 @@ $variableGroups = is_array($variableGroups ?? null) ? $variableGroups : [];
<div class="mt-12"> <div class="mt-12">
<label class="form-field"> <label class="form-field">
<span class="field-label">Tresc wiadomosci *</span> <span class="field-label">Treść wiadomości *</span>
<textarea class="form-control" name="body" id="js-sms-body" rows="6" maxlength="918" required placeholder="np. Czesc {{kupujacy.imie_nazwisko}}, Twoja przesylka {{przesylka.numer}} jest w drodze."><?= $e((string) ($template['body'] ?? '')) ?></textarea> <textarea class="form-control" name="body" id="js-sms-body" rows="6" maxlength="918" required placeholder="np. Cześć {{kupujacy.imie_nazwisko}}, Twoja przesyłka {{przesylka.numer}} jest w drodze."><?= $e((string) ($template['body'] ?? '')) ?></textarea>
<div class="sms-template-counter muted mt-4"> <div class="sms-template-counter muted mt-4">
<span id="js-sms-body-count">0</span> / 918 znakow <span id="js-sms-body-count">0</span> / 918 znaków
<span class="sms-template-counter-warning" id="js-sms-body-warn" hidden>(pamietaj o stopce dodawanej przez SMSPLANET)</span> <span class="sms-template-counter-warning" id="js-sms-body-warn" hidden>(pamiętaj o stopce dodawanej przez SMSPLANET)</span>
</div> </div>
</label> </label>
</div> </div>
<div class="sms-var-panel mt-12"> <div class="sms-var-panel mt-12">
<div class="sms-var-panel__head"> <div class="sms-var-panel__head">
<span class="field-label sms-var-panel__title">Dostepne zmienne</span> <span class="field-label sms-var-panel__title">Dostępne zmienne</span>
<span class="muted sms-var-panel__hint">Kliknij chip, aby wstawic w pozycji kursora.</span> <span class="muted sms-var-panel__hint">Kliknij chip, aby wstawić w pozycji kursora.</span>
</div> </div>
<?php foreach ($variableGroups as $groupKey => $group): ?> <?php foreach ($variableGroups as $groupKey => $group): ?>
<div class="sms-var-group" data-group="<?= $e($groupKey) ?>"> <div class="sms-var-group" data-group="<?= $e($groupKey) ?>">
@@ -69,7 +69,7 @@ $variableGroups = is_array($variableGroups ?? null) ? $variableGroups : [];
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj szablon' ?></button> <button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj szablon' ?></button>
<a href="/settings/sms-templates" class="btn btn--secondary">Powrot do listy</a> <a href="/settings/sms-templates" class="btn btn--secondary">Powrót do listy</a>
</div> </div>
</form> </form>
</section> </section>

View File

@@ -7,7 +7,7 @@ $templates = is_array($templates ?? null) ? $templates : [];
<h2 class="section-title">Szablony SMS</h2> <h2 class="section-title">Szablony SMS</h2>
<a href="/settings/sms-templates/create" class="btn btn--primary btn--sm">Dodaj szablon</a> <a href="/settings/sms-templates/create" class="btn btn--primary btn--sm">Dodaj szablon</a>
</div> </div>
<p class="muted mt-12">Szybkie szablony wiadomosci SMS do wstawiania z zakladki SMS w szczegolach zamowienia. Stopka SMSPLANET jest doklejana automatycznie.</p> <p class="muted mt-12">Szybkie szablony wiadomości SMS do wstawiania z zakładki SMS w szczegółach zamówienia. Stopka SMSPLANET jest doklejana automatycznie.</p>
<?php if (!empty($errorMessage)): ?> <?php if (!empty($errorMessage)): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $errorMessage, 'dismissible' => true]); ?></div>
@@ -28,7 +28,7 @@ $templates = is_array($templates ?? null) ? $templates : [];
<thead> <thead>
<tr> <tr>
<th>Nazwa</th> <th>Nazwa</th>
<th>Tresc</th> <th>Treść</th>
<th>Status</th> <th>Status</th>
<th>Akcje</th> <th>Akcje</th>
</tr> </tr>
@@ -59,10 +59,10 @@ $templates = is_array($templates ?? null) ? $templates : [];
data-active="<?= (int) ($tpl['is_active'] ?? 0) ?>"> data-active="<?= (int) ($tpl['is_active'] ?? 0) ?>">
<?= ((int) ($tpl['is_active'] ?? 0)) === 1 ? 'Dezaktywuj' : 'Aktywuj' ?> <?= ((int) ($tpl['is_active'] ?? 0)) === 1 ? 'Dezaktywuj' : 'Aktywuj' ?>
</button> </button>
<form action="/settings/sms-templates/delete" method="post" class="inline-form js-confirm-delete" data-confirm-title="Usuwanie szablonu" data-confirm-message="Czy na pewno chcesz usunac ten szablon SMS?"> <form action="/settings/sms-templates/delete" method="post" class="inline-form js-confirm-delete" data-confirm-title="Usuwanie szablonu" data-confirm-message="Czy na pewno chcesz usuńąć ten szablon SMS?">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>"> <input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $templateId ?>"> <input type="hidden" name="id" value="<?= $templateId ?>">
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button> <button type="button" class="btn btn--sm btn--danger js-delete-btn">Usuń</button>
</form> </form>
</td> </td>
</tr> </tr>

View File

@@ -60,8 +60,8 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<section class="card"> <section class="card">
<div class="order-details-head"> <div class="order-details-head">
<div> <div>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>" class="order-back-link">&larr; Powrot do zamowienia</a> <a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>" class="order-back-link">&larr; Powrót do zamówienia</a>
<h2 class="section-title mt-12">Przygotuj przesylke #<?= $e((string) ($orderId ?? 0)) ?></h2> <h2 class="section-title mt-12">Przygotuj przesyłkę #<?= $e((string) ($orderId ?? 0)) ?></h2>
<div class="order-details-sub mt-4"> <div class="order-details-sub mt-4">
<span><?= $e(ucfirst((string) ($orderRow['source'] ?? ''))) ?> <?= $e((string) ($orderRow['external_order_id'] ?? '')) ?></span> <span><?= $e(ucfirst((string) ($orderRow['source'] ?? ''))) ?> <?= $e((string) ($orderRow['external_order_id'] ?? '')) ?></span>
</div> </div>
@@ -103,7 +103,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<div class="shipment-grid mt-16"> <div class="shipment-grid mt-16">
<section class="card"> <section class="card">
<h3 class="section-title">Przesylka</h3> <h3 class="section-title">Przesyłka</h3>
<?php if ($servicesError !== ''): ?> <?php if ($servicesError !== ''): ?>
<div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $servicesError, 'dismissible' => true]); ?></div> <div class="mt-12"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $servicesError, 'dismissible' => true]); ?></div>
@@ -119,7 +119,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<option value="polkurier"<?= $preselectedCarrier === 'polkurier' ? ' selected' : '' ?>>polkurier</option> <option value="polkurier"<?= $preselectedCarrier === 'polkurier' ? ' selected' : '' ?>>polkurier</option>
</select> </select>
<?php if ($deliveryMethodName !== ''): ?> <?php if ($deliveryMethodName !== ''): ?>
<div class="muted mt-4" style="font-size:12px">Metoda z zamowienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> &rarr; <?= $e($mappedCarrierLabel) ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div> <div class="muted mt-4" style="font-size:12px">Metoda z zamówienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> &rarr; <?= $e($mappedCarrierLabel) ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div>
<?php endif; ?> <?php endif; ?>
<?php if ($deliveryMappingDiagnostic !== ''): ?> <?php if ($deliveryMappingDiagnostic !== ''): ?>
<div class="mt-8"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $deliveryMappingDiagnostic, 'dismissible' => true]); ?></div> <div class="mt-8"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $deliveryMappingDiagnostic, 'dismissible' => true]); ?></div>
@@ -127,7 +127,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
</div> </div>
<div class="form-field mt-12"> <div class="form-field mt-12">
<span class="field-label">Usluga dostawy</span> <span class="field-label">Usługa dostawy</span>
<input type="hidden" name="delivery_method_id" id="shipment-delivery-service" value="" required> <input type="hidden" name="delivery_method_id" id="shipment-delivery-service" value="" required>
<div id="shipment-allegro-panel" style="<?= $preselectedCarrier !== 'allegro' ? 'display:none' : '' ?>"> <div id="shipment-allegro-panel" style="<?= $preselectedCarrier !== 'allegro' ? 'display:none' : '' ?>">
@@ -135,7 +135,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<div class="mt-4"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $servicesError, 'dismissible' => true]); ?></div> <div class="mt-4"><?php $component('components/alert', ['type' => 'danger', 'message' => (string) $servicesError, 'dismissible' => true]); ?></div>
<?php endif; ?> <?php endif; ?>
<div class="searchable-select" id="shipment-service-wrapper" data-match-id="<?= $e($deliveryMethodId) ?>"> <div class="searchable-select" id="shipment-service-wrapper" data-match-id="<?= $e($deliveryMethodId) ?>">
<input type="text" class="form-control" id="shipment-service-search" placeholder="Szukaj uslugi dostawy Allegro..." autocomplete="off"> <input type="text" class="form-control" id="shipment-service-search" placeholder="Szukaj usługi dostawy Allegro..." autocomplete="off">
<div class="searchable-select__dropdown" id="shipment-service-dropdown"> <div class="searchable-select__dropdown" id="shipment-service-dropdown">
<?php foreach ($services as $svc): ?> <?php foreach ($services as $svc): ?>
<?php <?php
@@ -160,10 +160,10 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<div id="shipment-inpost-panel" style="<?= $preselectedCarrier !== 'inpost' ? 'display:none' : '' ?>"> <div id="shipment-inpost-panel" style="<?= $preselectedCarrier !== 'inpost' ? 'display:none' : '' ?>">
<?php if ($inpostSvcList === []): ?> <?php if ($inpostSvcList === []): ?>
<div class="muted">Brak uslug InPost (sprawdz konfiguracje InPost).</div> <div class="muted">Brak usług InPost (sprawdz konfiguracje InPost).</div>
<?php else: ?> <?php else: ?>
<select class="form-control" id="shipment-inpost-select"> <select class="form-control" id="shipment-inpost-select">
<option value="">-- Wybierz usluge InPost --</option> <option value="">-- Wybierz usługe InPost --</option>
<?php foreach ($inpostSvcList as $inSvc): ?> <?php foreach ($inpostSvcList as $inSvc): ?>
<?php <?php
$inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : []; $inSvcId = is_array($inSvc['id'] ?? null) ? $inSvc['id'] : [];
@@ -187,10 +187,10 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<div id="shipment-apaczka-panel" style="<?= $preselectedCarrier !== 'apaczka' ? 'display:none' : '' ?>"> <div id="shipment-apaczka-panel" style="<?= $preselectedCarrier !== 'apaczka' ? 'display:none' : '' ?>">
<?php if ($apaczkaSvcList === []): ?> <?php if ($apaczkaSvcList === []): ?>
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div> <div class="muted">Brak usług Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
<?php else: ?> <?php else: ?>
<select class="form-control" id="shipment-apaczka-select"> <select class="form-control" id="shipment-apaczka-select">
<option value="">-- Wybierz usluge Apaczka --</option> <option value="">-- Wybierz usługe Apaczka --</option>
<?php foreach ($apaczkaSvcList as $aSvc): ?> <?php foreach ($apaczkaSvcList as $aSvc): ?>
<?php <?php
if (!is_array($aSvc)) { if (!is_array($aSvc)) {
@@ -221,16 +221,16 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<input type="checkbox" name="weekend_delivery" id="shipment-apaczka-weekend" value="1"> <input type="checkbox" name="weekend_delivery" id="shipment-apaczka-weekend" value="1">
Dostawa w weekend (sobota) Dostawa w weekend (sobota)
</label> </label>
<div class="muted" style="font-size:12px">Dostepne dla paczkomatow InPost. Etykiety mozna generowac od czwartku 20:00 do piatku 18:00.</div> <div class="muted" style="font-size:12px">Dostępne dla paczkomatow InPost. Etykiety można generowac od czwartku 20:00 do piatku 18:00.</div>
</div> </div>
</div> </div>
<div id="shipment-polkurier-panel" style="<?= $preselectedCarrier !== 'polkurier' ? 'display:none' : '' ?>"> <div id="shipment-polkurier-panel" style="<?= $preselectedCarrier !== 'polkurier' ? 'display:none' : '' ?>">
<?php if ($polkurierSvcList === []): ?> <?php if ($polkurierSvcList === []): ?>
<div class="muted">Brak uslug polkurier (sprawdz konfiguracje w Ustawienia &rarr; Integracje &rarr; polkurier).</div> <div class="muted">Brak usług polkurier (sprawdz konfiguracje w Ustawienia &rarr; Integracje &rarr; polkurier).</div>
<?php else: ?> <?php else: ?>
<select class="form-control" id="shipment-polkurier-select"> <select class="form-control" id="shipment-polkurier-select">
<option value="">-- Wybierz usluge polkurier --</option> <option value="">-- Wybierz usługe polkurier --</option>
<?php foreach ($polkurierSvcList as $pSvc): ?> <?php foreach ($polkurierSvcList as $pSvc): ?>
<?php <?php
if (!is_array($pSvc)) { if (!is_array($pSvc)) {
@@ -251,7 +251,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div class="muted mt-4" style="font-size:12px">Dla uslug paczkomatowych wpisz ID punktu w pole "Punkt odbioru" w sekcji Adres odbiorcy ponizej (np. <code>POP-RZE54</code>).</div> <div class="muted mt-4" style="font-size:12px">Dla usług paczkomatowych wpisz ID punktu w pole "Punkt odbioru" w sekcji Adres odbiorcy poniżej (np. <code>POP-RZE54</code>).</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -279,7 +279,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<div class="form-grid-4"> <div class="form-grid-4">
<label class="form-field"> <label class="form-field">
<span class="field-label">Dlugosc (cm)</span> <span class="field-label">Długość (cm)</span>
<input class="form-control" type="number" name="length_cm" step="0.1" min="0.1" value="<?= $e((string) ($comp['default_package_length_cm'] ?? '25')) ?>"> <input class="form-control" type="number" name="length_cm" step="0.1" min="0.1" value="<?= $e((string) ($comp['default_package_length_cm'] ?? '25')) ?>">
</label> </label>
<label class="form-field"> <label class="form-field">
@@ -329,7 +329,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<h3 class="section-title">Adres odbiorcy</h3> <h3 class="section-title">Adres odbiorcy</h3>
<label class="form-field mt-12"> <label class="form-field mt-12">
<span class="field-label">Imie i nazwisko</span> <span class="field-label">Imię i nazwisko</span>
<input class="form-control" type="text" name="receiver_name" maxlength="200" value="<?= $e((string) ($receiver['name'] ?? '')) ?>" required> <input class="form-control" type="text" name="receiver_name" maxlength="200" value="<?= $e((string) ($receiver['name'] ?? '')) ?>" required>
</label> </label>
@@ -384,11 +384,11 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
</div> </div>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Pozycje zamowienia (<?= $e((string) count($itemsList)) ?>)</h3> <h3 class="section-title">Pozycje zamówienia (<?= $e((string) count($itemsList)) ?>)</h3>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
<tr><th>Lp.</th><th>Nazwa</th><th>Ilosc</th><th>Cena</th></tr> <tr><th>Lp.</th><th>Nazwa</th><th>Ilość</th><th>Cena</th></tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($itemsList as $idx => $item): ?> <?php foreach ($itemsList as $idx => $item): ?>
@@ -404,7 +404,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
</div> </div>
<div class="form-actions mt-16"> <div class="form-actions mt-16">
<button type="submit" class="btn btn--primary">Utworz przesylke</button> <button type="submit" class="btn btn--primary">Utworz przesyłkę</button>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>" class="btn btn--secondary">Anuluj</a> <a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>" class="btn btn--secondary">Anuluj</a>
</div> </div>
</section> </section>
@@ -412,7 +412,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<?php if ($packages !== []): ?> <?php if ($packages !== []): ?>
<section class="card mt-16"> <section class="card mt-16">
<h3 class="section-title">Utworzone przesylki</h3> <h3 class="section-title">Utworzone przesyłki</h3>
<div class="table-wrap mt-12"> <div class="table-wrap mt-12">
<table class="table table--details"> <table class="table table--details">
<thead> <thead>
@@ -466,7 +466,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<td style="white-space:nowrap"> <td style="white-space:nowrap">
<?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php <?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php
$pkgTrackUrl = DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, trim((string) ($pkg['carrier_id'] ?? ''))); $pkgTrackUrl = DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, trim((string) ($pkg['carrier_id'] ?? '')));
if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesylke">&#128279;</a><?php endif; ?> if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesyłkę">&#128279;</a><?php endif; ?>
</td> </td>
<td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td> <td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td>
<td> <td>
@@ -495,7 +495,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
class="btn btn--sm btn--secondary btn-print-label" class="btn btn--sm btn--secondary btn-print-label"
data-package-id="<?= $e((string) $pkgId) ?>" data-package-id="<?= $e((string) $pkgId) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
title="Wyslij do drukarki">Drukuj</button> title="Wyślij do drukarki">Drukuj</button>
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
</td> </td>
@@ -510,7 +510,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
data-check-status="<?= $e((string) $pkgId) ?>" data-check-status="<?= $e((string) $pkgId) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
data-package-status="<?= $e($pkgStatus) ?>" data-package-status="<?= $e($pkgStatus) ?>"
data-auto-check="1">Sprawdz status</button> data-auto-check="1">Sprawdź status</button>
<?php endif; ?> <?php endif; ?>
</td> </td>
</tr> </tr>
@@ -851,26 +851,26 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
setTimeout(function () { setTimeout(function () {
checkPackageStatus(pkgId, oId, btn, attempt + 1); checkPackageStatus(pkgId, oId, btn, attempt + 1);
}, delayCreated); }, delayCreated);
if (btn) btn.textContent = 'Generuje etykiete... (' + (attempt + 1) + ')'; if (btn) btn.textContent = 'Generuje etykietę... (' + (attempt + 1) + ')';
} else if (btn) { } else if (btn) {
btn.textContent = 'W toku... Odswiez status'; btn.textContent = 'W toku... Odśwież status';
btn.disabled = false; btn.disabled = false;
} }
} else if (data.status === 'error') { } else if (data.status === 'error') {
if (btn) { if (btn) {
btn.textContent = 'Blad: ' + (data.error || ''); btn.textContent = 'Błąd: ' + (data.error || '');
btn.disabled = false; btn.disabled = false;
} }
} else { } else {
if (btn) { if (btn) {
btn.textContent = 'W toku... Sprobuj ponownie'; btn.textContent = 'W toku... Spróbuj ponownie';
btn.disabled = false; btn.disabled = false;
} }
} }
}) })
.catch(function () { .catch(function () {
if (btn) { if (btn) {
btn.textContent = 'Blad sieci'; btn.textContent = 'Błąd sieci';
btn.disabled = false; btn.disabled = false;
} }
}); });
@@ -901,7 +901,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
btn.classList.remove('js-print-queue-pending'); btn.classList.remove('js-print-queue-pending');
btn.classList.add('btn--secondary'); btn.classList.add('btn--secondary');
btn.classList.add('btn-print-label'); btn.classList.add('btn-print-label');
btn.setAttribute('title', 'Wyslij do drukarki'); btn.setAttribute('title', 'Wyślij do drukarki');
} }
function stopPrintQueuePoll() { function stopPrintQueuePoll() {
@@ -946,7 +946,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
if (!packageId) return; if (!packageId) return;
btn.disabled = true; btn.disabled = true;
var originalText = btn.innerHTML; var originalText = btn.innerHTML;
btn.innerHTML = 'Wysylam...'; btn.innerHTML = 'Wysyłam...';
fetch('/api/print/jobs', { fetch('/api/print/jobs', {
method: 'POST', method: 'POST',
@@ -965,7 +965,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
btn.classList.add('js-print-queue-pending'); btn.classList.add('js-print-queue-pending');
watchPrintQueueButton(btn); watchPrintQueueButton(btn);
} else { } else {
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad'; var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany błąd';
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: msg, type: 'error' }); window.OrderProAlerts.show({ message: msg, type: 'error' });
} }
@@ -975,7 +975,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
}) })
.catch(function () { .catch(function () {
if (window.OrderProAlerts) { if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: 'Blad sieci — sprobuj ponownie.', type: 'error' }); window.OrderProAlerts.show({ message: 'Błąd sieci — spróbuj ponownie.', type: 'error' });
} }
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false; btn.disabled = false;
@@ -983,7 +983,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
}); });
}); });
// Przy zaladowaniu strony: uruchom polling dla przyciskow juz w kolejce // Przy zaladowaniu strony: uruchom polling dla przyciskow już w kolejce
document.querySelectorAll('.js-print-queue-pending').forEach(function (btn) { document.querySelectorAll('.js-print-queue-pending').forEach(function (btn) {
watchPrintQueueButton(btn); watchPrintQueueButton(btn);
}); });
@@ -1137,7 +1137,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
} }
// --- Apply preset (autofill form) --- // --- Apply preset (autofill form) ---
// Preset nadpisuje wylacznie rozmiary paczki i wage. Forma dostawy (carrier, // Preset nadpisuje wyłącznie rozmiary paczki i wage. Forma dostawy (carrier,
// serwis, punkt nadania, format etykiety) pozostaje bez zmian. // serwis, punkt nadania, format etykiety) pozostaje bez zmian.
function applyPreset(preset) { function applyPreset(preset) {
setFieldValue('package_type', preset.package_type || 'PACKAGE'); setFieldValue('package_type', preset.package_type || 'PACKAGE');
@@ -1342,10 +1342,10 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
function deletePreset(preset) { function deletePreset(preset) {
if (window.OrderProAlerts && window.OrderProAlerts.confirm) { if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
window.OrderProAlerts.confirm({ window.OrderProAlerts.confirm({
message: 'Usun\u0105\u0107 przycisk "' + preset.name + '"?', message: 'Usuń\u0105\u0107 przycisk "' + preset.name + '"?',
onConfirm: function () { executeDelete(preset.id); } onConfirm: function () { executeDelete(preset.id); }
}); });
} else if (confirm('Usun\u0105\u0107 przycisk "' + preset.name + '"?')) { } else if (confirm('Usuń\u0105\u0107 przycisk "' + preset.name + '"?')) {
executeDelete(preset.id); executeDelete(preset.id);
} }
} }

2
scripts/codex-export.cmd Normal file
View File

@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-export.ps1" %*

83
scripts/codex-export.ps1 Normal file
View File

@@ -0,0 +1,83 @@
param(
[string]$OutputDir = "D:\notatnik-ai\codex",
[string]$SourceCodexHome = (Join-Path $env:USERPROFILE ".codex")
)
$ErrorActionPreference = "Stop"
function New-CleanDirectory {
param([Parameter(Mandatory = $true)][string]$Path)
if (Test-Path -LiteralPath $Path) {
Remove-Item -LiteralPath $Path -Recurse -Force
}
New-Item -ItemType Directory -Path $Path -Force | Out-Null
}
function Copy-DirectoryContents {
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][string]$Destination
)
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
Get-ChildItem -LiteralPath $Source -Force | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $Destination -Recurse -Force
}
}
if (-not (Test-Path -LiteralPath $SourceCodexHome)) {
throw "Codex home not found: $SourceCodexHome"
}
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$archiveBaseName = "codex-backup-$timestamp"
$stagingRoot = Join-Path $env:TEMP $archiveBaseName
$payloadDir = Join-Path $stagingRoot "payload"
$toolsDir = Join-Path $stagingRoot "tools"
$sourceScriptsDir = $PSScriptRoot
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
New-CleanDirectory -Path $stagingRoot
New-Item -ItemType Directory -Path $payloadDir -Force | Out-Null
New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
Copy-DirectoryContents -Source $SourceCodexHome -Destination (Join-Path $payloadDir ".codex")
$profileCodexFiles = Get-ChildItem -LiteralPath $env:USERPROFILE -Force -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like ".codex*" -and $_.FullName -ne $SourceCodexHome }
foreach ($file in $profileCodexFiles) {
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $payloadDir $file.Name) -Force
}
Copy-Item -LiteralPath (Join-Path $sourceScriptsDir "codex-import.ps1") -Destination (Join-Path $toolsDir "codex-import.ps1") -Force
Copy-Item -LiteralPath (Join-Path $sourceScriptsDir "codex-import.cmd") -Destination (Join-Path $toolsDir "codex-import.cmd") -Force
$files = Get-ChildItem -LiteralPath $payloadDir -Force -Recurse -File
$manifest = [ordered]@{
kind = "codex-local-backup"
createdAt = (Get-Date).ToString("o")
sourceCodexHome = $SourceCodexHome
sourceUserProfile = $env:USERPROFILE
payloadRoot = "payload"
tools = @(
"tools/codex-import.ps1",
"tools/codex-import.cmd"
)
fileCount = @($files).Count
totalBytes = [int64](($files | Measure-Object -Property Length -Sum).Sum)
}
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $stagingRoot "manifest.json") -Encoding UTF8
$zipPath = Join-Path $OutputDir "$archiveBaseName.zip"
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
}
Compress-Archive -Path (Join-Path $stagingRoot "*") -DestinationPath $zipPath -Force
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
Write-Output $zipPath

2
scripts/codex-import.cmd Normal file
View File

@@ -0,0 +1,2 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-import.ps1" %*

62
scripts/codex-import.ps1 Normal file
View File

@@ -0,0 +1,62 @@
param(
[Parameter(Mandatory = $true)][string]$ArchivePath,
[string]$TargetCodexHome = (Join-Path $env:USERPROFILE ".codex"),
[switch]$NoBackup
)
$ErrorActionPreference = "Stop"
function New-CleanDirectory {
param([Parameter(Mandatory = $true)][string]$Path)
if (Test-Path -LiteralPath $Path) {
Remove-Item -LiteralPath $Path -Recurse -Force
}
New-Item -ItemType Directory -Path $Path -Force | Out-Null
}
if (-not (Test-Path -LiteralPath $ArchivePath)) {
throw "Archive not found: $ArchivePath"
}
$extractRoot = Join-Path $env:TEMP ("codex-restore-" + (Get-Date -Format "yyyyMMdd-HHmmss"))
New-CleanDirectory -Path $extractRoot
try {
Expand-Archive -LiteralPath $ArchivePath -DestinationPath $extractRoot -Force
$manifestPath = Join-Path $extractRoot "manifest.json"
$payloadCodexHome = Join-Path $extractRoot "payload\.codex"
if (-not (Test-Path -LiteralPath $manifestPath)) {
throw "Invalid archive: manifest.json missing"
}
if (-not (Test-Path -LiteralPath $payloadCodexHome)) {
throw "Invalid archive: payload/.codex missing"
}
if ((Test-Path -LiteralPath $TargetCodexHome) -and -not $NoBackup) {
$backupPath = "$TargetCodexHome.before-import-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Move-Item -LiteralPath $TargetCodexHome -Destination $backupPath
Write-Output "Existing Codex config moved to: $backupPath"
} elseif (Test-Path -LiteralPath $TargetCodexHome) {
Remove-Item -LiteralPath $TargetCodexHome -Recurse -Force
}
Copy-Item -LiteralPath $payloadCodexHome -Destination $TargetCodexHome -Recurse -Force
$payloadRoot = Join-Path $extractRoot "payload"
Get-ChildItem -LiteralPath $payloadRoot -Force -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like ".codex*" } |
ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $env:USERPROFILE $_.Name) -Force
}
Write-Output "Codex config restored to: $TargetCodexHome"
} finally {
if (Test-Path -LiteralPath $extractRoot) {
Remove-Item -LiteralPath $extractRoot -Recurse -Force
}
}

View File

@@ -70,7 +70,7 @@ final class InvoiceController
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];
if ((int) ($order['invoice_requested'] ?? 0) !== 1) { if ((int) ($order['invoice_requested'] ?? 0) !== 1) {
Flash::set('order.error', 'Faktura nie zostala zazadana dla tego zamowienia.'); Flash::set('order.error', 'Faktura nie zostala zazadana dla tego zamówienia.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -79,7 +79,7 @@ final class InvoiceController
static fn (array $c): bool => (int) ($c['is_active'] ?? 0) === 1 static fn (array $c): bool => (int) ($c['is_active'] ?? 0) === 1
)); ));
if ($configs === []) { if ($configs === []) {
Flash::set('order.error', 'Brak aktywnych konfiguracji faktur. Skonfiguruj w Ustawienia > Ksiegowosc > Faktury.'); Flash::set('order.error', 'Brak aktywnych konfiguracji faktur. Skonfiguruj w Ustawienia > Księgowość > Faktury.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -99,7 +99,7 @@ final class InvoiceController
$existingInvoices = $this->invoices->findByOrderId($orderId); $existingInvoices = $this->invoices->findByOrderId($orderId);
$html = $this->template->render('accounting/invoice_form', [ $html = $this->template->render('accounting/invoice_form', [
'title' => 'Wystaw fakture', 'title' => 'Wystaw fakturę',
'activeMenu' => 'orders', 'activeMenu' => 'orders',
'activeOrders' => 'list', 'activeOrders' => 'list',
'user' => $this->auth->user(), 'user' => $this->auth->user(),
@@ -123,7 +123,7 @@ final class InvoiceController
$orderId = max(0, (int) $request->input('id', 0)); $orderId = max(0, (int) $request->input('id', 0));
if (!Csrf::validate((string) $request->input('_token', ''))) { if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('order.error', 'Nieprawidlowy token CSRF.'); Flash::set('order.error', 'Nieprawidłowy token CSRF.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -154,7 +154,7 @@ final class InvoiceController
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'invoice_issued', 'invoice_issued',
($result['mode'] === 'delegated' ? 'Wystawiono fakture (Fakturownia): ' : 'Wystawiono fakture: ') . $result['invoice_number'], ($result['mode'] === 'delegated' ? 'Wystawiono fakturę (Fakturownia): ' : 'Wystawiono fakturę: ') . $result['invoice_number'],
[ [
'invoice_number' => $result['invoice_number'], 'invoice_number' => $result['invoice_number'],
'config_id' => $configId, 'config_id' => $configId,
@@ -170,7 +170,7 @@ final class InvoiceController
} catch (InvoiceIssueException $e) { } catch (InvoiceIssueException $e) {
Flash::set('invoice.error', $e->getMessage()); Flash::set('invoice.error', $e->getMessage());
} catch (Throwable $e) { } catch (Throwable $e) {
Flash::set('invoice.error', 'Blad wystawiania faktury: ' . $e->getMessage()); Flash::set('invoice.error', 'Błąd wystawiania faktury: ' . $e->getMessage());
} }
return Response::redirect('/orders/' . $orderId . '/invoice/create'); return Response::redirect('/orders/' . $orderId . '/invoice/create');

View File

@@ -46,12 +46,12 @@ final class InvoiceService
$config = $this->invoiceConfigs->findById($configId); $config = $this->invoiceConfigs->findById($configId);
if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) { if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) {
throw new InvoiceIssueException('Nieprawidlowa lub nieaktywna konfiguracja faktury.'); throw new InvoiceIssueException('Nieprawidłowa lub nieaktywna konfiguracja faktury.');
} }
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
throw new InvoiceIssueException('Zamowienie nie istnieje.'); throw new InvoiceIssueException('Zamówienie nie istnieje.');
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];
@@ -266,7 +266,7 @@ final class InvoiceService
$message = 'Fakturownia: ' . $e->getMessage(); $message = 'Fakturownia: ' . $e->getMessage();
$this->invoices->markDelegatedExternalFailed($invoiceId, $message); $this->invoices->markDelegatedExternalFailed($invoiceId, $message);
throw new InvoiceIssueException($message . ' Sprobuj ponownie - orderPRO uzyje tego samego oid i najpierw sprawdzi Fakturownie.'); throw new InvoiceIssueException($message . ' Spróbuj ponownie - orderPRO użyje tego samego oid i najpierw sprawdźi Fakturownie.');
} }
return $this->finalizeDelegatedInvoice($invoiceId, $response); return $this->finalizeDelegatedInvoice($invoiceId, $response);
@@ -539,7 +539,7 @@ final class InvoiceService
$totalGross += $deliveryPrice; $totalGross += $deliveryPrice;
$totalNet += $deliveryNet; $totalNet += $deliveryNet;
$itemsSnapshot[] = [ $itemsSnapshot[] = [
'name' => 'Koszt wysylki', 'name' => 'Koszt wysyłki',
'quantity' => 1.0, 'quantity' => 1.0,
'price_gross' => $deliveryPrice, 'price_gross' => $deliveryPrice,
'price_net' => $deliveryNet, 'price_net' => $deliveryNet,
@@ -583,12 +583,12 @@ final class InvoiceService
$saleDay = substr($saleDate, 0, 10); $saleDay = substr($saleDate, 0, 10);
$dueDay = substr($paymentDueDate, 0, 10); $dueDay = substr($paymentDueDate, 0, 10);
// UWAGA: seller_* pola CELOWO pominiete. Konta Fakturowni z podwyzszonym // UWAGA: seller_* pola CELOWO pominięte. Konta Fakturowni z podwyzszonym
// poziomem zabezpieczen interpretuja roznice w seller_name/tax_no/bank // poziomem zabezpieczen interpretuja roznice w seller_name/tax_no/bank
// jako proba "utworzenia nowego dzialu" i odrzucaja request HTTP 422 // jako proba "utworzenia nowego dzialu" i odrzucaja request HTTP 422
// ("Poziom zabezpieczenia przed zmiana konta bankowego nie pozwala na // ("Poziom zabezpieczenia przed zmiana konta bankowego nie pozwala na
// utworzenie dzialu"). Fakturownia uzywa wtedy danych sprzedawcy // utworzenie dzialu"). Fakturownia uzywa wtedy danych sprzedawcy
// zarejestrowanych na koncie (uzytkownik IS sprzedawca w Fakturowni). // zarejestrowanych na koncie (użytkownik IS sprzedawca w Fakturowni).
// Lokalny snapshot `seller_data_json` w tabeli `invoices` zachowuje // Lokalny snapshot `seller_data_json` w tabeli `invoices` zachowuje
// dane orderPRO dla audytu — niezalezne od tego co poszlo do Fakturowni. // dane orderPRO dla audytu — niezalezne od tego co poszlo do Fakturowni.
$invoice = [ $invoice = [
@@ -623,10 +623,10 @@ final class InvoiceService
$descriptionReference = $orderReference !== '' ? $orderReference : $externalOid; $descriptionReference = $orderReference !== '' ? $orderReference : $externalOid;
if ($descriptionReference !== '') { if ($descriptionReference !== '') {
$invoice['additional_info_desc'] = 'Zamowienie: ' . $descriptionReference; $invoice['additional_info_desc'] = 'Zamówienie: ' . $descriptionReference;
} }
// department_id celowo pominiete — konta Fakturowni z podwyzszonym // department_id celowo pominięte — konta Fakturowni z podwyzszonym
// poziomem zabezpieczen odrzucaja ten parametr przez API (HTTP 422 // poziomem zabezpieczen odrzucaja ten parametr przez API (HTTP 422
// "Poziom zabezpieczenia ... nie pozwala na utworzenie dzialu"). // "Poziom zabezpieczenia ... nie pozwala na utworzenie dzialu").
// Fakturownia uzywa wtedy domyslnego dzialu konta. // Fakturownia uzywa wtedy domyslnego dzialu konta.
@@ -712,7 +712,7 @@ final class InvoiceService
$externalPdfUrl = trim((string) ($remote['pdf_url'] ?? $remote['view_url'] ?? '')); $externalPdfUrl = trim((string) ($remote['pdf_url'] ?? $remote['view_url'] ?? ''));
if ($externalId === '' || $externalNumber === '') { if ($externalId === '' || $externalNumber === '') {
throw new InvoiceIssueException('Fakturownia zwrocila niekompletna odpowiedz (brak id/number).'); throw new InvoiceIssueException('Fakturownia zwróciła niekompletną odpowiedź (brak id/number).');
} }
$this->invoices->finalizeDelegatedExternal($invoiceId, [ $this->invoices->finalizeDelegatedExternal($invoiceId, [

View File

@@ -77,7 +77,7 @@ final class ReceiptController
$orderId = max(0, (int) $request->input('id', 0)); $orderId = max(0, (int) $request->input('id', 0));
if (!Csrf::validate((string) $request->input('_token', ''))) { if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('order.error', 'Nieprawidlowy token CSRF'); Flash::set('order.error', 'Nieprawidłowy token CSRF');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -116,7 +116,7 @@ final class ReceiptController
} catch (ReceiptIssueException $e) { } catch (ReceiptIssueException $e) {
Flash::set('order.error', $e->getMessage()); Flash::set('order.error', $e->getMessage());
} catch (Throwable) { } catch (Throwable) {
Flash::set('order.error', 'Blad wystawiania paragonu'); Flash::set('order.error', 'Błąd wystawiania paragonu');
} }
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);

View File

@@ -36,12 +36,12 @@ final class ReceiptService
$config = $this->receiptConfigs->findById($configId); $config = $this->receiptConfigs->findById($configId);
if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) { if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) {
throw new ReceiptIssueException('Nieprawidlowa lub nieaktywna konfiguracja paragonu'); throw new ReceiptIssueException('Nieprawidłowa lub nieaktywna konfiguracja paragonu');
} }
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
throw new ReceiptIssueException('Zamowienie nie istnieje'); throw new ReceiptIssueException('Zamówienie nie istnieje');
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];
@@ -287,7 +287,7 @@ final class ReceiptService
$totalGross += $deliveryPrice; $totalGross += $deliveryPrice;
$totalNet += $deliveryNet; $totalNet += $deliveryNet;
$itemsSnapshot[] = [ $itemsSnapshot[] = [
'name' => 'Koszt wysylki', 'name' => 'Koszt wysyłki',
'quantity' => 1.0, 'quantity' => 1.0,
'price' => $deliveryPrice, 'price' => $deliveryPrice,
'total' => round($deliveryPrice, 2), 'total' => round($deliveryPrice, 2),

View File

@@ -29,9 +29,9 @@ final class AutomationController
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date']; 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 ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
private const PAYMENT_METHOD_OPTIONS = [ private const PAYMENT_METHOD_OPTIONS = [
'cod' => 'Platnosc przy odbiorze (COD)', 'cod' => 'Płatność przy odbiorze (COD)',
'transfer' => 'Przelew bankowy', 'transfer' => 'Przelew bankowy',
'online' => 'Karta / platnosc online', 'online' => 'Karta / płatność online',
'other' => 'Inna', 'other' => 'Inna',
]; ];
public function __construct( public function __construct(
@@ -120,7 +120,7 @@ final class AutomationController
); );
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo utworzone'); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo utworzone');
} catch (Throwable) { } catch (Throwable) {
return $this->renderForm($this->buildRuleFromRequest($request), 'Blad zapisu zadania automatycznego'); return $this->renderForm($this->buildRuleFromRequest($request), 'Błąd zapisu zadania automatycznego');
} }
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
@@ -135,7 +135,7 @@ final class AutomationController
$id = (int) $request->input('id', '0'); $id = (int) $request->input('id', '0');
if ($id <= 0) { if ($id <= 0) {
Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
} }
@@ -153,7 +153,7 @@ final class AutomationController
); );
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo zaktualizowane'); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo zaktualizowane');
} catch (Throwable) { } catch (Throwable) {
return $this->renderForm($this->buildRuleFromRequest($request, $id), 'Blad aktualizacji zadania automatycznego'); return $this->renderForm($this->buildRuleFromRequest($request, $id), 'Błąd aktualizacji zadania automatycznego');
} }
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
@@ -168,15 +168,15 @@ final class AutomationController
$id = (int) $request->input('id', '0'); $id = (int) $request->input('id', '0');
if ($id <= 0) { if ($id <= 0) {
Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
} }
try { try {
$this->repository->delete($id); $this->repository->delete($id);
Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo usuniete'); Flash::set('settings.automation.success', 'Zadanie automatyczne zostalo usuńięte');
} catch (Throwable) { } catch (Throwable) {
Flash::set('settings.automation.error', 'Blad usuwania zadania automatycznego'); Flash::set('settings.automation.error', 'Błąd usuwania zadania automatycznego');
} }
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
@@ -191,7 +191,7 @@ final class AutomationController
$id = (int) $request->input('id', '0'); $id = (int) $request->input('id', '0');
if ($id <= 0) { if ($id <= 0) {
Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
} }
@@ -199,7 +199,7 @@ final class AutomationController
$this->repository->duplicate($id); $this->repository->duplicate($id);
Flash::set('settings.automation.success', 'Zadanie zostalo zduplikowane'); Flash::set('settings.automation.success', 'Zadanie zostalo zduplikowane');
} catch (Throwable) { } catch (Throwable) {
Flash::set('settings.automation.error', 'Blad duplikowania zadania'); Flash::set('settings.automation.error', 'Błąd duplikowania zadania');
} }
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
@@ -214,15 +214,15 @@ final class AutomationController
$id = (int) $request->input('id', '0'); $id = (int) $request->input('id', '0');
if ($id <= 0) { if ($id <= 0) {
Flash::set('settings.automation.error', 'Nieprawidlowy identyfikator'); Flash::set('settings.automation.error', 'Nieprawidłowy identyfikator');
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
} }
try { try {
$this->repository->toggleActive($id); $this->repository->toggleActive($id);
Flash::set('settings.automation.success', 'Status zadania zostal zmieniony'); Flash::set('settings.automation.success', 'Status zadania zostal zmieńiony');
} catch (Throwable) { } catch (Throwable) {
Flash::set('settings.automation.error', 'Blad zmiany statusu'); Flash::set('settings.automation.error', 'Błąd zmiany statusu');
} }
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
@@ -316,7 +316,7 @@ final class AutomationController
private function validateCsrf(Request $request): ?Response private function validateCsrf(Request $request): ?Response
{ {
if (!Csrf::validate((string) $request->input('_token', ''))) { if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings.automation.error', 'Nieprawidlowy token CSRF'); Flash::set('settings.automation.error', 'Nieprawidłowy token CSRF');
return Response::redirect('/settings/automation'); return Response::redirect('/settings/automation');
} }
@@ -327,12 +327,12 @@ final class AutomationController
{ {
$name = trim((string) $request->input('name', '')); $name = trim((string) $request->input('name', ''));
if ($name === '' || mb_strlen($name) > 128) { if ($name === '' || mb_strlen($name) > 128) {
return 'Nazwa jest wymagana (maks. 128 znakow)'; return 'Nazwa jest wymagana (maks. 128 znaków)';
} }
$eventType = (string) $request->input('event_type', ''); $eventType = (string) $request->input('event_type', '');
if (!in_array($eventType, self::ALLOWED_EVENTS, true)) { if (!in_array($eventType, self::ALLOWED_EVENTS, true)) {
return 'Nieprawidlowy typ zdarzenia'; return 'Nieprawidłowy typ zdarzenia';
} }
$conditions = $this->extractConditions($request); $conditions = $this->extractConditions($request);

View File

@@ -87,7 +87,7 @@ final class AutomationService
$exception->getMessage(), $exception->getMessage(),
$context $context
); );
// Blad jednej reguly nie blokuje kolejnych // Błąd jednej reguly nie blokuje kolejnych
} }
} }
} }
@@ -428,7 +428,7 @@ final class AutomationService
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'automation_receipt_skipped', 'automation_receipt_skipped',
$actorName . ' - pomieto, paragon juz istnieje dla zamowienia', $actorName . ' - pominięto, paragon już istnieje dla zamówienia',
['duplicate_policy' => $duplicatePolicy, 'existing_count' => count($existingReceipts)], ['duplicate_policy' => $duplicatePolicy, 'existing_count' => count($existingReceipts)],
'system', 'system',
$actorName $actorName
@@ -477,7 +477,7 @@ final class AutomationService
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'automation_receipt_failed', 'automation_receipt_failed',
$actorName . ' - blad wystawiania paragonu', $actorName . ' - błąd wystawiania paragonu',
['error' => $exception->getMessage(), 'receipt_config_id' => $configId], ['error' => $exception->getMessage(), 'receipt_config_id' => $configId],
'system', 'system',
$actorName $actorName
@@ -521,7 +521,7 @@ final class AutomationService
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'automation_shipment_status_updated', 'automation_shipment_status_updated',
$actorName . ' - zaktualizowano status przesylki', $actorName . ' - zaktualizowano status przesyłki',
[ [
'package_id' => $packageId, 'package_id' => $packageId,
'previous_status' => $previousStatus, 'previous_status' => $previousStatus,
@@ -572,7 +572,7 @@ final class AutomationService
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'automation_order_status_failed', 'automation_order_status_failed',
$actorName . ' - nie udalo sie zmienic statusu zamowienia', $actorName . ' - nie udalo się zmienić statusu zamówienia',
['target_status_code' => $statusCode], ['target_status_code' => $statusCode],
'system', 'system',
$actorName $actorName
@@ -764,7 +764,7 @@ final class AutomationService
'executed_at' => date('Y-m-d H:i:s'), 'executed_at' => date('Y-m-d H:i:s'),
]); ]);
} catch (Throwable) { } catch (Throwable) {
// Historia automatyzacji nie moze blokowac glownego flow. // Historia automatyzacji nie może blokowac głównego flow.
} }
} }

View File

@@ -30,7 +30,7 @@ final class OrderStatusAgedService
try { try {
$totalTriggered += $this->processRule($rule); $totalTriggered += $this->processRule($rule);
} catch (Throwable) { } catch (Throwable) {
// Blad jednej reguly nie blokuje kolejnych // Błąd jednej reguly nie blokuje kolejnych
} }
} }
@@ -71,7 +71,7 @@ final class OrderStatusAgedService
]); ]);
$triggered++; $triggered++;
} catch (Throwable) { } catch (Throwable) {
// Blad jednego zamowienia nie blokuje kolejnych // Błąd jednego zamówienia nie blokuje kolejnych
} }
} }

View File

@@ -25,7 +25,7 @@ final class AllegroTokenRefreshHandler
{ {
$credentials = $this->repository->getRefreshTokenCredentials(); $credentials = $this->repository->getRefreshTokenCredentials();
if ($credentials === null) { if ($credentials === null) {
throw new RuntimeException('Brak kompletnych danych Allegro OAuth do odswiezenia tokenu.'); throw new RuntimeException('Brak kompletnych danych Allegro OAuth do odświeżenia tokenu.');
} }
$token = $this->oauthClient->refreshAccessToken( $token = $this->oauthClient->refreshAccessToken(

View File

@@ -32,7 +32,7 @@ final class EmailSendingService
{ {
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
return ['success' => false, 'error' => 'Zamowienie nie znalezione', 'log_id' => 0]; return ['success' => false, 'error' => 'Zamówienie nie znalezione', 'log_id' => 0];
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];
@@ -103,7 +103,7 @@ final class EmailSendingService
$templateName = (string) ($template['name'] ?? ''); $templateName = (string) ($template['name'] ?? '');
$activitySummary = $status === 'sent' $activitySummary = $status === 'sent'
? 'Wyslano e-mail "' . $resolvedSubject . '" do ' . $recipientEmail ? 'Wyslano e-mail "' . $resolvedSubject . '" do ' . $recipientEmail
: 'Blad wysylki e-mail "' . $resolvedSubject . '" do ' . $recipientEmail . ': ' . ($errorMessage ?? ''); : 'Błąd wysyłki e-mail "' . $resolvedSubject . '" do ' . $recipientEmail . ': ' . ($errorMessage ?? '');
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'email_' . $status, 'email_' . $status,
@@ -127,7 +127,7 @@ final class EmailSendingService
{ {
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
return ['subject' => '', 'body_html' => '<p>Zamowienie nie znalezione</p>', 'attachments' => []]; return ['subject' => '', 'body_html' => '<p>Zamówienie nie znalezione</p>', 'attachments' => []];
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];

View File

@@ -9,7 +9,7 @@ use RuntimeException;
/** /**
* Phase 129-01: CRUD notatek autorskich operatora (note_type='user'). * Phase 129-01: CRUD notatek autorskich operatora (note_type='user').
* Importowane notatki ze zrodla (shoppro/allegro/message) maja wlasne zarzadzanie * Importowane notatki ze źródła (shoppro/allegro/message) mają własne zarządzanie
* w OrderImportRepository::replaceNotes() — ten serwis ich nie dotyka. * w OrderImportRepository::replaceNotes() — ten serwis ich nie dotyka.
*/ */
final class OrderNotesService final class OrderNotesService
@@ -96,7 +96,7 @@ final class OrderNotesService
{ {
$body = $this->sanitizeBody($body); $body = $this->sanitizeBody($body);
if ($orderId <= 0 || $userId <= 0) { if ($orderId <= 0 || $userId <= 0) {
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.'); throw new InvalidArgumentException('Nieprawidłowe parametry notatki.');
} }
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
@@ -117,13 +117,13 @@ final class OrderNotesService
} }
/** /**
* @throws RuntimeException kod 403 gdy uzytkownik nie jest autorem notatki * @throws RuntimeException kod 403 gdy użytkownik nie jest autorem notatki
*/ */
public function update(int $noteId, int $userId, string $body): void public function update(int $noteId, int $userId, string $body): void
{ {
$body = $this->sanitizeBody($body); $body = $this->sanitizeBody($body);
if ($noteId <= 0 || $userId <= 0) { if ($noteId <= 0 || $userId <= 0) {
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.'); throw new InvalidArgumentException('Nieprawidłowe parametry notatki.');
} }
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
@@ -139,17 +139,17 @@ final class OrderNotesService
]); ]);
if ($stmt->rowCount() === 0) { if ($stmt->rowCount() === 0) {
throw new RuntimeException('Brak uprawnien — tylko autor moze edytowac notatke.', 403); throw new RuntimeException('Brak uprawnien — tylko autor może edytowac notatkę.', 403);
} }
} }
/** /**
* @throws RuntimeException kod 403 gdy uzytkownik nie jest autorem notatki * @throws RuntimeException kod 403 gdy użytkownik nie jest autorem notatki
*/ */
public function delete(int $noteId, int $userId): void public function delete(int $noteId, int $userId): void
{ {
if ($noteId <= 0 || $userId <= 0) { if ($noteId <= 0 || $userId <= 0) {
throw new InvalidArgumentException('Nieprawidlowe parametry notatki.'); throw new InvalidArgumentException('Nieprawidłowe parametry notatki.');
} }
$stmt = $this->pdo->prepare( $stmt = $this->pdo->prepare(
@@ -163,7 +163,7 @@ final class OrderNotesService
]); ]);
if ($stmt->rowCount() === 0) { if ($stmt->rowCount() === 0) {
throw new RuntimeException('Brak uprawnien — tylko autor moze usunac notatke.', 403); throw new RuntimeException('Brak uprawnien — tylko autor może usuńąć notatkę.', 403);
} }
} }
@@ -171,10 +171,10 @@ final class OrderNotesService
{ {
$body = trim($body); $body = trim($body);
if ($body === '') { if ($body === '') {
throw new InvalidArgumentException('Tresc notatki nie moze byc pusta.'); throw new InvalidArgumentException('Treść notatki nie może byc pusta.');
} }
if (function_exists('mb_strlen') ? mb_strlen($body) > self::BODY_MAX_LENGTH : strlen($body) > self::BODY_MAX_LENGTH) { if (function_exists('mb_strlen') ? mb_strlen($body) > self::BODY_MAX_LENGTH : strlen($body) > self::BODY_MAX_LENGTH) {
throw new InvalidArgumentException('Tresc notatki przekracza ' . self::BODY_MAX_LENGTH . ' znakow.'); throw new InvalidArgumentException('Treść notatki przekracza ' . self::BODY_MAX_LENGTH . ' znaków.');
} }
return $body; return $body;

View File

@@ -320,7 +320,7 @@ final class OrdersController
} }
if ($orderId <= 0 || $this->smsConversation === null) { if ($orderId <= 0 || $this->smsConversation === null) {
Flash::set('order.error', 'Modul SMS nie jest dostepny.'); Flash::set('order.error', 'Modul SMS nie jest dostępny.');
return Response::redirect($redirectTo); return Response::redirect($redirectTo);
} }
@@ -335,12 +335,12 @@ final class OrdersController
); );
if ($result['ok']) { if ($result['ok']) {
Flash::set('order.success', 'SMS zostal wyslany.'); Flash::set('order.success', 'SMS zostal wysłany.');
} else { } else {
Flash::set('order.error', 'Nie udalo sie wyslac SMS: ' . $result['message']); Flash::set('order.error', 'Nie udało się wysłać SMS: ' . $result['message']);
} }
} catch (Throwable $exception) { } catch (Throwable $exception) {
Flash::set('order.error', 'Nie udalo sie wyslac SMS: ' . $exception->getMessage()); Flash::set('order.error', 'Nie udało się wysłać SMS: ' . $exception->getMessage());
} }
return Response::redirect($redirectTo); return Response::redirect($redirectTo);
@@ -352,10 +352,10 @@ final class OrdersController
$templateId = max(0, (int) $request->input('template_id', 0)); $templateId = max(0, (int) $request->input('template_id', 0));
if ($orderId <= 0 || $templateId <= 0) { if ($orderId <= 0 || $templateId <= 0) {
return Response::json(['ok' => false, 'error' => 'Nieprawidlowe parametry.'], 400); return Response::json(['ok' => false, 'error' => 'Nieprawidłowe parametry.'], 400);
} }
if ($this->smsTemplates === null || $this->smsVariableResolver === null) { if ($this->smsTemplates === null || $this->smsVariableResolver === null) {
return Response::json(['ok' => false, 'error' => 'Modul szablonow SMS nie jest dostepny.'], 500); return Response::json(['ok' => false, 'error' => 'Modul szablonow SMS nie jest dostępny.'], 500);
} }
$template = $this->smsTemplates->findById($templateId); $template = $this->smsTemplates->findById($templateId);
@@ -365,7 +365,7 @@ final class OrdersController
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
return Response::json(['ok' => false, 'error' => 'Zamowienie nie znalezione.'], 404); return Response::json(['ok' => false, 'error' => 'Zamówienie nie znalezione.'], 404);
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];
@@ -385,7 +385,7 @@ final class OrdersController
} }
/** /**
* Sklada informacje o historii zwrotow klienta biezacego zamowienia. * Składa informacje o historii zwrotów klienta bieżącego zamówienia.
* *
* @param array<string, mixed> $order * @param array<string, mixed> $order
* @return array{count:int, orders:array<int, array<string, mixed>>, email:string, phone:string, name:string, text:string} * @return array{count:int, orders:array<int, array<string, mixed>>, email:string, phone:string, name:string, text:string}
@@ -458,13 +458,13 @@ final class OrdersController
} elseif ($hasPhone) { } elseif ($hasPhone) {
$subject = 'Osoba o numerze telefonu ' . $phone; $subject = 'Osoba o numerze telefonu ' . $phone;
} elseif ($hasName) { } elseif ($hasName) {
$subject = 'Osoba o imieniu i nazwisku ' . $name; $subject = 'Osoba o imięniu i nazwisku ' . $name;
} else { } else {
$subject = 'Ten klient'; $subject = 'Ten klient';
} }
$noun = $count === 1 ? 'przesylke' : 'przesylek'; $noun = $count === 1 ? 'przesyłkę' : 'przesyłek';
return $subject . ' nie odebrala ' . $count . ' ' . $noun . '.'; return $subject . ' nie odebrała ' . $count . ' ' . $noun . '.';
} }
public function updateDetails(Request $request): Response public function updateDetails(Request $request): Response
@@ -622,7 +622,7 @@ final class OrdersController
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'invoice_requested_changed', 'invoice_requested_changed',
'Klient prosi o fakture: ' . ($value ? 'tak' : 'nie'), 'Klient prosi o fakturę: ' . ($value ? 'tak' : 'nie'),
['invoice_requested' => $value ? 1 : 0], ['invoice_requested' => $value ? 1 : 0],
'user', 'user',
$actorName !== '' ? $actorName : null $actorName !== '' ? $actorName : null
@@ -641,7 +641,7 @@ final class OrdersController
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
if ($orderId <= 0 || $this->orderNotes === null) { if ($orderId <= 0 || $this->orderNotes === null) {
Flash::set('order.error', 'Modul notatek nie jest dostepny.'); Flash::set('order.error', 'Modul notatek nie jest dostępny.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -658,7 +658,7 @@ final class OrdersController
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'note', 'note',
'Dodano notatke', 'Dodano notatkę',
null, null,
'user', 'user',
$authorName !== '' ? $authorName : null $authorName !== '' ? $authorName : null
@@ -667,7 +667,7 @@ final class OrdersController
} catch (\InvalidArgumentException $exception) { } catch (\InvalidArgumentException $exception) {
Flash::set('order.error', $exception->getMessage()); Flash::set('order.error', $exception->getMessage());
} catch (Throwable $exception) { } catch (Throwable $exception) {
Flash::set('order.error', 'Nie udalo sie dodac notatki: ' . $exception->getMessage()); Flash::set('order.error', 'Nie udało się dodać notatki: ' . $exception->getMessage());
} }
return Response::redirect($redirectTo); return Response::redirect($redirectTo);
@@ -684,7 +684,7 @@ final class OrdersController
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) { if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
Flash::set('order.error', 'Nieprawidlowe parametry.'); Flash::set('order.error', 'Nieprawidłowe parametry.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -701,7 +701,7 @@ final class OrdersController
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'note', 'note',
'Zaktualizowano notatke', 'Zaktualizowano notatkę',
['note_id' => $noteId], ['note_id' => $noteId],
'user', 'user',
$authorName !== '' ? $authorName : null $authorName !== '' ? $authorName : null
@@ -712,7 +712,7 @@ final class OrdersController
} catch (\InvalidArgumentException $exception) { } catch (\InvalidArgumentException $exception) {
Flash::set('order.error', $exception->getMessage()); Flash::set('order.error', $exception->getMessage());
} catch (Throwable $exception) { } catch (Throwable $exception) {
Flash::set('order.error', 'Nie udalo sie zaktualizowac notatki: ' . $exception->getMessage()); Flash::set('order.error', 'Nie udało się zaktualizować notatki: ' . $exception->getMessage());
} }
return Response::redirect($redirectTo); return Response::redirect($redirectTo);
@@ -729,7 +729,7 @@ final class OrdersController
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) { if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
Flash::set('order.error', 'Nieprawidlowe parametry.'); Flash::set('order.error', 'Nieprawidłowe parametry.');
return Response::redirect('/orders/' . $orderId); return Response::redirect('/orders/' . $orderId);
} }
@@ -746,16 +746,16 @@ final class OrdersController
$this->orders->recordActivity( $this->orders->recordActivity(
$orderId, $orderId,
'note', 'note',
'Usunieto notatke', 'Usuńięto notatkę',
['note_id' => $noteId], ['note_id' => $noteId],
'user', 'user',
$authorName !== '' ? $authorName : null $authorName !== '' ? $authorName : null
); );
Flash::set('order.success', 'Notatka usunieta.'); Flash::set('order.success', 'Notatka usuńięta.');
} catch (RuntimeException $exception) { } catch (RuntimeException $exception) {
Flash::set('order.error', $exception->getMessage()); Flash::set('order.error', $exception->getMessage());
} catch (Throwable $exception) { } catch (Throwable $exception) {
Flash::set('order.error', 'Nie udalo sie usunac notatki: ' . $exception->getMessage()); Flash::set('order.error', 'Nie udało się usuńąć notatki: ' . $exception->getMessage());
} }
return Response::redirect($redirectTo); return Response::redirect($redirectTo);
@@ -792,7 +792,7 @@ final class OrdersController
$projectsTotal = max(0, (int) ($row['projects_total'] ?? 0)); $projectsTotal = max(0, (int) ($row['projects_total'] ?? 0));
$returnedCount = max(0, (int) ($row['customer_returned_count'] ?? 0)); $returnedCount = max(0, (int) ($row['customer_returned_count'] ?? 0));
$returnedBadge = $returnedCount >= 1 $returnedBadge = $returnedCount >= 1
? ' <span class="risk-return-badge" title="Klient nie odebral ' . $returnedCount . ' przesylek w historii">zwroty: ' . $returnedCount . '</span>' ? ' <span class="risk-return-badge" title="Klient nie odebral ' . $returnedCount . ' przesyłek w historii">zwroty: ' . $returnedCount . '</span>'
: ''; : '';
$notesCount = max(0, (int) ($row['notes_count'] ?? 0)); $notesCount = max(0, (int) ($row['notes_count'] ?? 0));
@@ -1236,8 +1236,8 @@ final class OrdersController
{ {
return [ return [
'' => $this->translator->get('orders.filters.any'), '' => $this->translator->get('orders.filters.any'),
'0' => 'nieoplacone', '0' => 'nieopłacone',
'1' => 'czesciowo oplacone', '1' => 'częściowo opłacone',
'2' => 'oplacone', '2' => 'oplacone',
'3' => 'zwrocone', '3' => 'zwrocone',
]; ];
@@ -1290,12 +1290,12 @@ final class OrdersController
{ {
$orderId = max(0, (int) $request->input('id', 0)); $orderId = max(0, (int) $request->input('id', 0));
if ($orderId <= 0) { if ($orderId <= 0) {
return Response::json(['success' => false, 'message' => 'Nieprawidlowe zamowienie'], 400); return Response::json(['success' => false, 'message' => 'Nieprawidłowe zamówienie'], 400);
} }
$csrfToken = (string) $request->input('_token', ''); $csrfToken = (string) $request->input('_token', '');
if (!Csrf::validate($csrfToken)) { if (!Csrf::validate($csrfToken)) {
return Response::json(['success' => false, 'message' => 'Sesja wygasla, odswiez strone'], 403); return Response::json(['success' => false, 'message' => 'Sesja wygasla, odśwież strone'], 403);
} }
if ($this->emailService === null) { if ($this->emailService === null) {
@@ -1314,7 +1314,7 @@ final class OrdersController
return Response::json([ return Response::json([
'success' => $result['success'], 'success' => $result['success'],
'message' => $result['success'] ? 'E-mail wyslany pomyslnie' : ('Blad wysylki: ' . ($result['error'] ?? 'nieznany')), 'message' => $result['success'] ? 'E-mail wysłany pomyslnie' : ('Błąd wysyłki: ' . ($result['error'] ?? 'nieznany')),
]); ]);
} }
@@ -1461,7 +1461,7 @@ final class OrdersController
$orderId = max(0, (int) $request->input('id', 0)); $orderId = max(0, (int) $request->input('id', 0));
$details = $this->orders->findDetails($orderId); $details = $this->orders->findDetails($orderId);
if ($details === null) { if ($details === null) {
return Response::html('<div class="order-preview-error">Zamowienie nie znalezione.</div>', 404); return Response::html('<div class="order-preview-error">Zamówienie nie znalezione.</div>', 404);
} }
$order = is_array($details['order'] ?? null) ? $details['order'] : []; $order = is_array($details['order'] ?? null) ? $details['order'] : [];

View File

@@ -599,8 +599,8 @@ final class OrdersRepository
*/ */
private function loadOrderNotes(int $orderId): array private function loadOrderNotes(int $orderId): array
{ {
// Phase 129-01: zwraca tylko notatki importowane ze zrodla (note_type != 'user'). // Phase 129-01: zwraca tylko notatki importowane ze źródła (note_type != 'user').
// Notatki autorskie operatora ladowane sa osobno przez OrderNotesService::listUserNotes(). // Notatki autorskie operatora ladowane są osobno przez OrderNotesService::listUserNotes().
$stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id AND note_type <> "user" ORDER BY created_at_external DESC, id DESC'); $stmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id AND note_type <> "user" ORDER BY created_at_external DESC, id DESC');
$stmt->execute(['order_id' => $orderId]); $stmt->execute(['order_id' => $orderId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -682,10 +682,10 @@ final class OrdersRepository
} }
/** /**
* Subquery zliczajaca zamowienia klienta biezacego wiersza, ktore w historii * Subquery zliczająca zamówienia klienta bieżącego wiersza, które w historii
* mialy paczke z delivery_status='returned' (zwrot do nadawcy). * mialy paczkę z delivery_status='returned' (zwrot do nadawcy).
* Matching po email LUB phone (tylko cyfry, min 6) LUB name — identyczne dopasowanie * Matching po email LUB phone (tylko cyfry, min 6) LUB name — identyczne dopasowanie
* po LOWER/TRIM. Wyklucza biezace zamowienie (self-exclusion). * po LOWER/TRIM. Wyklucza bieżące zamówienie (self-exclusion).
* *
* Wymagania: MySQL 8.0+ (REGEXP_REPLACE). * Wymagania: MySQL 8.0+ (REGEXP_REPLACE).
* *
@@ -714,7 +714,7 @@ final class OrdersRepository
} }
/** /**
* Subquery liczby wszystkich notatek zamowienia, autorskich i importowanych. * Subquery liczby wszystkich notatek zamówienia, autorskich i importowanych.
* Wspierane indeksem order_notes_order_idx (order_id). * Wspierane indeksem order_notes_order_idx (order_id).
*/ */
private function notesCountSubquerySql(string $orderAlias): string private function notesCountSubquerySql(string $orderAlias): string
@@ -906,10 +906,10 @@ final class OrdersRepository
$summaryParts = []; $summaryParts = [];
if (isset($changed['delivery_method'])) { if (isset($changed['delivery_method'])) {
$summaryParts[] = 'forma dostawy'; $summaryParts[] = 'formą dostawy';
} }
if (isset($changed['payment_method']) || isset($changed['external_payment_type_id'])) { if (isset($changed['payment_method']) || isset($changed['external_payment_type_id'])) {
$summaryParts[] = 'forma płatności'; $summaryParts[] = 'formą płatności';
} }
$summary = 'Zmiana danych zamówienia: ' . implode(', ', $summaryParts); $summary = 'Zmiana danych zamówienia: ' . implode(', ', $summaryParts);

View File

@@ -64,17 +64,17 @@ final class PrintApiController
$this->lastLabelError = ''; $this->lastLabelError = '';
$labelPath = $this->ensureLabel($packageId, $package); $labelPath = $this->ensureLabel($packageId, $package);
if ($labelPath === '') { if ($labelPath === '') {
$msg = 'Etykieta niedostepna'; $msg = 'Etykieta niedostępna';
if ($this->lastLabelError !== '') { if ($this->lastLabelError !== '') {
$msg .= ': ' . $this->lastLabelError; $msg .= ': ' . $this->lastLabelError;
} }
$msg .= '. Kliknij najpierw "Pobierz" aby pobrac etykiete.'; $msg .= '. Kliknij najpierw "Pobierz" aby pobrać etykietę.';
return Response::json(['error' => $msg], 400); return Response::json(['error' => $msg], 400);
} }
$existingPending = $this->printJobs->findPendingByPackageId($packageId); $existingPending = $this->printJobs->findPendingByPackageId($packageId);
if ($existingPending !== null) { if ($existingPending !== null) {
return Response::json(['error' => 'Zlecenie juz w kolejce'], 409); return Response::json(['error' => 'Zlecenie już w kolejce'], 409);
} }
$user = $this->auth->user(); $user = $this->auth->user();

View File

@@ -15,7 +15,7 @@ final class AllegroApiClient
{ {
$safeId = rawurlencode(trim($checkoutFormId)); $safeId = rawurlencode(trim($checkoutFormId));
if ($safeId === '') { if ($safeId === '') {
throw new AllegroApiException('Brak ID zamowienia Allegro do pobrania.'); throw new AllegroApiException('Brak ID zamówienia Allegro do pobrania.');
} }
$url = rtrim($this->apiBaseUrl($environment), '/') . '/order/checkout-forms/' . $safeId; $url = rtrim($this->apiBaseUrl($environment), '/') . '/order/checkout-forms/' . $safeId;
@@ -46,7 +46,7 @@ final class AllegroApiClient
{ {
$safeId = rawurlencode(trim($checkoutFormId)); $safeId = rawurlencode(trim($checkoutFormId));
if ($safeId === '') { if ($safeId === '') {
throw new AllegroApiException('Brak ID zamowienia Allegro do pobrania przesylek.'); throw new AllegroApiException('Brak ID zamówienia Allegro do pobrania przesyłek.');
} }
$url = rtrim($this->apiBaseUrl($environment), '/') . '/order/checkout-forms/' . $safeId . '/shipments'; $url = rtrim($this->apiBaseUrl($environment), '/') . '/order/checkout-forms/' . $safeId . '/shipments';
@@ -158,7 +158,7 @@ final class AllegroApiClient
): array { ): array {
$safeId = rawurlencode(trim($checkoutFormId)); $safeId = rawurlencode(trim($checkoutFormId));
if ($safeId === '') { if ($safeId === '') {
throw new AllegroApiException('Brak ID zamowienia Allegro do aktualizacji statusu.'); throw new AllegroApiException('Brak ID zamówienia Allegro do aktualizacji statusu.');
} }
$normalizedStatus = strtoupper(trim($status)); $normalizedStatus = strtoupper(trim($status));
@@ -213,7 +213,7 @@ final class AllegroApiClient
$ch = curl_init($url); $ch = curl_init($url);
if ($ch === false) { if ($ch === false) {
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.'); throw new AllegroApiException('Nie udało się zainicjować połączenia z API Allegro.');
} }
curl_setopt_array($ch, $this->withSslOptions([ curl_setopt_array($ch, $this->withSslOptions([
@@ -236,12 +236,12 @@ final class AllegroApiClient
$ch = null; $ch = null;
if ($responseBody === false) { if ($responseBody === false) {
throw new AllegroApiException('Blad polaczenia z API Allegro: ' . $curlError); throw new AllegroApiException('Błąd połączenia z API Allegro: ' . $curlError);
} }
$json = json_decode((string) $responseBody, true); $json = json_decode((string) $responseBody, true);
if (!is_array($json)) { if (!is_array($json)) {
throw new AllegroApiException('Nieprawidlowy JSON odpowiedzi API Allegro.'); throw new AllegroApiException('Nieprawidłowy JSON odpowiedźi API Allegro.');
} }
if ($httpCode === 401) { if ($httpCode === 401) {
@@ -261,7 +261,7 @@ final class AllegroApiClient
$message = implode('; ', array_filter($parts)); $message = implode('; ', array_filter($parts));
} }
if ($message === '') { if ($message === '') {
$message = 'Blad API Allegro.'; $message = 'Błąd API Allegro.';
} }
throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message); throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message);
} }
@@ -279,7 +279,7 @@ final class AllegroApiClient
$ch = curl_init($url); $ch = curl_init($url);
if ($ch === false) { if ($ch === false) {
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.'); throw new AllegroApiException('Nie udało się zainicjować połączenia z API Allegro.');
} }
curl_setopt_array($ch, $this->withSslOptions([ curl_setopt_array($ch, $this->withSslOptions([
@@ -302,12 +302,12 @@ final class AllegroApiClient
$ch = null; $ch = null;
if ($responseBody === false) { if ($responseBody === false) {
throw new AllegroApiException('Blad polaczenia z API Allegro: ' . $curlError); throw new AllegroApiException('Błąd połączenia z API Allegro: ' . $curlError);
} }
$json = json_decode((string) $responseBody, true); $json = json_decode((string) $responseBody, true);
if (!is_array($json)) { if (!is_array($json)) {
throw new AllegroApiException('Nieprawidlowy JSON odpowiedzi API Allegro.'); throw new AllegroApiException('Nieprawidłowy JSON odpowiedźi API Allegro.');
} }
if ($httpCode === 401) { if ($httpCode === 401) {
@@ -327,7 +327,7 @@ final class AllegroApiClient
$message = implode('; ', array_filter($parts)); $message = implode('; ', array_filter($parts));
} }
if ($message === '') { if ($message === '') {
$message = 'Blad API Allegro.'; $message = 'Błąd API Allegro.';
} }
throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message); throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message);
} }
@@ -344,7 +344,7 @@ final class AllegroApiClient
$ch = curl_init($url); $ch = curl_init($url);
if ($ch === false) { if ($ch === false) {
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.'); throw new AllegroApiException('Nie udało się zainicjować połączenia z API Allegro.');
} }
curl_setopt_array($ch, $this->withSslOptions([ curl_setopt_array($ch, $this->withSslOptions([
@@ -367,7 +367,7 @@ final class AllegroApiClient
$ch = null; $ch = null;
if ($responseBody === false) { if ($responseBody === false) {
throw new AllegroApiException('Blad polaczenia z API Allegro: ' . $curlError); throw new AllegroApiException('Błąd połączenia z API Allegro: ' . $curlError);
} }
if ($httpCode === 401) { if ($httpCode === 401) {
@@ -375,12 +375,12 @@ final class AllegroApiClient
} }
if ($httpCode === 204) { if ($httpCode === 204) {
throw new AllegroApiException('Brak etykiety dla podanej przesylki.'); throw new AllegroApiException('Brak etykiety dla podanej przesyłki.');
} }
if ($httpCode < 200 || $httpCode >= 300) { if ($httpCode < 200 || $httpCode >= 300) {
$json = json_decode((string) $responseBody, true); $json = json_decode((string) $responseBody, true);
$message = is_array($json) ? trim((string) ($json['message'] ?? 'Blad API Allegro.')) : 'Blad API Allegro.'; $message = is_array($json) ? trim((string) ($json['message'] ?? 'Błąd API Allegro.')) : 'Błąd API Allegro.';
throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message); throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message);
} }
@@ -394,7 +394,7 @@ final class AllegroApiClient
{ {
$ch = curl_init($url); $ch = curl_init($url);
if ($ch === false) { if ($ch === false) {
throw new AllegroApiException('Nie udalo sie zainicjowac polaczenia z API Allegro.'); throw new AllegroApiException('Nie udało się zainicjować połączenia z API Allegro.');
} }
curl_setopt_array($ch, $this->withSslOptions([ curl_setopt_array($ch, $this->withSslOptions([
@@ -415,12 +415,12 @@ final class AllegroApiClient
$ch = null; $ch = null;
if ($responseBody === false) { if ($responseBody === false) {
throw new AllegroApiException('Blad polaczenia z API Allegro: ' . $curlError); throw new AllegroApiException('Błąd połączenia z API Allegro: ' . $curlError);
} }
$json = json_decode((string) $responseBody, true); $json = json_decode((string) $responseBody, true);
if (!is_array($json)) { if (!is_array($json)) {
throw new AllegroApiException('Nieprawidlowy JSON odpowiedzi API Allegro.'); throw new AllegroApiException('Nieprawidłowy JSON odpowiedźi API Allegro.');
} }
if ($httpCode === 401) { if ($httpCode === 401) {
@@ -428,7 +428,7 @@ final class AllegroApiClient
} }
if ($httpCode < 200 || $httpCode >= 300) { if ($httpCode < 200 || $httpCode >= 300) {
$message = trim((string) ($json['message'] ?? 'Blad API Allegro.')); $message = trim((string) ($json['message'] ?? 'Błąd API Allegro.'));
throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message); throw new AllegroApiException('API Allegro HTTP ' . $httpCode . ': ' . $message);
} }

View File

@@ -64,7 +64,7 @@ final class AllegroOAuthClient
$accessToken = trim((string) ($payload['access_token'] ?? '')); $accessToken = trim((string) ($payload['access_token'] ?? ''));
$refreshToken = trim((string) ($payload['refresh_token'] ?? '')); $refreshToken = trim((string) ($payload['refresh_token'] ?? ''));
if ($accessToken === '' || $refreshToken === '') { if ($accessToken === '' || $refreshToken === '') {
throw new AllegroOAuthException('Allegro nie zwrocilo kompletu tokenow OAuth.'); throw new AllegroOAuthException('Allegro nie zwróciło kompletu tokenow OAuth.');
} }
return [ return [
@@ -97,7 +97,7 @@ final class AllegroOAuthClient
$accessToken = trim((string) ($payload['access_token'] ?? '')); $accessToken = trim((string) ($payload['access_token'] ?? ''));
if ($accessToken === '') { if ($accessToken === '') {
throw new AllegroOAuthException('Allegro nie zwrocilo access_token po odswiezeniu.'); throw new AllegroOAuthException('Allegro nie zwróciło access_token po odświeżeniu.');
} }
return [ return [
@@ -149,7 +149,7 @@ final class AllegroOAuthClient
): array { ): array {
$ch = curl_init($url); $ch = curl_init($url);
if ($ch === false) { if ($ch === false) {
throw new AllegroOAuthException('Nie udalo sie zainicjowac polaczenia OAuth z Allegro.'); throw new AllegroOAuthException('Nie udało się zainicjować połączenia OAuth z Allegro.');
} }
$sslOpts = [ $sslOpts = [
@@ -179,17 +179,17 @@ final class AllegroOAuthClient
$ch = null; $ch = null;
if ($responseBody === false) { if ($responseBody === false) {
throw new AllegroOAuthException('Blad polaczenia OAuth z Allegro: ' . $curlError); throw new AllegroOAuthException('Błąd połączenia OAuth z Allegro: ' . $curlError);
} }
$json = json_decode((string) $responseBody, true); $json = json_decode((string) $responseBody, true);
if (!is_array($json)) { if (!is_array($json)) {
throw new AllegroOAuthException('Nieprawidlowy JSON odpowiedzi OAuth Allegro.'); throw new AllegroOAuthException('Nieprawidłowy JSON odpowiedźi OAuth Allegro.');
} }
if ($httpCode < 200 || $httpCode >= 300) { if ($httpCode < 200 || $httpCode >= 300) {
$error = trim((string) ($json['error'] ?? 'oauth_error')); $error = trim((string) ($json['error'] ?? 'oauth_error'));
$description = trim((string) ($json['error_description'] ?? 'Brak szczegolow bledu OAuth.')); $description = trim((string) ($json['error_description'] ?? 'Brak szczegółów bledu OAuth.'));
throw new AllegroOAuthException('OAuth Allegro [' . $error . ']: ' . $description); throw new AllegroOAuthException('OAuth Allegro [' . $error . ']: ' . $description);
} }

View File

@@ -16,8 +16,8 @@ final class AllegroOrderImportService
{ {
private const IMPORT_TRIGGERS = [ private const IMPORT_TRIGGERS = [
'manual_import' => 'Import reczny', 'manual_import' => 'Import reczny',
'orders_sync' => 'Synchronizacja zamowien', 'orders_sync' => 'Synchronizacja zamówień',
'status_sync' => 'Synchronizacja statusow', 'status_sync' => 'Synchronizacja statusów',
]; ];
public function __construct( public function __construct(
@@ -39,7 +39,7 @@ final class AllegroOrderImportService
{ {
$orderId = trim($checkoutFormId); $orderId = trim($checkoutFormId);
if ($orderId === '') { if ($orderId === '') {
throw new AllegroApiException('Podaj ID zamowienia Allegro.'); throw new AllegroApiException('Podaj ID zamówienia Allegro.');
} }
$normalizedTrigger = $this->normalizeTrigger($trigger); $normalizedTrigger = $this->normalizeTrigger($trigger);
$triggerLabel = self::IMPORT_TRIGGERS[$normalizedTrigger]; $triggerLabel = self::IMPORT_TRIGGERS[$normalizedTrigger];
@@ -74,8 +74,8 @@ final class AllegroOrderImportService
if ($savedOrderId > 0) { if ($savedOrderId > 0) {
$sourceUpdatedAt = trim((string) ($mapped['order']['source_updated_at'] ?? '')); $sourceUpdatedAt = trim((string) ($mapped['order']['source_updated_at'] ?? ''));
$summary = $wasCreated $summary = $wasCreated
? 'Zaimportowano zamowienie z Allegro' ? 'Zaimportowano zamówienie z Allegro'
: 'Zaktualizowano zamowienie z Allegro (re-import)'; : 'Zaktualizowano zamówienie z Allegro (re-import)';
$details = [ $details = [
'source' => IntegrationSources::ALLEGRO, 'source' => IntegrationSources::ALLEGRO,
'source_order_id' => trim($checkoutFormId), 'source_order_id' => trim($checkoutFormId),
@@ -139,7 +139,7 @@ final class AllegroOrderImportService
} }
/** /**
* Detect "klient prosi o fakture" flag from Allegro checkout-form payload. * Detect "klient prosi o fakturę" flag from Allegro checkout-form payload.
* Triggers on explicit `invoice.required`, business buyer (`naturalPerson=false`), * Triggers on explicit `invoice.required`, business buyer (`naturalPerson=false`),
* NIP in invoice address, or explicit company name. * NIP in invoice address, or explicit company name.
* *
@@ -186,7 +186,7 @@ final class AllegroOrderImportService
{ {
$checkoutFormId = trim((string) ($payload['id'] ?? '')); $checkoutFormId = trim((string) ($payload['id'] ?? ''));
if ($checkoutFormId === '') { if ($checkoutFormId === '') {
throw new AllegroApiException('Odpowiedz Allegro nie zawiera ID zamowienia.'); throw new AllegroApiException('Odpowiedz Allegro nie zawiera ID zamówienia.');
} }
$status = trim((string) ($payload['status'] ?? '')); $status = trim((string) ($payload['status'] ?? ''));

View File

@@ -115,7 +115,7 @@ final class AllegroStatusSyncService
'pushed' => 0, 'pushed' => 0,
'skipped' => 0, 'skipped' => 0,
'failed' => 0, 'failed' => 0,
'message' => 'Brak mapowan statusow orderPRO -> Allegro.', 'message' => 'Brak mapowań statusów orderPRO -> Allegro.',
'errors' => [], 'errors' => [],
]; ];
} }
@@ -131,7 +131,7 @@ final class AllegroStatusSyncService
'pushed' => 0, 'pushed' => 0,
'skipped' => 0, 'skipped' => 0,
'failed' => 0, 'failed' => 0,
'message' => 'Brak zamowien do synchronizacji statusow.', 'message' => 'Brak zamówień do synchronizacji statusów.',
'errors' => [], 'errors' => [],
]; ];
} }
@@ -304,7 +304,7 @@ final class AllegroStatusSyncService
$statement = $this->pdo->prepare('UPDATE orders SET last_status_checked_at = NOW() WHERE id = ?'); $statement = $this->pdo->prepare('UPDATE orders SET last_status_checked_at = NOW() WHERE id = ?');
$statement->execute([$orderId]); $statement->execute([$orderId]);
} catch (Throwable) { } catch (Throwable) {
// Blad zapisu znacznika nie powinien przerywac petli synchronizacji. // Błąd zapisu znacznika nie powinien przerywac petli synchronizacji.
} }
} }
} }

View File

@@ -23,7 +23,7 @@ final class AllegroTokenManager
{ {
$oauth = $this->repository->getTokenCredentials(); $oauth = $this->repository->getTokenCredentials();
if ($oauth === null) { if ($oauth === null) {
throw new AllegroOAuthException('Brak polaczenia OAuth Allegro. Polacz konto w Ustawieniach.'); throw new AllegroOAuthException('Brak połączenia OAuth Allegro. Połącz konto w Ustawieniach.');
} }
$env = (string) ($oauth['environment'] ?? 'sandbox'); $env = (string) ($oauth['environment'] ?? 'sandbox');
@@ -89,7 +89,7 @@ final class AllegroTokenManager
$updated = $this->repository->getTokenCredentials(); $updated = $this->repository->getTokenCredentials();
$newToken = trim((string) ($updated['access_token'] ?? '')); $newToken = trim((string) ($updated['access_token'] ?? ''));
if ($newToken === '') { if ($newToken === '') {
throw new AllegroOAuthException('Nie udalo sie odswiezyc tokenu Allegro.'); throw new AllegroOAuthException('Nie udało się odświeżyć tokenu Allegro.');
} }
return [$newToken, (string) ($updated['environment'] ?? 'sandbox')]; return [$newToken, (string) ($updated['environment'] ?? 'sandbox')];

Some files were not shown because too many files have changed in this diff Show More