update
This commit is contained in:
@@ -7,6 +7,58 @@ Completed milestone log for this project.
|
|||||||
| v0.1 Initial Release | 2026-03-13 | 2 days | 6 phases, 15 plans |
|
| v0.1 Initial Release | 2026-03-13 | 2 days | 6 phases, 15 plans |
|
||||||
| v0.2 Pre-Expansion Fixes | 2026-03-15 | 2 days | 1 phase, 5 plans |
|
| v0.2 Pre-Expansion Fixes | 2026-03-15 | 2 days | 1 phase, 5 plans |
|
||||||
| v0.3 Moduł Paragonów | 2026-03-15 | 1 day | 5 phases, 5 plans |
|
| v0.3 Moduł Paragonów | 2026-03-15 | 1 day | 5 phases, 5 plans |
|
||||||
|
| v3.0 Mobile Responsive | 2026-04-19 | ~5 tygodni | 52 phases shipped, 55 plans |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ v3.0 Mobile Responsive
|
||||||
|
|
||||||
|
**Completed:** 2026-04-19
|
||||||
|
**Duration:** ~5 tygodni (2026-03-15 → 2026-04-19)
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Phases shipped | 52 (z zakresu 52–105) |
|
||||||
|
| Phases deferred | 1 (Phase 68 Code Deduplication Refactor) |
|
||||||
|
| Phases cancelled | 1 (Phase 99 Order Delivery & Payment Edit) |
|
||||||
|
| Plans completed | 55 |
|
||||||
|
| Files changed | ~250+ |
|
||||||
|
|
||||||
|
### Key Accomplishments
|
||||||
|
|
||||||
|
- **Mobile UI core:** hamburger menu, slide-in sidebar, status panel toggle, image hover zoom, collapsed sidebar fix (Phase 52–55).
|
||||||
|
- **Order payments i automation events:** reczne platnosci + push `set_paid` do shopPRO; `payment.status_changed`, `order.status_changed`, `order.status_aged`, `order.imported`, `shipment.created/status_changed`, warunki statusowe i `payment_method` (Phase 56–61, 84, 95–96, 98).
|
||||||
|
- **Import re-import safety + personalizacja:** ochrona pol lokalnych przy re-imporcie shopPRO, activity log rozroznia import/aktualizacje, custom_fields + message jako personalizacja pozycji (Phase 62–63, 75, 79).
|
||||||
|
- **Receipts:** DATETIME precision, koszt wysylki jako pozycja, bugfix `buildItemsSnapshot`/`delivery_price` (Phase 64, 70, 90).
|
||||||
|
- **PAUL framework:** Delegated Apply + Codex Executor (Phase 65, 67).
|
||||||
|
- **Tracking Allegro i shopPRO:** English statuses, pull mapping, push statusow orderPRO→shopPRO/Allegro, Delivery Tracking (Phase 66, 69, 74–75, 83).
|
||||||
|
- **Przesylki:** preset auto-submit, przekazywanie waybill do Allegro, Apaczka COD bank account, Apaczka long-street truncation, Weekend Delivery (InPost), delete z szczegolow zamowienia, User-Agent header z publiczna /info (Phase 78, 86–89, 102, 104).
|
||||||
|
- **Listowanie i wyszukiwanie:** per_page w localStorage, search po produkcie, tooltipy, AJAX refresh, status group filter, global search w topbarze, aged highlight, popup preview (Phase 72–73, 80–82, 85, 94–95, 101).
|
||||||
|
- **Remember Me login, buyer name copy, info page** (Phase 92–93, 89).
|
||||||
|
- **Print pipeline:** resilient polling 3-layer timeout w OrderPROPrint, preset scope + print UX, auto-click najnowszej etykiety (Phase 91, 100, 103).
|
||||||
|
- **Project generation (PSD):** polautomatyczne generowanie projektow graficznych z zamowien + `external_status_id` → `status_code` rename + flagi statusu projektu (Phase 97).
|
||||||
|
- **Statistics (ostatnia faza):** dzienny raport zamowien `/statistics/orders` z filtrami (daty/kanaly/grupy statusow) i stopka `Podsumowanie`; hotfix collation MySQL; fallback netto 23% VAT (Phase 105).
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| Event `order.imported` tylko przy pierwszym imporcie + backfill statusow | Unikniecie duplikatow reakcji automatyzacji, poprawa starych zamowien w_realizacji→wyslane |
|
||||||
|
| Push waybill do Allegro niekrytyczny dla lokalnej paczki | Blad API Allegro nie blokuje utworzenia przesylki; tylko dla `source=allegro` |
|
||||||
|
| Historia automatyzacji audytowana per rule + retencja 30 dni | Observability bez recznego utrzymania danych |
|
||||||
|
| Preset przesylek nadpisuje tylko wymiary+wage + auto-submit | Spojnosc z polityka single responsibility preseta |
|
||||||
|
| `_token` CSRF field standardized | Eliminacja konfliktu nazw z wczesnego `_csrf_token` |
|
||||||
|
| Statistics channelSql: `CAST(int AS CHAR) COLLATE utf8mb4_unicode_ci` | Unikniecie `1271 Illegal mix of collations`; pattern dla przyszlych raportow per-integration |
|
||||||
|
| Statistics netto fallback `/ 1.23` | shopPRO nie wysyla netto ani w zamowieniu ani w items — tymczasowy fallback; docelowy fix opisany w `.paul/TODO.md` (STAT-NET) |
|
||||||
|
| Phase 68 deferred | Refaktor deduplikacyjny wymaga wiekszego kontekstu — przeniesiony na pozniej |
|
||||||
|
|
||||||
|
### Deferred to Next Milestone
|
||||||
|
|
||||||
|
- **STAT-NET:** netto z shopPRO / `order_items.tax_rate` (unikniecie sztywnego 23% VAT).
|
||||||
|
- **Phase 68 Code Deduplication Refactor** (`0/2`, Planning).
|
||||||
|
- **Mobile Orders List / Mobile Order Details / Mobile Settings** — TBD z tabeli ROADMAP.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
|
|
||||||
| Attribute | Value |
|
| Attribute | Value |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
| Version | 1.0.0 |
|
| Version | 3.0.0 |
|
||||||
| Status | v2.3 Complete |
|
| Status | v3.0 Complete |
|
||||||
| Last Updated | 2026-03-28 |
|
| Last Updated | 2026-04-19 |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -107,16 +107,23 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Naprawa tworzenia przesylek Apaczka dla dlugich adresow: auto-truncate dla uslug punktowych (ORLEN/Paczkomat), walidacja + blad dla uslug kurierskich — Phase 102
|
- [x] Naprawa tworzenia przesylek Apaczka dla dlugich adresow: auto-truncate dla uslug punktowych (ORLEN/Paczkomat), walidacja + blad dla uslug kurierskich — Phase 102
|
||||||
- [x] Naprawa auto-click etykiety po utworzeniu przesylki: klik na najnowsza paczke zamiast najstarszej (buttons[0] vs buttons[length-1]) — Phase 103
|
- [x] Naprawa auto-click etykiety po utworzeniu przesylki: klik na najnowsza paczke zamiast najstarszej (buttons[0] vs buttons[length-1]) — Phase 103
|
||||||
- [x] Apaczka Paczka w Weekend: checkbox "Dostawa w weekend (sobota)" dla uslug InPost paczkomatowych + mapowanie pole formularza weekend_delivery -> option[19] w API Apaczki — Phase 104
|
- [x] Apaczka Paczka w Weekend: checkbox "Dostawa w weekend (sobota)" dla uslug InPost paczkomatowych + mapowanie pole formularza weekend_delivery -> option[19] w API Apaczki — Phase 104
|
||||||
|
- [x] Statystyki zamowien: widok `/statistics/orders` z filtrami (daty, kanaly multiselect, grupy statusow multiselect) i raportem dziennym per kanal (Allegro, shopPRO per integracja); hotfix collation MySQL + fallback netto 23% VAT — Phase 105
|
||||||
|
- [x] Wersja mobilna — modul po module (v3.0) — shipped across phases 52–105
|
||||||
|
|
||||||
|
### Deferred
|
||||||
|
|
||||||
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
|
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
|
||||||
|
- [ ] STAT-NET: pobieranie netto z shopPRO lub wyliczanie z `order_items.tax_rate` (unikniecie sztywnego 23% VAT) — `.paul/TODO.md`
|
||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
- [ ] Wersja mobilna — modul po module (v3.0)
|
- [ ] (brak — milestone v3.0 zakonczony, oczekiwanie na kolejny)
|
||||||
|
|
||||||
### Planned (Next)
|
### Planned (Next)
|
||||||
|
|
||||||
- [ ] ZarzÄ…dzanie produktami
|
- [ ] ZarzÄ…dzanie produktami
|
||||||
- [ ] ZarzÄ…dzanie stanami magazynowymi
|
- [ ] ZarzÄ…dzanie stanami magazynowymi
|
||||||
|
- [ ] Mobile Orders List / Mobile Order Details / Mobile Settings — pelna wersja mobilna pozostalych ekranow
|
||||||
|
|
||||||
### Out of Scope
|
### Out of Scope
|
||||||
|
|
||||||
@@ -178,6 +185,10 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
|||||||
| Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active |
|
| Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active |
|
||||||
| Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active |
|
| Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active |
|
||||||
| Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active |
|
| Push waybilla do Allegro checkout forms wykonywany tylko dla zamowien source=allegro i jest niekrytyczny dla lokalnego tworzenia paczki | Eliminacja recznego kroku po stronie Allegro bez ryzyka utraty lokalnie utworzonej przesylki przy bledzie API | 2026-03-28 | Active |
|
||||||
|
| Event `order.imported` emitowany tylko przy pierwszym imporcie zamowienia | Unikniecie duplikatow reakcji automatyzacji przy kolejnych synchronizacjach | 2026-04-15 | Active |
|
||||||
|
| Preset przesylek nadpisuje wylacznie wymiary+wage + auto-submit po autofill | Single responsibility preseta + szybszy flow operatora | 2026-04-17 | Active |
|
||||||
|
| Statistics channelSql: explicit `COLLATE utf8mb4_unicode_ci` na CASE z `CAST(integration_id AS CHAR)` | Unikniecie `1271 Illegal mix of collations` w `IN (...)` z parametrami bindowanymi; pattern dla przyszlych agregacji per-integration | 2026-04-19 | Active |
|
||||||
|
| Statistics netto fallback `ROUND(gross / 1.23, 2)` gdy `total_without_tax` puste | shopPRO nie wysyla netto ani w zamowieniu ani w `order_items`; tymczasowy fallback — docelowy fix w `.paul/TODO.md` (STAT-NET) | 2026-04-19 | Active |
|
||||||
|
|
||||||
## Success Metrics
|
## Success Metrics
|
||||||
|
|
||||||
@@ -209,6 +220,6 @@ Quick Reference:
|
|||||||
|
|
||||||
---
|
---
|
||||||
*PROJECT.md — Updated when requirements or context change*
|
*PROJECT.md — Updated when requirements or context change*
|
||||||
*Last updated: 2026-04-17 after Phase 104 completion (Apaczka Weekend Delivery)*
|
*Last updated: 2026-04-19 after v3.0 Mobile Responsive milestone completion (Phase 105 Orders Statistics)*
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,27 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
|
|||||||
|
|
||||||
## Current Milestone
|
## Current Milestone
|
||||||
|
|
||||||
### v3.0 Mobile Responsive — In progress
|
**v3.0 Mobile Responsive** (v3.0.0)
|
||||||
|
Status: Complete
|
||||||
|
Completed: 2026-04-19
|
||||||
|
|
||||||
Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na telefonach i tabletach.
|
## Next Milestone
|
||||||
|
|
||||||
|
Run `/paul:discuss-milestone` lub `/paul:milestone` aby zdefiniowac zakres nastepnego milestone.
|
||||||
|
|
||||||
|
Kandydaci w kolejce:
|
||||||
|
- Mobile Orders List / Mobile Order Details / Mobile Settings
|
||||||
|
- Zarzadzanie produktami
|
||||||
|
- Zarzadzanie stanami magazynowymi
|
||||||
|
- STAT-NET (netto shopPRO bez fallbacku 23%)
|
||||||
|
- Phase 68 — Code Deduplication Refactor
|
||||||
|
|
||||||
|
## Completed Milestones
|
||||||
|
|
||||||
|
<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 |
|
| Phase | Name | Plans | Status |
|
||||||
|-------|------|-------|--------|
|
|-------|------|-------|--------|
|
||||||
@@ -28,7 +46,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
|||||||
| 65 | PAUL Delegated Apply | 1/1 | Complete |
|
| 65 | PAUL Delegated Apply | 1/1 | Complete |
|
||||||
| 66 | Allegro Delivery Tracking | 2/2 | Complete |
|
| 66 | Allegro Delivery Tracking | 2/2 | Complete |
|
||||||
| 67 | PAUL Codex Executor | 1/1 | Complete |
|
| 67 | PAUL Codex Executor | 1/1 | Complete |
|
||||||
| 68 | Code Deduplication Refactor | 0/2 | Planning |
|
| 68 | Code Deduplication Refactor | 0/2 | Deferred |
|
||||||
| 69 | Allegro Tracking English Statuses | 1/1 | Complete |
|
| 69 | Allegro Tracking English Statuses | 1/1 | Complete |
|
||||||
| 70 | Receipt Shipping Cost | 1/1 | Complete |
|
| 70 | Receipt Shipping Cost | 1/1 | Complete |
|
||||||
| 71 | Attributes Import | 1/1 | Complete |
|
| 71 | Attributes Import | 1/1 | Complete |
|
||||||
@@ -65,12 +83,11 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
|||||||
| 102 | Apaczka Receiver Street Length | 1/1 | Complete |
|
| 102 | Apaczka Receiver Street Length | 1/1 | Complete |
|
||||||
| 103 | Print Autoclick Fix | 1/1 | Complete |
|
| 103 | Print Autoclick Fix | 1/1 | Complete |
|
||||||
| 104 | Apaczka Weekend Delivery | 1/1 | Complete |
|
| 104 | Apaczka Weekend Delivery | 1/1 | Complete |
|
||||||
| TBD | Mobile Orders List | - | Not started |
|
| 105 | Orders Statistics | 1/1 | Complete |
|
||||||
| TBD | Mobile Order Details | - | Not started |
|
|
||||||
| TBD | Mobile Settings | - | Not started |
|
|
||||||
| ... | Kolejne moduly wg potrzeb | - | Not started |
|
|
||||||
|
|
||||||
## Completed Milestones
|
Archive: `.paul/milestones/v3.0-ROADMAP.md`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>v2.3 Email HTML Layout - 2026-03-28 (1 phase, 1 plan)</summary>
|
<summary>v2.3 Email HTML Layout - 2026-03-28 (1 phase, 1 plan)</summary>
|
||||||
@@ -388,4 +405,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-04-17 - phase 104 unified (Apaczka Weekend Delivery)*
|
*Last updated: 2026-04-19 - v3.0 Mobile Responsive milestone closed (52 phases shipped, 55 plans; phase 68 deferred, phase 99 cancelled)*
|
||||||
|
|||||||
@@ -2,34 +2,42 @@
|
|||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
See: .paul/PROJECT.md (updated 2026-04-17)
|
See: .paul/PROJECT.md (updated 2026-04-19)
|
||||||
|
|
||||||
**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:** Milestone v3.0 - Phase 104 complete, ready for next phase
|
**Current focus:** Awaiting next milestone (v3.0 Mobile Responsive complete 2026-04-19).
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.0 Mobile Responsive - In progress
|
Milestone: Awaiting next milestone
|
||||||
Phase: 104 (Apaczka Weekend Delivery) — Complete
|
Phase: None active
|
||||||
Plan: 104-01 Complete
|
Plan: None
|
||||||
Status: Loop closed, ready for next PLAN
|
Version: 3.0.0
|
||||||
Last activity: 2026-04-17 — UNIFY Phase 104 complete
|
Status: Milestone v3.0 Mobile Responsive complete — ready for next
|
||||||
|
Last activity: 2026-04-19 — Milestone v3.0 closed (52 phases, 55 plans)
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone: [#########.] ~96%
|
- v3.0 Mobile Responsive: [##########] 100% shipped
|
||||||
- Phase 104: [##########] 100%
|
- Next milestone: [__________] 0%
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN --> APPLY --> UNIFY
|
||||||
✓ ✓ ✓ [Loop complete — ready for next PLAN]
|
o o o [Milestone complete — ready for next]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-17
|
Last session: 2026-04-19 (complete-milestone v3.0)
|
||||||
Stopped at: Phase 104 Complete
|
Stopped at: Milestone v3.0 Mobile Responsive zamkniety, archiwum `.paul/milestones/v3.0-ROADMAP.md`
|
||||||
Next action: /paul:plan dla kolejnej fazy
|
Next action: `/paul:discuss-milestone` lub `/paul:milestone` — zdefiniowanie nastepnego milestone
|
||||||
Resume file: .paul/phases/104-apaczka-weekend-delivery/104-01-SUMMARY.md
|
Resume file: .paul/MILESTONES.md
|
||||||
|
|
||||||
|
## Deferred to Next Milestones
|
||||||
|
|
||||||
|
- Phase 68 — Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety)
|
||||||
|
- STAT-NET — netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`)
|
||||||
|
- Mobile Orders List / Mobile Order Details / Mobile Settings (TBD z poprzedniego roadmapu)
|
||||||
|
- sonar-scanner — skan dla phase 105 nie zostal uruchomiony w sesji UNIFY (gap odnotowany)
|
||||||
|
|||||||
25
.paul/TODO.md
Normal file
25
.paul/TODO.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## STAT-NET — netto zamowien w statystykach (data: 2026-04-19)
|
||||||
|
|
||||||
|
### 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)`).
|
||||||
27
.paul/changelog/2026-04-19.md
Normal file
27
.paul/changelog/2026-04-19.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 2026-04-19
|
||||||
|
|
||||||
|
## Co zrobiono
|
||||||
|
|
||||||
|
- [Phase 105, Plan 01] Orders Statistics — nowa sekcja menu `Statystyki -> Zamowienia` + raport dzienny per kanal (Allegro, shopPRO per integracja).
|
||||||
|
- Filtr: zakres dat, multiselect kanalow, multiselect grup statusow (domyslnie wszystkie poza `anulowane`).
|
||||||
|
- Tabela dzienna: Ilosc / Netto / Brutto per kanal + wiersz `Podsumowanie`.
|
||||||
|
- Hotfix (post-APPLY): kolizja collation w MySQL (`1271 Illegal mix of collations`) przy `CAST(integration_id AS CHAR)` — statystyki zwracaly pusta tabele; fix przez explicit `COLLATE utf8mb4_unicode_ci` na CASE zwracajacym `channel_key`.
|
||||||
|
- Scope addition (post-APPLY): fallback netto 23% VAT, gdy `orders.total_without_tax` jest puste (shopPRO nie wysyla netto). TODO `STAT-NET` zapisane w `.paul/TODO.md` (docelowo netto z shopPRO lub z `order_items.tax_rate`).
|
||||||
|
- Dokumentacja: ARCHITECTURE, DB_SCHEMA (brak migracji), TECH_CHANGELOG zaktualizowane.
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `src/Modules/Statistics/OrdersStatisticsController.php`
|
||||||
|
- `src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||||
|
- `resources/views/statistics/orders.php`
|
||||||
|
- `routes/web.php`
|
||||||
|
- `resources/views/layouts/app.php`
|
||||||
|
- `resources/lang/pl.php`
|
||||||
|
- `resources/scss/app.scss`
|
||||||
|
- `public/assets/css/app.css`
|
||||||
|
- `.paul/docs/ARCHITECTURE.md`
|
||||||
|
- `.paul/docs/DB_SCHEMA.md`
|
||||||
|
- `.paul/docs/TECH_CHANGELOG.md`
|
||||||
|
- `.paul/TODO.md`
|
||||||
|
- `.paul/phases/105-orders-statistics/105-01-PLAN.md`
|
||||||
|
- `.paul/phases/105-orders-statistics/105-01-SUMMARY.md`
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# ARCHITECTURE
|
# ARCHITECTURE
|
||||||
|
|
||||||
## Zakres
|
## Zakres
|
||||||
- Dokument opisuje aktualna architekture kodu (stan repo: 2026-04-18).
|
- Dokument opisuje aktualna architekture kodu (stan repo: 2026-04-19).
|
||||||
- Zrodlem prawdy sa: `src/`, `routes/web.php`, `database/migrations/`.
|
- Zrodlem prawdy sa: `src/`, `routes/web.php`, `database/migrations/`.
|
||||||
|
|
||||||
## Warstwy systemu
|
## Warstwy systemu
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
- `Automation`: reguly event-condition-action, historia wykonan.
|
- `Automation`: reguly event-condition-action, historia wykonan.
|
||||||
- `Settings`: konfiguracja statusow, integracji, cron, skrzynek, szablonow, drukowania, mapowan projektow.
|
- `Settings`: konfiguracja statusow, integracji, cron, skrzynek, szablonow, drukowania, mapowan projektow.
|
||||||
- `Printing`: API kolejkowania wydruku i klucze API dla klienta desktop.
|
- `Printing`: API kolejkowania wydruku i klucze API dla klienta desktop.
|
||||||
|
- `Statistics`: raporty i agregacje dzienne zamowien z filtrowaniem po datach, kanalach i grupach statusow.
|
||||||
- `Cron`: synchronizacje integracji i zadania utrzymaniowe.
|
- `Cron`: synchronizacje integracji i zadania utrzymaniowe.
|
||||||
- `Info`: endpoint diagnostyczny `/info`.
|
- `Info`: endpoint diagnostyczny `/info`.
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@
|
|||||||
- `App\Modules\Printing\PrintApiController`: endpointy kolejki wydruku (session/api-key).
|
- `App\Modules\Printing\PrintApiController`: endpointy kolejki wydruku (session/api-key).
|
||||||
- `App\Modules\Automation\AutomationService`: trigger eventow, ewaluacja warunkow, wykonanie akcji.
|
- `App\Modules\Automation\AutomationService`: trigger eventow, ewaluacja warunkow, wykonanie akcji.
|
||||||
- `App\Modules\Settings\ProjectMappingController`: CRUD mapowania produkt -> skrypt generacji projektu.
|
- `App\Modules\Settings\ProjectMappingController`: CRUD mapowania produkt -> skrypt generacji projektu.
|
||||||
|
- `App\Modules\Statistics\OrdersStatisticsController`: endpoint `/statistics/orders`, walidacja filtrow i przygotowanie modelu tabeli.
|
||||||
|
- `App\Modules\Statistics\OrdersStatisticsRepository`: agregacje SQL dzienne (`COUNT`, `SUM total_net`, `SUM total_with_tax`) i mapowanie filtrow kanal/status-group.
|
||||||
|
|
||||||
## Integracje zewnetrzne
|
## Integracje zewnetrzne
|
||||||
- Allegro: OAuth, import zamowien, sync statusow push/pull, mapowania statusow i dostaw.
|
- Allegro: OAuth, import zamowien, sync statusow push/pull, mapowania statusow i dostaw.
|
||||||
@@ -47,6 +50,18 @@
|
|||||||
- Controller waliduje dane i CSRF, wywoluje Repository/Service.
|
- Controller waliduje dane i CSRF, wywoluje Repository/Service.
|
||||||
- Response: HTML (widoki) albo JSON (endpointy AJAX/API).
|
- Response: HTML (widoki) albo JSON (endpointy AJAX/API).
|
||||||
|
|
||||||
|
### Przeplyw Statystyk Zamowien
|
||||||
|
- Route: `GET /statistics/orders` (wymaga sesji uzytkownika).
|
||||||
|
- Controller:
|
||||||
|
- parsuje `date_from`, `date_to`, `channels[]`, `status_groups[]`,
|
||||||
|
- ustawia domyslne grupy statusow (wszystkie poza grupa `anulowane`),
|
||||||
|
- pobiera agregaty dzienne z repozytorium,
|
||||||
|
- buduje tabele z dynamicznymi kolumnami kanalow i stopka `Razem`.
|
||||||
|
- Repository:
|
||||||
|
- liczy kanaly jako `allegro` oraz `shoppro:{integration_id}`,
|
||||||
|
- dla statusu efektywnego allegro stosuje mapowanie `allegro_order_status_mappings`,
|
||||||
|
- zwraca zagregowane rekordy dzien/kanal.
|
||||||
|
|
||||||
## Glowny przeplyw Cron
|
## Glowny przeplyw Cron
|
||||||
- Trigger:
|
- Trigger:
|
||||||
- `GET /cron` (public token) lub web-cron w `Application::maybeRunCronOnWeb`.
|
- `GET /cron` (public token) lub web-cron w `Application::maybeRunCronOnWeb`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Zakres i zrodlo prawdy
|
## Zakres i zrodlo prawdy
|
||||||
- Schemat wynika z migracji SQL w `database/migrations`.
|
- Schemat wynika z migracji SQL w `database/migrations`.
|
||||||
- Dokument odzwierciedla stan repo na 2026-04-18 (migracje do `20260413_000100`).
|
- Dokument odzwierciedla stan repo na 2026-04-19 (migracje do `20260413_000100`).
|
||||||
|
|
||||||
## Ostatnie istotne migracje
|
## Ostatnie istotne migracje
|
||||||
- `20260413_000100_ensure_orders_delivery_payment_columns.sql`
|
- `20260413_000100_ensure_orders_delivery_payment_columns.sql`
|
||||||
@@ -176,3 +176,7 @@
|
|||||||
- Migracje `ensure_` i `000100` sa idempotentne i kompensuja roznice miedzy srodowiskami.
|
- Migracje `ensure_` i `000100` sa idempotentne i kompensuja roznice miedzy srodowiskami.
|
||||||
- Rename `orders.external_status_id -> status_code` wymaga, aby nowe query i dokumentacja uzywaly tylko `status_code`.
|
- Rename `orders.external_status_id -> status_code` wymaga, aby nowe query i dokumentacja uzywaly tylko `status_code`.
|
||||||
- `delivery_price` jest backfillowane z JSON payloadow importu (Allegro/shopPRO).
|
- `delivery_price` jest backfillowane z JSON payloadow importu (Allegro/shopPRO).
|
||||||
|
|
||||||
|
## Zmiany 2026-04-19
|
||||||
|
- Brak zmian schematu bazy danych dla funkcji `Statystyki -> Zamowienia`.
|
||||||
|
- Raport korzysta z istniejacych kolumn i tabel: `orders`, `integrations`, `order_status_groups`, `order_statuses`, `allegro_order_status_mappings`.
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
# TECH_CHANGELOG
|
# TECH_CHANGELOG
|
||||||
|
|
||||||
> Chronologiczny log zmian technicznych — co i dlaczego.
|
> Chronologiczny log zmian technicznych — co i dlaczego.
|
||||||
|
|
||||||
|
## 2026-04-19 - Statystyki zamowien (menu + raport dzienny)
|
||||||
|
|
||||||
|
- Dodano nowy modul `Statistics`:
|
||||||
|
- `OrdersStatisticsController` (obsluga filtrow i render strony `/statistics/orders`).
|
||||||
|
- `OrdersStatisticsRepository` (agregacje dzienne po kanalach i grupach statusow).
|
||||||
|
- Dodano nowa pozycje menu: `Statystyki -> Zamowienia`.
|
||||||
|
- Dodano widok raportowy z filtrem zakresu dat, multiselectem kanalow i multiselectem grup statusow.
|
||||||
|
- Dodano tabele dzienna z metrykami `Ilosc`, `Netto`, `Brutto` per kanal oraz stopka `Razem`.
|
||||||
|
- Dodano tlumaczenia `statistics.orders.*` i `navigation.statistics*`.
|
||||||
|
- Brak zmian migracyjnych i brak zmian schematu bazy danych.
|
||||||
|
|
||||||
|
## 2026-04-19 - Fix: Statystyki nie pokazywaly zamowien (kolizja collation)
|
||||||
|
|
||||||
|
- `OrdersStatisticsRepository::channelSql()` generowal wyrazenie `CONCAT("shoppro:", CAST(integration_id AS CHAR))`, ktore w MySQL dawalo wynik z collation `utf8mb4_bin`. W zestawieniu z parametrami bindowanymi (`utf8mb4_general_ci`) MySQL rzucal `SQLSTATE[HY000] 1271 Illegal mix of collations for operation 'in'`.
|
||||||
|
- Blad byl polykany przez `try/catch (Throwable)` w `aggregateByDay()`, przez co widok dostawal pusta tablice i nie pokazywal zadnych zamowien.
|
||||||
|
- Fix: dodano jawne `COLLATE utf8mb4_unicode_ci` na `CAST(integration_id AS CHAR)` oraz na calym wyrazeniu `CASE` zwracajacym `channel_key`, tak aby klucz kanalu mial spojne collation zgodne z `orders.source`.
|
||||||
|
|
||||||
|
## 2026-04-19 - Statystyki: fallback netto 23% VAT
|
||||||
|
|
||||||
|
- `OrdersStatisticsRepository::netAmountSql()` dostal fallback: jesli `orders.total_without_tax` jest `NULL` lub `0`, a `orders.total_with_tax` ma wartosc, netto wyliczane jest jako `ROUND(total_with_tax / 1.23, 2)`.
|
||||||
|
- Uzasadnienie: shopPRO nie wysyla netto ani na zamowieniu ani w pozycjach (`order_items.original_price_without_tax` jest puste), wiec bez fallbacku kolumna `Netto` w statystykach pokazywala 0.
|
||||||
|
- Uwaga: fallback zaklada 23% VAT. Ostateczne rozwiazanie (prawidlowy netto z shopPRO / z `order_items.tax_rate`) opisane w `.paul/TODO.md` (tag `STAT-NET`).
|
||||||
|
|||||||
50
.paul/governance/governance_2026-04-19.jsonl
Normal file
50
.paul/governance/governance_2026-04-19.jsonl
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{"ts":"2026-04-19T13:58:53Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT id, product_name_pat","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T13:59:05Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T13:59:15Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":fal","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T13:59:29Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/pudelko_komunia_kwiaty.py --help 2>&1 | head -40\",\"description\":\"Check script parameters\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:00:34Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/pudelko_komunia_kwiaty.py --klient \"Magdalena Rukasz Rukasz\" --imie \"Wiktora\" --data \"17.05.2026\" --od-kog","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:00:42Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:04:38Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:04:46Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"SELECT oa.name, o.internal_","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:04:54Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:05:14Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/email_photo_fetcher.py --email \"m.wroblewska82@wp.pl\" --out \"C:/tmp/orderpro_photos\" --days 365 2>&1\",\"des","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:05:18Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/email_message_fetcher.py --email \"m.wroblewska82@wp.pl\" --days 60 --json 2>&1\",\"description\":\"Fetch client","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:05:21Z","tool":"Bash","cmd":"tasklist //FI \"IMAGENAME eq Photoshop.exe\" 2>/dev/null | grep -i photoshop\",\"description\":\"Check if Photoshop is running\"},\"tool_response\":{\"stdout\":\"Binary file (standard input) matches\"","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:05:39Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_photo_fetcher.py --help 2>&1\",\"description\":\"Check photo fetcher help/options\"},\"tool_response\":{\"stdout\":\"us","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:05:56Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/akrylowe_podziekowanie_matka_chrzestna_wzor1.py --help 2>&1 && echo \"---\" && python tools/generowanie/akrylowe_podziekowani","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:07:17Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/akrylowe_podziekowanie_matka_chrzestna_wzor1.py --klient \"Magdalena Wróblewska\" --imie \"Leosia\" --data \"26","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:07:28Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/akrylowe_podziekowanie_ojciec_chrzestny_wzor1.py --klient \"Magdalena Wróblewska\" --imie \"Leosia\" --data \"2","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:07:36Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:18:49Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:19:09Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/pudelko_komunia_kielich.py --help 2>&1\",\"description\":\"Check script parameters for kielich script\"},\"tool_response\":{\","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:19:33Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Dagmara Pietrzyk-Mroczkowska\" \\\\\\n --imie \"Marysi\" \\\\\\n","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:20:41Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:25:54Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:26:29Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/pudelko_chrzest_golabek_uv.py --help 2>&1\",\"description\":\"Check chrzest gołąbek UV script params\"},\"tool_response\":{","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:27:00Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/pudelko_chrzest_golabek_uv.py \\\\\\n --klient \"Natalia Lewandowska\" \\\\\\n --imie \"Lilianny\" \\\\\\n --","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:27:12Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Natalia Lewandowska\" \\\\\\n --imie \"Julii\" \\\\\\n --data \","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:27:20Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:43:50Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T14:44:16Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONUTF8=1 python tools/generowanie/pudelko_komunia_kielich.py \\\\\\n --klient \"Danuta Kaczmarek-Ruta\" \\\\\\n --imie \"Michaliny\" \\\\\\n --","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T15:05:36Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T15:05:45Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"\\nSELECT \\n oi.id AS i","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:28:50Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Statistics\\\\OrdersStatisticsRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:29:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\docs\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:33:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Statistics\\\\OrdersStatisticsRepository.php","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:34:19Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\TODO.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:34:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\docs\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:37:04Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\105-orders-statistics\\\\105-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:37:16Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-19.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:37:30Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:37:45Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:37:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:39:18Z","tool":"Bash","cmd":"cp \".paul/ROADMAP.md\" \".paul/milestones/v3.0-ROADMAP.md\"\",\"description\":\"Archive v3.0 ROADMAP snapshot\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":f","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:39:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\milestones\\\\v3.0-ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:40:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\MILESTONES.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:40:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:40:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:40:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:40:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:41:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:41:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
|
{"ts":"2026-04-19T20:41:57Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"}
|
||||||
400
.paul/milestones/v3.0-ROADMAP.md
Normal file
400
.paul/milestones/v3.0-ROADMAP.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# v3.0 Mobile Responsive — Archive
|
||||||
|
|
||||||
|
**Archived:** 2026-04-19
|
||||||
|
**Status:** Complete
|
||||||
|
**Scope:** Phases 52–105 (UI mobile, automation, integrations tuning, statistics)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.0 Mobile Responsive — In progress
|
||||||
|
|
||||||
|
Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na telefonach i tabletach.
|
||||||
|
|
||||||
|
| 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 | Planning |
|
||||||
|
| 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 |
|
||||||
|
| TBD | Mobile Orders List | - | Not started |
|
||||||
|
| TBD | Mobile Order Details | - | Not started |
|
||||||
|
| TBD | Mobile Settings | - | Not started |
|
||||||
|
| ... | Kolejne moduly wg potrzeb | - | Not started |
|
||||||
|
|
||||||
|
## Completed Milestones
|
||||||
|
|
||||||
|
<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-04-19 - phase 105 complete (Orders Statistics — menu + raport dzienny per kanal, hotfix collation + fallback netto 23% VAT)*
|
||||||
257
.paul/phases/105-orders-statistics/105-01-PLAN.md
Normal file
257
.paul/phases/105-orders-statistics/105-01-PLAN.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
---
|
||||||
|
phase: 105-orders-statistics
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Statistics/OrdersStatisticsController.php
|
||||||
|
- src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/layouts/app.php
|
||||||
|
- resources/views/statistics/orders.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/app.scss
|
||||||
|
- .paul/docs/ARCHITECTURE.md
|
||||||
|
- .paul/docs/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: auto
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodac nowa sekcje menu `Statystyki -> Zamowienia` z tabela dziennych podsumowan zamowien i kwot (netto/brutto), z rozbiciem na kanaly: `allegro` jako jeden kanal oraz kazda aktywna integracja `shopPRO` jako osobny kanal.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Operator potrzebuje szybkiego podsumowania sprzedazy dziennej w jednym miejscu, z mozliwoscia filtrowania po datach, kanalach sprzedazy i grupach statusow, bez recznego eksportu danych.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowy endpoint i widok `/statistics/orders`.
|
||||||
|
- Filtry: zakres dat (calendar), kanal sprzedazy (multiselect), grupa statusow (multiselect).
|
||||||
|
- Domyslnie wszystkie grupy statusow zaznaczone poza grupa `anulowane`.
|
||||||
|
- Tabela dzienna z dynamicznymi kolumnami per kanal (ilosc/netto/brutto) oraz wierszem `Razem` na dole.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/docs/DB_SCHEMA.md
|
||||||
|
@.paul/docs/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/21-order-source-display/21-01-SUMMARY.md
|
||||||
|
@.paul/phases/85-status-group-filter/85-01-SUMMARY.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@routes/web.php
|
||||||
|
@resources/views/layouts/app.php
|
||||||
|
@resources/lang/pl.php
|
||||||
|
@src/Modules/Orders/OrdersRepository.php
|
||||||
|
@src/Modules/Orders/OrdersController.php
|
||||||
|
@resources/views/components/table-list.php
|
||||||
|
@resources/scss/app.scss
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
## Required Skills (from SPECIAL-FLOWS.md)
|
||||||
|
|
||||||
|
| Skill | Priority | When to Invoke | Loaded? |
|
||||||
|
|-------|----------|----------------|---------|
|
||||||
|
| sonar-scanner (CLI) | required | Po APPLY, przed UNIFY | o |
|
||||||
|
| /feature-dev | optional | Przy implementacji nowej funkcjonalnosci | o |
|
||||||
|
| /frontend-design | optional | Przy dopracowaniu widoku tabeli/statystyk | o |
|
||||||
|
|
||||||
|
## Skill Invocation Checklist
|
||||||
|
- [ ] Potwierdzony plan uruchomienia `sonar-scanner` po APPLY.
|
||||||
|
- [ ] (Opcjonalnie) /feature-dev
|
||||||
|
- [ ] (Opcjonalnie) /frontend-design
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Nowa pozycja menu i routing statystyk
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik jest zalogowany do panelu
|
||||||
|
When otworzy menu boczne
|
||||||
|
Then widzi nowa grupe "Statystyki" z podpozycja "Zamowienia"
|
||||||
|
And klikniecie "Zamowienia" otwiera strone /statistics/orders
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Filtry dzialaja zgodnie z wymaganiami
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik otworzy /statistics/orders
|
||||||
|
When nie poda filtrow
|
||||||
|
Then domyslnie widzi zakres dat obejmujacy biezacy miesiac
|
||||||
|
And domyslnie zaznaczone sa wszystkie grupy statusow poza "anulowane"
|
||||||
|
And domyslnie zaznaczone sa wszystkie kanaly (allegro + wszystkie shopPRO)
|
||||||
|
|
||||||
|
When ustawi date od/do przez pola typu date
|
||||||
|
Then tabela zawiera tylko dni i zamowienia z wybranego zakresu
|
||||||
|
|
||||||
|
When wybierze podzbior kanalow sprzedazy (multiselect)
|
||||||
|
Then tabela pokazuje tylko wybrane kanaly i przelicza sumy tylko dla nich
|
||||||
|
|
||||||
|
When wybierze podzbior grup statusow (multiselect)
|
||||||
|
Then do agregacji wchodza tylko zamowienia o statusach nalezacych do wybranych grup
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Tabela dzienna z rozbiciem kanalowym i podsumowaniem
|
||||||
|
```gherkin
|
||||||
|
Given filtry sa poprawne
|
||||||
|
When backend zwroci dane statystyk
|
||||||
|
Then widok pokazuje tabele z kolumna "Dzien"
|
||||||
|
And dla kazdego wybranego kanalu pokazuje 3 metryki: ilosc zamowien, kwota netto, kwota brutto
|
||||||
|
And na dole tabeli widoczny jest wiersz "Razem" sumujacy wszystkie dni
|
||||||
|
And kwoty sa formatowane do 2 miejsc po przecinku
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Rozbicie kanalow zgodne z modelem danych
|
||||||
|
```gherkin
|
||||||
|
Given zamowienia allegro pochodza z source='allegro'
|
||||||
|
When generujemy statystyki
|
||||||
|
Then wszystkie zamowienia allegro trafiaja do jednego kanalu "Allegro"
|
||||||
|
|
||||||
|
Given zamowienia shopPRO maja source='shoppro' i integration_id
|
||||||
|
When generujemy statystyki
|
||||||
|
Then kazda integracja shopPRO jest liczona jako osobny kanal
|
||||||
|
And naglowek kolumny pokazuje nazwe integracji z tabeli integrations
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Backend statystyk (controller + repository + route)</name>
|
||||||
|
<files>src/Modules/Statistics/OrdersStatisticsController.php, src/Modules/Statistics/OrdersStatisticsRepository.php, routes/web.php</files>
|
||||||
|
<action>
|
||||||
|
1. Dodac nowy modul `Statistics` z klasami:
|
||||||
|
- `OrdersStatisticsController` (parsowanie filtrow, walidacja, budowa view-modelu).
|
||||||
|
- `OrdersStatisticsRepository` (agregacje SQL przez prepared statements).
|
||||||
|
2. W `routes/web.php`:
|
||||||
|
- Zainicjalizowac controller (analogicznie do pozostalych modulow).
|
||||||
|
- Dodac route `GET /statistics/orders` za `AuthMiddleware`.
|
||||||
|
3. Implementacja filtrow:
|
||||||
|
- Daty: `date_from`, `date_to` (format `YYYY-MM-DD`, fallback: pierwszy i ostatni dzien biezacego miesiaca).
|
||||||
|
- Kanaly (multiselect): `channels[]` (np. `allegro`, `shoppro:12`).
|
||||||
|
- Grupy statusow (multiselect): `status_groups[]` (id grup).
|
||||||
|
- Domyslnie wybierz wszystkie grupy poza grupa, ktorej nazwa po normalizacji to `anulowane`.
|
||||||
|
4. Implementacja agregacji:
|
||||||
|
- Dzien: `DATE(COALESCE(o.ordered_at, o.source_created_at, o.source_updated_at, o.fetched_at))`.
|
||||||
|
- Kanal:
|
||||||
|
- `allegro` gdy `o.source = 'allegro'`
|
||||||
|
- `shoppro:{integration_id}` gdy `o.source = 'shoppro'`
|
||||||
|
- Metryki:
|
||||||
|
- `orders_count = COUNT(*)`
|
||||||
|
- `total_net = SUM(COALESCE(o.total_net, o.total_without_tax, 0))`
|
||||||
|
- `total_gross = SUM(COALESCE(o.total_with_tax, 0))`
|
||||||
|
- Filtr statusow przez mapowanie grupa -> lista `order_statuses.code`, porownanie do efektywnego statusu (zachowac zgodnosc z logika Allegro mapowania stosowana w OrdersRepository).
|
||||||
|
5. Zwracany model musi zawierac:
|
||||||
|
- liste dni,
|
||||||
|
- liste aktywnych kanalow,
|
||||||
|
- dane tabeli per dzien/per kanal,
|
||||||
|
- sumy koncowe per kanal i globalnie.
|
||||||
|
|
||||||
|
Avoid: nie dokladac migracji DB; nie wprowadzac SQL skladanego stringami z danymi usera; wszystkie parametry bindowac przez prepared statements.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `php -l src/Modules/Statistics/OrdersStatisticsController.php`
|
||||||
|
- `php -l src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||||
|
- Wejscie na `/statistics/orders` zwraca HTTP 200 i nie rzuca exception.
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 i AC-4 spelnione na warstwie backend.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: UI strony statystyk + menu + tlumaczenia</name>
|
||||||
|
<files>resources/views/layouts/app.php, resources/views/statistics/orders.php, resources/lang/pl.php, resources/scss/app.scss</files>
|
||||||
|
<action>
|
||||||
|
1. W sidebarze dodac nowa grupe:
|
||||||
|
- `Statystyki` jako menu glowne,
|
||||||
|
- podpozycja `Zamowienia` linkujaca do `/statistics/orders`,
|
||||||
|
- aktywny stan oparty o `activeMenu='statistics'` i `activeStatistics='orders'`.
|
||||||
|
2. W `resources/lang/pl.php` dodac klucze:
|
||||||
|
- `navigation.statistics`,
|
||||||
|
- `navigation.statistics_orders`,
|
||||||
|
- sekcje `statistics.orders.*` (tytul, opisy, etykiety filtrow, nazwy kolumn, pusty stan, razem).
|
||||||
|
3. Stworzyc widok `resources/views/statistics/orders.php`:
|
||||||
|
- karta z formularzem filtrow (2 pola `date`, multiselect kanalow, multiselect grup statusow),
|
||||||
|
- tabela: kolumna `Dzien`, dynamiczne grupy kolumn kanalowych (`Ilosc`, `Netto`, `Brutto`), stopka `Razem`.
|
||||||
|
- escape danych helperem `$e`.
|
||||||
|
4. Multiselect:
|
||||||
|
- bez dodawania nowych zaleznosci JS; zastosowac natywne `select multiple` + kompaktowy CSS.
|
||||||
|
- nazwy inputow: `channels[]`, `status_groups[]`.
|
||||||
|
5. Style:
|
||||||
|
- dopisac sekcje SCSS do `resources/scss/app.scss` dla kompaktowego layoutu filtrow i tabeli statystyk.
|
||||||
|
- nie dodawac CSS inline w widoku.
|
||||||
|
|
||||||
|
Avoid: nie uzywac natywnych `alert()`/`confirm()`; brak logiki biznesowej w widoku.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Po wejściu na dowolna strone menu widzi `Statystyki -> Zamowienia`.
|
||||||
|
- `/statistics/orders` pokazuje formularz filtrow i tabele/empty-state bez bledow.
|
||||||
|
- `npm run build` (lub projektowy build assets) generuje aktualny CSS bez bledow.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-3 spelnione na warstwie UI.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Dokumentacja techniczna po wdrozeniu</name>
|
||||||
|
<files>.paul/docs/ARCHITECTURE.md, .paul/docs/TECH_CHANGELOG.md</files>
|
||||||
|
<action>
|
||||||
|
1. Zaktualizowac `.paul/docs/ARCHITECTURE.md`:
|
||||||
|
- dodac modul `Statistics` i opis nowych klas/metod oraz przeplywu danych.
|
||||||
|
- opisac nowy endpoint `/statistics/orders`.
|
||||||
|
2. Zaktualizowac `.paul/docs/TECH_CHANGELOG.md`:
|
||||||
|
- wpis z data wdrozenia, zakresem funkcji i uzasadnieniem biznesowym.
|
||||||
|
3. Poniewaz zmiana nie modyfikuje schematu DB:
|
||||||
|
- w changelogu jasno zaznaczyc brak zmian migracyjnych.
|
||||||
|
|
||||||
|
Avoid: nie pomijac dokumentacji, bo to trwale wymaganie projektowe.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `git diff -- .paul/docs/ARCHITECTURE.md .paul/docs/TECH_CHANGELOG.md` pokazuje oczekiwane wpisy.
|
||||||
|
</verify>
|
||||||
|
<done>Dokumentacja projektu zgodna z wymaganiami utrwalania zmian.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Moduly importu/synchronizacji (Allegro/shopPRO) - brak zmian logiki importu.
|
||||||
|
- Istniejace endpointy `/orders/*` i ich kontrakty.
|
||||||
|
- Struktura tabel DB i migracje.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Zakres obejmuje tylko statystyki zamowien (bez produktow, magazynu, paragonow).
|
||||||
|
- Brak eksportu CSV/XLSX w tej fazie.
|
||||||
|
- Brak cache materializowanego; agregacja wykonywana bezposrednio na zapytaniach SQL.
|
||||||
|
- Brak nowego API publicznego JSON - tylko widok HTML panelu.
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `php -l` dla nowych klas Statistics.
|
||||||
|
- [ ] Manualne UAT filtrow: data, kanaly multiselect, grupy statusow multiselect.
|
||||||
|
- [ ] UAT domyslnego filtra grup statusow (wszystkie poza `anulowane`).
|
||||||
|
- [ ] UAT podsumowania: wiersz `Razem` zgodny z suma dziennych wierszy.
|
||||||
|
- [ ] Aktualizacja `.paul/docs/ARCHITECTURE.md` i `.paul/docs/TECH_CHANGELOG.md`.
|
||||||
|
- [ ] `sonar-scanner` uruchomiony po APPLY (wymog SPECIAL-FLOWS).
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Uzytkownik ma nowa pozycje menu `Statystyki -> Zamowienia`.
|
||||||
|
- Widok zwraca dzienne podsumowanie z rozbiciem: Allegro + kazdy shopPRO osobno.
|
||||||
|
- Filtry dzialaja: zakres dat, kanaly multiselect, grupy statusow multiselect.
|
||||||
|
- Domyslne grupy statusow wykluczaja `anulowane`.
|
||||||
|
- Wiersz `Razem` poprawnie sumuje tabele.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/105-orders-statistics/105-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
|
|
||||||
158
.paul/phases/105-orders-statistics/105-01-SUMMARY.md
Normal file
158
.paul/phases/105-orders-statistics/105-01-SUMMARY.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
phase: 105-orders-statistics
|
||||||
|
plan: 01
|
||||||
|
subsystem: statistics
|
||||||
|
tags: [statistics, orders, reporting, filters, ui, mysql-collation, vat-fallback]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: none
|
||||||
|
provides: existing orders + status dictionaries
|
||||||
|
provides:
|
||||||
|
- new menu section Statistics -> Orders
|
||||||
|
- daily orders report by channel with totals
|
||||||
|
- filters: date range, channels multiselect, status groups multiselect
|
||||||
|
- net amount fallback (23% VAT) when source channel does not expose net
|
||||||
|
affects: [orders analytics workflow]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- daily SQL aggregation with dynamic channel columns
|
||||||
|
- explicit COLLATE utf8mb4_unicode_ci on CAST expressions to avoid collation mix in mixed-charset schemas
|
||||||
|
- brutto->netto deterministic fallback (/ 1.23) as last resort when source net is missing
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- src/Modules/Statistics/OrdersStatisticsController.php
|
||||||
|
- src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||||
|
- resources/views/statistics/orders.php
|
||||||
|
- .paul/TODO.md
|
||||||
|
modified:
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/layouts/app.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/app.scss
|
||||||
|
- public/assets/css/app.css
|
||||||
|
- .paul/docs/ARCHITECTURE.md
|
||||||
|
- .paul/docs/DB_SCHEMA.md
|
||||||
|
- .paul/docs/TECH_CHANGELOG.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Channel split: allegro as one channel, shopPRO by integration_id (shoppro:{id})"
|
||||||
|
- "Default status groups: all active except group normalized to 'anulowane'"
|
||||||
|
- "No DB migration - feature based on existing schema"
|
||||||
|
- "Net fallback: gross / 1.23 when orders.total_without_tax is NULL or 0 (shopPRO does not send net)"
|
||||||
|
- "Explicit COLLATE utf8mb4_unicode_ci on channel CASE (avoids 1271 Illegal mix of collations with CAST integer->CHAR)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Statistics module pattern (Controller + Repository + dedicated view)"
|
||||||
|
- "Filter presence hidden flags to distinguish defaults from explicit empty multiselect"
|
||||||
|
- "Post-APPLY hotfix pattern: collation bug caught by catching Throwable in repo → silently empty response; require explicit COLLATE on any CAST(int AS CHAR) used in IN clauses"
|
||||||
|
|
||||||
|
completed: 2026-04-19
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 105 Plan 01: Orders Statistics Summary
|
||||||
|
|
||||||
|
Dodano nowa sekcje `Statystyki -> Zamowienia` z dziennym raportem ilosci/netto/brutto, filtrami (daty, kanaly multiselect, grupy statusow multiselect) oraz stopka `Podsumowanie`. Po poczatkowym wdrozeniu poprawiono bug kolizji collation w MySQL (widok zwracal pusta tabele) oraz dodano fallback wyliczania netto z brutto dla zrodel (shopPRO) ktore nie wysylaja kwoty netto.
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Nowa pozycja menu i routing | Pass | `/statistics/orders` + nowy blok menu |
|
||||||
|
| AC-2: Filtry dat/kanalow/grup statusow | Pass | Date range + multiselect + domyslne wykluczenie `anulowane` |
|
||||||
|
| AC-3: Tabela dzienna + wiersz podsumowania | Pass | Dynamiczne kolumny per kanal, stopka `Podsumowanie` |
|
||||||
|
| AC-4: Rozbicie na Allegro i shopPRO per integracja | Pass | `allegro` i `shoppro:{integration_id}` |
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Statistics/OrdersStatisticsController.php` | Created | Parsowanie filtrow, budowa view-modelu, render strony |
|
||||||
|
| `src/Modules/Statistics/OrdersStatisticsRepository.php` | Created | Agregacja dzienna SQL + diagnostics + fallback netto |
|
||||||
|
| `resources/views/statistics/orders.php` | Created | Widok formularza filtrow i tabeli dziennej |
|
||||||
|
| `routes/web.php` | Modified | Route `GET /statistics/orders` za `AuthMiddleware` |
|
||||||
|
| `resources/views/layouts/app.php` | Modified | Sidebar: grupa `Statystyki -> Zamowienia` |
|
||||||
|
| `resources/lang/pl.php` | Modified | Klucze `navigation.statistics*`, `statistics.orders.*` |
|
||||||
|
| `resources/scss/app.scss` | Modified | Kompaktowy layout filtrow i tabeli statystyk |
|
||||||
|
| `public/assets/css/app.css` | Modified | Build SCSS |
|
||||||
|
| `.paul/docs/ARCHITECTURE.md` | Modified | Opis modulu Statistics + endpoint |
|
||||||
|
| `.paul/docs/DB_SCHEMA.md` | Modified | Adnotacja: feature bez migracji |
|
||||||
|
| `.paul/docs/TECH_CHANGELOG.md` | Modified | Wpis + hotfix collation + hotfix fallback netto |
|
||||||
|
| `.paul/TODO.md` | Created | Tag `STAT-NET`: docelowy netto z shopPRO / order_items.tax_rate |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Explicit `COLLATE utf8mb4_unicode_ci` na CASE zwracajacym `channel_key` | `CAST(integration_id AS CHAR)` zwracal `utf8mb4_bin`, przez co `IN (...)` z parametrami (`utf8mb4_general_ci`) rzucal `1271 Illegal mix of collations`. Repo lapalo `Throwable` i zwracalo `[]`, widok byl pusty. | Statystyki dzialaja; pattern udokumentowany dla przyszlych agregacji po `integration_id` |
|
||||||
|
| Fallback netto `ROUND(gross / 1.23, 2)` gdy `total_without_tax` puste | shopPRO nie przesyla netto ani na poziomie zamowienia, ani pozycji (`order_items.original_price_without_tax` = NULL); bez fallbacku kolumna `Netto` pokazywala 0 | Kolumna `Netto` pokazuje sensowne wartosci; docelowe rozwiazanie (STAT-NET) zapisane w TODO |
|
||||||
|
| Status codes pozostaja `order_statuses.code` (LOWER-normalized) | Mapping Allegro zachowuje zgodnosc z `OrdersRepository` dzieki `allegro_order_status_mappings` | Spojna semantyka statusow w module Orders i Statistics |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Essential — zerowe wyniki zanim hotfix |
|
||||||
|
| Scope additions | 1 | Uzasadnione — brak fallback netto byl pokazywany jako bug |
|
||||||
|
| Deferred | 1 | `STAT-NET` w `.paul/TODO.md` |
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [SQL] Kolizja collation w channelSql() — statystyki zwracaly pusta tabele**
|
||||||
|
- **Found during:** Post-APPLY walidacja — user zglosil "strona statystyk nie pokazuje zamowien"
|
||||||
|
- **Issue:** `CAST(o.integration_id AS CHAR)` daje `utf8mb4_bin`, `CONCAT("shoppro:", ...)` + `IN (:ch0,:ch1)` rzuca `SQLSTATE[HY000] 1271`. `try/catch(Throwable)` polykal blad → puste dane.
|
||||||
|
- **Fix:** `COLLATE utf8mb4_unicode_ci` na `CAST(...)` oraz na calym wyrazeniu CASE zwracajacym `channel_key`.
|
||||||
|
- **Files:** `src/Modules/Statistics/OrdersStatisticsRepository.php` (`channelSql`)
|
||||||
|
- **Verification:** End-to-end test na produkcyjnym DB (host700513) — 41 wierszy zagregowanych dla 2026-04-01..30, kanaly: `allegro, shoppro:5, shoppro:6, shoppro:7`.
|
||||||
|
|
||||||
|
### Scope Additions
|
||||||
|
|
||||||
|
**1. [Feature] Fallback netto 23% VAT**
|
||||||
|
- **Why:** shopPRO nie wysyla `total_without_tax`; `order_items.original_price_without_tax` rowniez NULL. Bez fallbacku kolumna Netto byla bezuzyteczna (same zera).
|
||||||
|
- **Scope:** `netAmountSql()` rozszerzony o `CASE ... WHEN total_without_tax > 0 THEN total_without_tax ... WHEN total_with_tax > 0 THEN ROUND(total_with_tax/1.23, 2) ELSE 0 END`.
|
||||||
|
- **Files:** `src/Modules/Statistics/OrdersStatisticsRepository.php`, `.paul/docs/TECH_CHANGELOG.md`, `.paul/TODO.md`
|
||||||
|
|
||||||
|
### Deferred Items
|
||||||
|
|
||||||
|
- **STAT-NET:** Pobieranie netto z shopPRO (API) lub dokladne wyliczanie z `order_items.tax_rate` (unikniecie sztywnego 23%). Backfill historycznych rekordow. Zapisane w `.paul/TODO.md`.
|
||||||
|
|
||||||
|
## Verification Executed
|
||||||
|
|
||||||
|
- `php -l src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||||
|
- `php -l src/Modules/Statistics/OrdersStatisticsController.php`
|
||||||
|
- `php -l routes/web.php`
|
||||||
|
- `php -l resources/views/layouts/app.php`
|
||||||
|
- `php -l resources/views/statistics/orders.php`
|
||||||
|
- `php -l resources/lang/pl.php`
|
||||||
|
- `npm run build:css`
|
||||||
|
- End-to-end on production DB (post-hotfix): 41 agregowanych wierszy dla 2026-04, netto/brutto zgodne (np. shoppro:7 2026-04-19: orders=17, net=989.45, gross=1217.05).
|
||||||
|
|
||||||
|
## Skill Audit
|
||||||
|
|
||||||
|
| Expected | Invoked | Notes |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| `sonar-scanner` (required) | o | Gap — nie uruchomiony podczas tej sesji UNIFY. `.scannerwork/report-task.txt` pokazuje modyfikacje wczesniej; skan dedykowany do tej zmiany nie wykonany. |
|
||||||
|
| /feature-dev (optional) | o | Nie uzywany |
|
||||||
|
| /frontend-design (optional) | o | Nie uzywany |
|
||||||
|
| /code-review (optional) | o | Nie uzywany |
|
||||||
|
| /simplify (optional) | o | Nie uzywany |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Modul Statistics dziala; schemat patternu dla kolejnych raportow (per-kanalowych).
|
||||||
|
- TODO `STAT-NET` przechowane z konkretnym planem realizacji.
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Kolumna `Netto` na kanalach shopPRO jest obecnie estymatem (sztywne 23% VAT). Jesli pojawia sie produkty o innej stawce (5%, 8%, 0%), agregat bedzie zawyzal netto dla tych pozycji.
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 105-orders-statistics, Plan: 01*
|
||||||
|
*Completed: 2026-04-19*
|
||||||
@@ -2,5 +2,5 @@ projectKey=orderPRO
|
|||||||
serverUrl=https://sonar.project-pro.pl
|
serverUrl=https://sonar.project-pro.pl
|
||||||
serverVersion=26.3.0.120487
|
serverVersion=26.3.0.120487
|
||||||
dashboardUrl=https://sonar.project-pro.pl/dashboard?id=orderPRO
|
dashboardUrl=https://sonar.project-pro.pl/dashboard?id=orderPRO
|
||||||
ceTaskId=995489dc-a44a-4b15-a2e9-d992a8884994
|
ceTaskId=eb3d3f09-2a6e-4fd3-a7ae-5c3724064a7f
|
||||||
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=995489dc-a44a-4b15-a2e9-d992a8884994
|
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=eb3d3f09-2a6e-4fd3-a7ae-5c3724064a7f
|
||||||
|
|||||||
38
.vscode/ftp-kr.sync.cache.json
vendored
38
.vscode/ftp-kr.sync.cache.json
vendored
@@ -882,44 +882,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DOCS": {
|
|
||||||
"ARCHITECTURE.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 44136,
|
|
||||||
"lmtime": 1776408155319,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"DB_SCHEMA.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 34555,
|
|
||||||
"lmtime": 1775948078122,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"ORDERS_SCHEMA_APILO_DRAFT.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 2464,
|
|
||||||
"lmtime": 1772493922430,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"ORDERS_SCHEMA_DRAFT.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 1706,
|
|
||||||
"lmtime": 1772494183695,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"SHIPMENT_TRACKING_STATUSES.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 8703,
|
|
||||||
"lmtime": 1774287008643,
|
|
||||||
"modified": false
|
|
||||||
},
|
|
||||||
"TECH_CHANGELOG.md": {
|
|
||||||
"type": "-",
|
|
||||||
"size": 83494,
|
|
||||||
"lmtime": 1776408168979,
|
|
||||||
"modified": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
".env": {
|
".env": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 924,
|
"size": 924,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -23,6 +23,8 @@ return [
|
|||||||
'products' => 'Produkty',
|
'products' => 'Produkty',
|
||||||
'orders' => 'Zamowienia',
|
'orders' => 'Zamowienia',
|
||||||
'orders_list' => 'Lista zamowien',
|
'orders_list' => 'Lista zamowien',
|
||||||
|
'statistics' => 'Statystyki',
|
||||||
|
'statistics_orders' => 'Zamowienia',
|
||||||
'marketplace' => 'Marketplace',
|
'marketplace' => 'Marketplace',
|
||||||
'cron' => 'Harmonogram',
|
'cron' => 'Harmonogram',
|
||||||
'dashboard' => 'Dashboard',
|
'dashboard' => 'Dashboard',
|
||||||
@@ -217,6 +219,31 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'statistics' => [
|
||||||
|
'orders' => [
|
||||||
|
'title' => 'Statystyki zamowien',
|
||||||
|
'description' => 'Dzienne podsumowanie ilosci i kwot zamowien z podzialem na kanaly sprzedazy.',
|
||||||
|
'empty' => 'Brak danych dla wybranych filtrow.',
|
||||||
|
'filters' => [
|
||||||
|
'date_from' => 'Data od',
|
||||||
|
'date_to' => 'Data do',
|
||||||
|
'channels' => 'Kanaly sprzedazy',
|
||||||
|
'status_groups' => 'Grupy statusow',
|
||||||
|
],
|
||||||
|
'columns' => [
|
||||||
|
'day' => 'Dzien',
|
||||||
|
'orders_count' => 'Ilosc',
|
||||||
|
'total_net' => 'Netto',
|
||||||
|
'total_gross' => 'Brutto',
|
||||||
|
'total' => 'Razem',
|
||||||
|
'summary' => 'Podsumowanie',
|
||||||
|
],
|
||||||
|
'actions' => [
|
||||||
|
'apply_filters' => 'Filtruj',
|
||||||
|
'reset_filters' => 'Wyczysc',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'users' => [
|
'users' => [
|
||||||
'title' => 'Zarzadzanie uzytkownikami',
|
'title' => 'Zarzadzanie uzytkownikami',
|
||||||
'description' => 'Dodawaj konta dostepowe dla zespolu i zarzadzaj dostepem do panelu.',
|
'description' => 'Dodawaj konta dostepowe dla zespolu i zarzadzaj dostepem do panelu.',
|
||||||
|
|||||||
@@ -1041,6 +1041,65 @@ h4.section-title {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statistics-orders-page {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-filters__actions {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-multiselect {
|
||||||
|
min-height: 120px;
|
||||||
|
height: 120px;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-orders-table {
|
||||||
|
min-width: 880px;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td,
|
||||||
|
tfoot th {
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td:first-child,
|
||||||
|
tfoot th:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot th {
|
||||||
|
border-top: 2px solid #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.orders-head {
|
.orders-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<?php $currentMenu = (string) ($activeMenu ?? ''); ?>
|
<?php $currentMenu = (string) ($activeMenu ?? ''); ?>
|
||||||
<?php $currentSettings = (string) ($activeSettings ?? ''); ?>
|
<?php $currentSettings = (string) ($activeSettings ?? ''); ?>
|
||||||
<?php $currentOrders = (string) ($activeOrders ?? ''); ?>
|
<?php $currentOrders = (string) ($activeOrders ?? ''); ?>
|
||||||
|
<?php $currentStatistics = (string) ($activeStatistics ?? ''); ?>
|
||||||
<div class="app-shell" id="js-app-shell">
|
<div class="app-shell" id="js-app-shell">
|
||||||
<aside class="sidebar" id="js-sidebar">
|
<aside class="sidebar" id="js-sidebar">
|
||||||
<div class="sidebar__brand">
|
<div class="sidebar__brand">
|
||||||
@@ -47,6 +48,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="sidebar__group<?= $currentMenu === 'statistics' ? ' is-active' : '' ?>"<?= $currentMenu === 'statistics' ? ' open' : '' ?>>
|
||||||
|
<summary class="sidebar__group-toggle">
|
||||||
|
<span class="sidebar__icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 20V10"/>
|
||||||
|
<path d="M10 20V4"/>
|
||||||
|
<path d="M16 20v-8"/>
|
||||||
|
<path d="M22 20v-5"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="sidebar__label"><?= $e($t('navigation.statistics')) ?></span>
|
||||||
|
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M6 9l6 6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="sidebar__group-links">
|
||||||
|
<a class="sidebar__sublink<?= $currentMenu === 'statistics' && $currentStatistics === 'orders' ? ' is-active' : '' ?>" href="/statistics/orders">
|
||||||
|
<?= $e($t('navigation.statistics_orders')) ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<a class="sidebar__link<?= ($currentMenu ?? '') === 'accounting' ? ' is-active' : '' ?>" href="/accounting">
|
<a class="sidebar__link<?= ($currentMenu ?? '') === 'accounting' ? ' is-active' : '' ?>" href="/accounting">
|
||||||
<span class="sidebar__icon">
|
<span class="sidebar__icon">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|||||||
162
resources/views/statistics/orders.php
Normal file
162
resources/views/statistics/orders.php
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$filters = is_array($filters ?? null) ? $filters : [];
|
||||||
|
$channelOptions = is_array($channelOptions ?? null) ? $channelOptions : [];
|
||||||
|
$statusGroupOptions = is_array($statusGroupOptions ?? null) ? $statusGroupOptions : [];
|
||||||
|
$table = is_array($table ?? null) ? $table : [];
|
||||||
|
|
||||||
|
$selectedChannels = is_array($filters['selected_channels'] ?? null) ? $filters['selected_channels'] : [];
|
||||||
|
$selectedStatusGroups = is_array($filters['selected_status_groups'] ?? null) ? $filters['selected_status_groups'] : [];
|
||||||
|
|
||||||
|
$rows = is_array($table['rows'] ?? null) ? $table['rows'] : [];
|
||||||
|
$totals = is_array($table['totals'] ?? null) ? $table['totals'] : ['channels' => []];
|
||||||
|
$hasData = (bool) ($table['hasData'] ?? false);
|
||||||
|
$debugEnabled = (bool) ($debugEnabled ?? false);
|
||||||
|
$diagnostics = is_array($diagnostics ?? null) ? $diagnostics : [];
|
||||||
|
$debugMeta = is_array($debugMeta ?? null) ? $debugMeta : [];
|
||||||
|
|
||||||
|
$channelMap = [];
|
||||||
|
foreach ($channelOptions as $channelOption) {
|
||||||
|
$key = (string) ($channelOption['key'] ?? '');
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$channelMap[$key] = (string) ($channelOption['label'] ?? $key);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="card statistics-orders-page">
|
||||||
|
<div class="statistics-orders-head">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title"><?= $e($t('statistics.orders.title')) ?></h2>
|
||||||
|
<p class="muted mt-12"><?= $e($t('statistics.orders.description')) ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="get" action="/statistics/orders" class="statistics-orders-filters mt-16">
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('statistics.orders.filters.date_from')) ?></span>
|
||||||
|
<input class="form-control" type="date" name="date_from" value="<?= $e((string) ($filters['date_from'] ?? '')) ?>">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('statistics.orders.filters.date_to')) ?></span>
|
||||||
|
<input class="form-control" type="date" name="date_to" value="<?= $e((string) ($filters['date_to'] ?? '')) ?>">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('statistics.orders.filters.channels')) ?></span>
|
||||||
|
<select class="form-control statistics-orders-multiselect" name="channels[]" multiple size="6">
|
||||||
|
<?php foreach ($channelOptions as $channelOption): ?>
|
||||||
|
<?php
|
||||||
|
$key = (string) ($channelOption['key'] ?? '');
|
||||||
|
$label = (string) ($channelOption['label'] ?? $key);
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($key) ?>"<?= in_array($key, $selectedChannels, true) ? ' selected' : '' ?>>
|
||||||
|
<?= $e($label) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-field">
|
||||||
|
<span class="field-label"><?= $e($t('statistics.orders.filters.status_groups')) ?></span>
|
||||||
|
<select class="form-control statistics-orders-multiselect" name="status_groups[]" multiple size="6">
|
||||||
|
<?php foreach ($statusGroupOptions as $groupOption): ?>
|
||||||
|
<?php
|
||||||
|
$groupId = (int) ($groupOption['id'] ?? 0);
|
||||||
|
$groupName = (string) ($groupOption['name'] ?? '');
|
||||||
|
if ($groupId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<option value="<?= $e((string) $groupId) ?>"<?= in_array($groupId, $selectedStatusGroups, true) ? ' selected' : '' ?>>
|
||||||
|
<?= $e($groupName) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="form-field statistics-orders-filters__actions">
|
||||||
|
<span class="field-label"> </span>
|
||||||
|
<div class="filters-actions">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('statistics.orders.actions.apply_filters')) ?></button>
|
||||||
|
<a href="/statistics/orders" class="btn btn--secondary"><?= $e($t('statistics.orders.actions.reset_filters')) ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card mt-16 statistics-orders-table-wrap">
|
||||||
|
<?php if ($debugEnabled): ?>
|
||||||
|
<div class="alert alert--warning" role="alert">
|
||||||
|
<strong>DEBUG</strong>
|
||||||
|
<pre style="margin:8px 0 0;white-space:pre-wrap;"><?= $e(json_encode([
|
||||||
|
'filters' => $debugMeta,
|
||||||
|
'diagnostics' => $diagnostics,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$hasData): ?>
|
||||||
|
<p class="muted"><?= $e($t('statistics.orders.empty')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="table statistics-orders-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2"><?= $e($t('statistics.orders.columns.day')) ?></th>
|
||||||
|
<?php foreach ($selectedChannels as $channelKey): ?>
|
||||||
|
<th colspan="3"><?= $e($channelMap[$channelKey] ?? $channelKey) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<th colspan="3"><?= $e($t('statistics.orders.columns.total')) ?></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<?php foreach ($selectedChannels as $channelKey): ?>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.orders_count')) ?></th>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.total_net')) ?></th>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.total_gross')) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.orders_count')) ?></th>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.total_net')) ?></th>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.total_gross')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($rows as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e((string) ($row['day'] ?? '')) ?></td>
|
||||||
|
<?php foreach ($selectedChannels as $channelKey): ?>
|
||||||
|
<?php $channelStats = is_array($row['channels'][$channelKey] ?? null) ? $row['channels'][$channelKey] : []; ?>
|
||||||
|
<td><?= $e((string) ((int) ($channelStats['orders_count'] ?? 0))) ?></td>
|
||||||
|
<td><?= $e(number_format((float) ($channelStats['total_net'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
<td><?= $e(number_format((float) ($channelStats['total_gross'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<td><?= $e((string) ((int) ($row['day_total_orders'] ?? 0))) ?></td>
|
||||||
|
<td><?= $e(number_format((float) ($row['day_total_net'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
<td><?= $e(number_format((float) ($row['day_total_gross'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('statistics.orders.columns.summary')) ?></th>
|
||||||
|
<?php foreach ($selectedChannels as $channelKey): ?>
|
||||||
|
<?php $channelTotals = is_array($totals['channels'][$channelKey] ?? null) ? $totals['channels'][$channelKey] : []; ?>
|
||||||
|
<th><?= $e((string) ((int) ($channelTotals['orders_count'] ?? 0))) ?></th>
|
||||||
|
<th><?= $e(number_format((float) ($channelTotals['total_net'] ?? 0), 2, '.', ' ')) ?></th>
|
||||||
|
<th><?= $e(number_format((float) ($channelTotals['total_gross'] ?? 0), 2, '.', ' ')) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<th><?= $e((string) ((int) ($totals['orders_count'] ?? 0))) ?></th>
|
||||||
|
<th><?= $e(number_format((float) ($totals['total_net'] ?? 0), 2, '.', ' ')) ?></th>
|
||||||
|
<th><?= $e(number_format((float) ($totals['total_gross'] ?? 0), 2, '.', ' ')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
@@ -11,6 +11,8 @@ use App\Modules\Cron\CronRepository;
|
|||||||
use App\Modules\Orders\OrdersController;
|
use App\Modules\Orders\OrdersController;
|
||||||
use App\Modules\Orders\OrderImportRepository;
|
use App\Modules\Orders\OrderImportRepository;
|
||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
|
use App\Modules\Statistics\OrdersStatisticsController;
|
||||||
|
use App\Modules\Statistics\OrdersStatisticsRepository;
|
||||||
use App\Modules\Settings\AllegroApiClient;
|
use App\Modules\Settings\AllegroApiClient;
|
||||||
use App\Modules\Settings\AllegroDeliveryMappingController;
|
use App\Modules\Settings\AllegroDeliveryMappingController;
|
||||||
use App\Modules\Settings\AllegroIntegrationController;
|
use App\Modules\Settings\AllegroIntegrationController;
|
||||||
@@ -282,6 +284,12 @@ return static function (Application $app): void {
|
|||||||
);
|
);
|
||||||
$printJobRepository = new PrintJobRepository($app->db());
|
$printJobRepository = new PrintJobRepository($app->db());
|
||||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository, $automationService);
|
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository, $shopproIntegrationsRepository, $automationService);
|
||||||
|
$ordersStatisticsController = new OrdersStatisticsController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
new OrdersStatisticsRepository($app->db())
|
||||||
|
);
|
||||||
$receiptController = new ReceiptController(
|
$receiptController = new ReceiptController(
|
||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
@@ -420,6 +428,7 @@ return static function (Application $app): void {
|
|||||||
$router->get('/users', static fn (Request $request): Response => Response::redirect('/settings/users'), [$authMiddleware]);
|
$router->get('/users', static fn (Request $request): Response => Response::redirect('/settings/users'), [$authMiddleware]);
|
||||||
$router->get('/orders', static fn (Request $request): Response => Response::redirect('/orders/list'), [$authMiddleware]);
|
$router->get('/orders', static fn (Request $request): Response => Response::redirect('/orders/list'), [$authMiddleware]);
|
||||||
$router->get('/orders/list', [$ordersController, 'index'], [$authMiddleware]);
|
$router->get('/orders/list', [$ordersController, 'index'], [$authMiddleware]);
|
||||||
|
$router->get('/statistics/orders', [$ordersStatisticsController, 'index'], [$authMiddleware]);
|
||||||
$router->get('/orders/{id}', [$ordersController, 'show'], [$authMiddleware]);
|
$router->get('/orders/{id}', [$ordersController, 'show'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]);
|
$router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/details/update', [$ordersController, 'updateDetails'], [$authMiddleware]);
|
$router->post('/orders/{id}/details/update', [$ordersController, 'updateDetails'], [$authMiddleware]);
|
||||||
|
|||||||
369
src/Modules/Statistics/OrdersStatisticsController.php
Normal file
369
src/Modules/Statistics/OrdersStatisticsController.php
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Statistics;
|
||||||
|
|
||||||
|
use App\Core\Http\Request;
|
||||||
|
use App\Core\Http\Response;
|
||||||
|
use App\Core\Security\Csrf;
|
||||||
|
use App\Core\View\Template;
|
||||||
|
use App\Core\I18n\Translator;
|
||||||
|
use App\Modules\Auth\AuthService;
|
||||||
|
use DateInterval;
|
||||||
|
use DatePeriod;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
final class OrdersStatisticsController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Template $template,
|
||||||
|
private readonly Translator $translator,
|
||||||
|
private readonly AuthService $auth,
|
||||||
|
private readonly OrdersStatisticsRepository $repository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
[$dateFrom, $dateTo] = $this->resolveDateRange($request);
|
||||||
|
|
||||||
|
$statusGroups = $this->repository->listStatusGroups();
|
||||||
|
$statusGroupOptions = $this->mapStatusGroupOptions($statusGroups);
|
||||||
|
$selectedStatusGroups = $this->resolveSelectedStatusGroups($request, $statusGroupOptions);
|
||||||
|
|
||||||
|
$channelOptions = $this->mapChannelOptions($this->repository->listChannelOptions());
|
||||||
|
$selectedChannels = $this->resolveSelectedChannels($request, $channelOptions);
|
||||||
|
|
||||||
|
$statusCodes = $this->repository->statusCodesByGroupIds($selectedStatusGroups);
|
||||||
|
$aggregated = $this->repository->aggregateByDay($dateFrom, $dateTo, $selectedChannels, $statusCodes);
|
||||||
|
$debugEnabled = (string) $request->input('debug', '') === '1';
|
||||||
|
$diagnostics = $debugEnabled
|
||||||
|
? $this->repository->diagnostics($dateFrom, $dateTo, $selectedChannels, $statusCodes)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$table = $this->buildTable($dateFrom, $dateTo, $selectedChannels, $aggregated);
|
||||||
|
|
||||||
|
$html = $this->template->render('statistics/orders', [
|
||||||
|
'title' => $this->translator->get('statistics.orders.title'),
|
||||||
|
'activeMenu' => 'statistics',
|
||||||
|
'activeStatistics' => 'orders',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'csrfToken' => Csrf::token(),
|
||||||
|
'filters' => [
|
||||||
|
'date_from' => $dateFrom,
|
||||||
|
'date_to' => $dateTo,
|
||||||
|
'selected_channels' => $selectedChannels,
|
||||||
|
'selected_status_groups' => $selectedStatusGroups,
|
||||||
|
],
|
||||||
|
'channelOptions' => $channelOptions,
|
||||||
|
'statusGroupOptions' => $statusGroupOptions,
|
||||||
|
'table' => $table,
|
||||||
|
'debugEnabled' => $debugEnabled,
|
||||||
|
'diagnostics' => $diagnostics,
|
||||||
|
'debugMeta' => [
|
||||||
|
'selected_channels' => $selectedChannels,
|
||||||
|
'selected_status_groups' => $selectedStatusGroups,
|
||||||
|
'status_codes' => $statusCodes,
|
||||||
|
],
|
||||||
|
], 'layouts/app');
|
||||||
|
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0:string,1:string}
|
||||||
|
*/
|
||||||
|
private function resolveDateRange(Request $request): array
|
||||||
|
{
|
||||||
|
$now = new DateTimeImmutable('now');
|
||||||
|
$defaultFrom = $now->modify('first day of this month')->format('Y-m-d');
|
||||||
|
$defaultTo = $now->modify('last day of this month')->format('Y-m-d');
|
||||||
|
|
||||||
|
$dateFrom = trim((string) $request->input('date_from', $defaultFrom));
|
||||||
|
$dateTo = trim((string) $request->input('date_to', $defaultTo));
|
||||||
|
|
||||||
|
if (!$this->isValidDate($dateFrom)) {
|
||||||
|
$dateFrom = $defaultFrom;
|
||||||
|
}
|
||||||
|
if (!$this->isValidDate($dateTo)) {
|
||||||
|
$dateTo = $defaultTo;
|
||||||
|
}
|
||||||
|
if ($dateFrom > $dateTo) {
|
||||||
|
[$dateFrom, $dateTo] = [$dateTo, $dateFrom];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$dateFrom, $dateTo];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidDate(string $date): bool
|
||||||
|
{
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeImmutable::createFromFormat('Y-m-d', $date) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{id:int,name:string}> $groups
|
||||||
|
* @return array<int, array{id:int,name:string}>
|
||||||
|
*/
|
||||||
|
private function mapStatusGroupOptions(array $groups): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$groupId = (int) ($group['id'] ?? 0);
|
||||||
|
if ($groupId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$options[] = [
|
||||||
|
'id' => $groupId,
|
||||||
|
'name' => trim((string) ($group['name'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{id:int,name:string}> $statusGroupOptions
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
private function resolveSelectedStatusGroups(Request $request, array $statusGroupOptions): array
|
||||||
|
{
|
||||||
|
$allowed = [];
|
||||||
|
foreach ($statusGroupOptions as $option) {
|
||||||
|
$allowed[] = (int) $option['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allInput = $request->all();
|
||||||
|
$hasStatusGroupsParam = array_key_exists('status_groups', $allInput);
|
||||||
|
$selected = $this->toIntegerList($request->input('status_groups', []));
|
||||||
|
if (!$hasStatusGroupsParam) {
|
||||||
|
$selected = $this->defaultStatusGroupIds($statusGroupOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = array_values(array_intersect($selected, $allowed));
|
||||||
|
if ($selected !== []) {
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hasStatusGroupsParam ? [] : $this->defaultStatusGroupIds($statusGroupOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{id:int,name:string}> $statusGroupOptions
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
private function defaultStatusGroupIds(array $statusGroupOptions): array
|
||||||
|
{
|
||||||
|
$selected = [];
|
||||||
|
foreach ($statusGroupOptions as $group) {
|
||||||
|
$name = trim((string) ($group['name'] ?? ''));
|
||||||
|
if ($this->isCancelledGroup($name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$selected[] = (int) ($group['id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter($selected, static fn (int $id): bool => $id > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isCancelledGroup(string $name): bool
|
||||||
|
{
|
||||||
|
$normalized = strtr(mb_strtolower(trim($name)), [
|
||||||
|
'ą' => 'a',
|
||||||
|
'ć' => 'c',
|
||||||
|
'ę' => 'e',
|
||||||
|
'ł' => 'l',
|
||||||
|
'ń' => 'n',
|
||||||
|
'ó' => 'o',
|
||||||
|
'ś' => 's',
|
||||||
|
'ż' => 'z',
|
||||||
|
'ź' => 'z',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return in_array($normalized, ['anulowane', 'anulowany', 'cancelled', 'canceled'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{key:string,label:string}> $channels
|
||||||
|
* @return array<int, array{key:string,label:string}>
|
||||||
|
*/
|
||||||
|
private function mapChannelOptions(array $channels): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$key = trim((string) ($channel['key'] ?? ''));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$options[] = [
|
||||||
|
'key' => $key,
|
||||||
|
'label' => trim((string) ($channel['label'] ?? $key)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{key:string,label:string}> $channelOptions
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function resolveSelectedChannels(Request $request, array $channelOptions): array
|
||||||
|
{
|
||||||
|
$allowed = [];
|
||||||
|
foreach ($channelOptions as $option) {
|
||||||
|
$allowed[] = (string) $option['key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$allInput = $request->all();
|
||||||
|
$hasChannelsParam = array_key_exists('channels', $allInput);
|
||||||
|
$selected = $this->toStringList($request->input('channels', []));
|
||||||
|
if (!$hasChannelsParam) {
|
||||||
|
return $allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_intersect($selected, $allowed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $selectedChannels
|
||||||
|
* @param array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}> $aggregated
|
||||||
|
* @return array{rows:array<int,array<string,mixed>>,totals:array<string,mixed>,hasData:bool}
|
||||||
|
*/
|
||||||
|
private function buildTable(string $dateFrom, string $dateTo, array $selectedChannels, array $aggregated): array
|
||||||
|
{
|
||||||
|
$rows = [];
|
||||||
|
foreach ($this->dateRange($dateFrom, $dateTo) as $day) {
|
||||||
|
$rows[$day] = [
|
||||||
|
'day' => $day,
|
||||||
|
'channels' => $this->emptyChannelsRow($selectedChannels),
|
||||||
|
'day_total_orders' => 0,
|
||||||
|
'day_total_net' => 0.0,
|
||||||
|
'day_total_gross' => 0.0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($aggregated as $item) {
|
||||||
|
$day = (string) ($item['day'] ?? '');
|
||||||
|
$channelKey = (string) ($item['channel_key'] ?? '');
|
||||||
|
if ($day === '' || $channelKey === '' || !isset($rows[$day]['channels'][$channelKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordersCount = (int) ($item['orders_count'] ?? 0);
|
||||||
|
$totalNet = (float) ($item['total_net'] ?? 0);
|
||||||
|
$totalGross = (float) ($item['total_gross'] ?? 0);
|
||||||
|
|
||||||
|
$rows[$day]['channels'][$channelKey] = [
|
||||||
|
'orders_count' => $ordersCount,
|
||||||
|
'total_net' => $totalNet,
|
||||||
|
'total_gross' => $totalGross,
|
||||||
|
];
|
||||||
|
$rows[$day]['day_total_orders'] += $ordersCount;
|
||||||
|
$rows[$day]['day_total_net'] += $totalNet;
|
||||||
|
$rows[$day]['day_total_gross'] += $totalGross;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totals = [
|
||||||
|
'channels' => $this->emptyChannelsRow($selectedChannels),
|
||||||
|
'orders_count' => 0,
|
||||||
|
'total_net' => 0.0,
|
||||||
|
'total_gross' => 0.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasData = false;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
foreach ($selectedChannels as $channelKey) {
|
||||||
|
$channelStats = $row['channels'][$channelKey];
|
||||||
|
$totals['channels'][$channelKey]['orders_count'] += (int) ($channelStats['orders_count'] ?? 0);
|
||||||
|
$totals['channels'][$channelKey]['total_net'] += (float) ($channelStats['total_net'] ?? 0);
|
||||||
|
$totals['channels'][$channelKey]['total_gross'] += (float) ($channelStats['total_gross'] ?? 0);
|
||||||
|
}
|
||||||
|
$totals['orders_count'] += (int) ($row['day_total_orders'] ?? 0);
|
||||||
|
$totals['total_net'] += (float) ($row['day_total_net'] ?? 0);
|
||||||
|
$totals['total_gross'] += (float) ($row['day_total_gross'] ?? 0);
|
||||||
|
if ((int) ($row['day_total_orders'] ?? 0) > 0) {
|
||||||
|
$hasData = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'rows' => array_values($rows),
|
||||||
|
'totals' => $totals,
|
||||||
|
'hasData' => $hasData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $channelKeys
|
||||||
|
* @return array<string, array{orders_count:int,total_net:float,total_gross:float}>
|
||||||
|
*/
|
||||||
|
private function emptyChannelsRow(array $channelKeys): array
|
||||||
|
{
|
||||||
|
$row = [];
|
||||||
|
foreach ($channelKeys as $channelKey) {
|
||||||
|
$row[$channelKey] = [
|
||||||
|
'orders_count' => 0,
|
||||||
|
'total_net' => 0.0,
|
||||||
|
'total_gross' => 0.0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function dateRange(string $dateFrom, string $dateTo): array
|
||||||
|
{
|
||||||
|
$start = new DateTimeImmutable($dateFrom);
|
||||||
|
$end = (new DateTimeImmutable($dateTo))->add(new DateInterval('P1D'));
|
||||||
|
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
|
||||||
|
|
||||||
|
$dates = [];
|
||||||
|
foreach ($period as $date) {
|
||||||
|
$dates[] = $date->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function toStringList(mixed $value): array
|
||||||
|
{
|
||||||
|
$items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
|
||||||
|
$list = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$normalized = trim((string) $item);
|
||||||
|
if ($normalized !== '') {
|
||||||
|
$list[] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
private function toIntegerList(mixed $value): array
|
||||||
|
{
|
||||||
|
$items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
|
||||||
|
$list = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$number = (int) $item;
|
||||||
|
if ($number > 0) {
|
||||||
|
$list[] = $number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($list));
|
||||||
|
}
|
||||||
|
}
|
||||||
590
src/Modules/Statistics/OrdersStatisticsRepository.php
Normal file
590
src/Modules/Statistics/OrdersStatisticsRepository.php
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Statistics;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class OrdersStatisticsRepository
|
||||||
|
{
|
||||||
|
private static ?bool $hasOrdersTotalWithoutTax = null;
|
||||||
|
private static ?bool $hasOrdersTotalNet = null;
|
||||||
|
private static ?bool $hasOrdersTotalWithTax = null;
|
||||||
|
private static ?bool $hasOrdersTotalGross = null;
|
||||||
|
private static ?bool $hasOrdersIntegrationId = null;
|
||||||
|
private static ?bool $hasOrdersStatusCode = null;
|
||||||
|
private static ?bool $hasOrdersExternalStatusId = null;
|
||||||
|
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{id:int,name:string}>
|
||||||
|
*/
|
||||||
|
public function listStatusGroups(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->query(
|
||||||
|
'SELECT id, name
|
||||||
|
FROM order_status_groups
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY sort_order ASC, id ASC'
|
||||||
|
);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$groupId = (int) ($row['id'] ?? 0);
|
||||||
|
if ($groupId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[] = [
|
||||||
|
'id' => $groupId,
|
||||||
|
'name' => trim((string) ($row['name'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{key:string,label:string}>
|
||||||
|
*/
|
||||||
|
public function listChannelOptions(): array
|
||||||
|
{
|
||||||
|
$hasIntegrationId = $this->hasOrdersColumn('integration_id');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($hasIntegrationId) {
|
||||||
|
$rows = $this->pdo->query(
|
||||||
|
'SELECT DISTINCT COALESCE(o.integration_id, 0) AS integration_id
|
||||||
|
FROM orders o
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
|
||||||
|
ORDER BY integration_id ASC'
|
||||||
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} else {
|
||||||
|
$rows = $this->pdo->query(
|
||||||
|
'SELECT 0 AS integration_id
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM orders o
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
|
||||||
|
)'
|
||||||
|
)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
$rows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$channels = [
|
||||||
|
['key' => 'allegro', 'label' => 'Allegro'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$integrationId = (int) ($row['integration_id'] ?? 0);
|
||||||
|
$channels[] = [
|
||||||
|
'key' => 'shoppro:' . $integrationId,
|
||||||
|
'label' => $this->shopproChannelLabel($integrationId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, int> $groupIds
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function statusCodesByGroupIds(array $groupIds): array
|
||||||
|
{
|
||||||
|
$groupIds = array_values(array_unique(array_filter($groupIds, static fn (int $id): bool => $id > 0)));
|
||||||
|
if ($groupIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$inSql, $params] = $this->buildIntegerInClause('gid', $groupIds);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT code
|
||||||
|
FROM order_statuses
|
||||||
|
WHERE is_active = 1
|
||||||
|
AND group_id IN (' . $inSql . ')
|
||||||
|
ORDER BY sort_order ASC, id ASC'
|
||||||
|
);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$codes = [];
|
||||||
|
foreach ($rows as $code) {
|
||||||
|
$normalized = strtolower(trim((string) $code));
|
||||||
|
if ($normalized !== '') {
|
||||||
|
$codes[] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($codes));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $channels
|
||||||
|
* @param array<int, string> $statusCodes
|
||||||
|
* @return array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}>
|
||||||
|
*/
|
||||||
|
public function aggregateByDay(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
|
||||||
|
{
|
||||||
|
$channels = array_values(array_unique(array_filter(
|
||||||
|
array_map(static fn (string $item): string => trim($item), $channels),
|
||||||
|
static fn (string $item): bool => $item !== ''
|
||||||
|
)));
|
||||||
|
$statusCodes = array_values(array_unique(array_filter(
|
||||||
|
array_map(static fn (string $item): string => strtolower(trim($item)), $statusCodes),
|
||||||
|
static fn (string $item): bool => $item !== ''
|
||||||
|
)));
|
||||||
|
|
||||||
|
if ($channels === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||||
|
$effectiveDateSql = $this->effectiveDateSql('o');
|
||||||
|
$channelSql = $this->channelSql('o');
|
||||||
|
$netAmountSql = $this->netAmountSql('o');
|
||||||
|
$grossAmountSql = $this->grossAmountSql('o');
|
||||||
|
$rawStatusSql = $this->rawStatusSql('o');
|
||||||
|
|
||||||
|
[$channelInSql, $channelParams] = $this->buildStringInClause('ch', $channels);
|
||||||
|
$params = array_merge($channelParams, [
|
||||||
|
'date_from' => $dateFrom . ' 00:00:00',
|
||||||
|
'date_to' => $dateTo . ' 23:59:59',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$statusFilterSql = '';
|
||||||
|
if ($statusCodes !== []) {
|
||||||
|
[$statusInSql, $statusParams] = $this->buildStringInClause('st', $statusCodes);
|
||||||
|
$statusFilterSql = ' AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')';
|
||||||
|
$params = array_merge($params, $statusParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'SELECT
|
||||||
|
DATE(' . $effectiveDateSql . ') AS day,
|
||||||
|
' . $channelSql . ' AS channel_key,
|
||||||
|
COUNT(*) AS orders_count,
|
||||||
|
SUM(' . $netAmountSql . ') AS total_net,
|
||||||
|
SUM(' . $grossAmountSql . ') AS total_gross
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN allegro_order_status_mappings asm
|
||||||
|
ON o.source = "allegro"
|
||||||
|
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||||
|
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||||
|
AND ' . $effectiveDateSql . ' >= :date_from
|
||||||
|
AND ' . $effectiveDateSql . ' <= :date_to
|
||||||
|
AND ' . $channelSql . ' IN (' . $channelInSql . ')
|
||||||
|
' . $statusFilterSql . '
|
||||||
|
GROUP BY DATE(' . $effectiveDateSql . '), ' . $channelSql . '
|
||||||
|
ORDER BY DATE(' . $effectiveDateSql . ') ASC';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$day = trim((string) ($row['day'] ?? ''));
|
||||||
|
$channelKey = trim((string) ($row['channel_key'] ?? ''));
|
||||||
|
if ($day === '' || $channelKey === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'day' => $day,
|
||||||
|
'channel_key' => $channelKey,
|
||||||
|
'orders_count' => (int) ($row['orders_count'] ?? 0),
|
||||||
|
'total_net' => (float) ($row['total_net'] ?? 0),
|
||||||
|
'total_gross' => (float) ($row['total_gross'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $channels
|
||||||
|
* @param array<int, string> $statusCodes
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function diagnostics(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
|
||||||
|
{
|
||||||
|
$effectiveDateSql = $this->effectiveDateSql('o');
|
||||||
|
$channelSql = $this->channelSql('o');
|
||||||
|
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||||
|
$rawStatusSql = $this->rawStatusSql('o');
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'columns' => [
|
||||||
|
'integration_id' => $this->hasOrdersColumn('integration_id'),
|
||||||
|
'total_with_tax' => $this->hasOrdersColumn('total_with_tax'),
|
||||||
|
'total_gross' => $this->hasOrdersColumn('total_gross'),
|
||||||
|
'total_without_tax' => $this->hasOrdersColumn('total_without_tax'),
|
||||||
|
'total_net' => $this->hasOrdersColumn('total_net'),
|
||||||
|
'status_code' => $this->hasOrdersColumn('status_code'),
|
||||||
|
'external_status_id' => $this->hasOrdersColumn('external_status_id'),
|
||||||
|
],
|
||||||
|
'counts' => [],
|
||||||
|
'errors' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$data['counts']['in_date_and_source'] = $this->safeCount(
|
||||||
|
'SELECT COUNT(*)
|
||||||
|
FROM orders o
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||||
|
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||||
|
AND ' . $effectiveDateSql . ' >= :date_from
|
||||||
|
AND ' . $effectiveDateSql . ' <= :date_to',
|
||||||
|
[
|
||||||
|
'date_from' => $dateFrom . ' 00:00:00',
|
||||||
|
'date_to' => $dateTo . ' 23:59:59',
|
||||||
|
],
|
||||||
|
$data['errors'],
|
||||||
|
'in_date_and_source'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($channels !== []) {
|
||||||
|
[$channelInSql, $channelParams] = $this->buildStringInClause('dbg_ch', $channels);
|
||||||
|
$data['counts']['after_channel_filter'] = $this->safeCount(
|
||||||
|
'SELECT COUNT(*)
|
||||||
|
FROM orders o
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||||
|
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||||
|
AND ' . $effectiveDateSql . ' >= :date_from
|
||||||
|
AND ' . $effectiveDateSql . ' <= :date_to
|
||||||
|
AND ' . $channelSql . ' IN (' . $channelInSql . ')',
|
||||||
|
array_merge(
|
||||||
|
[
|
||||||
|
'date_from' => $dateFrom . ' 00:00:00',
|
||||||
|
'date_to' => $dateTo . ' 23:59:59',
|
||||||
|
],
|
||||||
|
$channelParams
|
||||||
|
),
|
||||||
|
$data['errors'],
|
||||||
|
'after_channel_filter'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$data['counts']['after_channel_filter'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($statusCodes !== []) {
|
||||||
|
[$statusInSql, $statusParams] = $this->buildStringInClause('dbg_st', $statusCodes);
|
||||||
|
$data['counts']['after_status_filter'] = $this->safeCount(
|
||||||
|
'SELECT COUNT(*)
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN allegro_order_status_mappings asm
|
||||||
|
ON o.source = "allegro"
|
||||||
|
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
|
||||||
|
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
|
||||||
|
AND ' . $effectiveDateSql . ' IS NOT NULL
|
||||||
|
AND ' . $effectiveDateSql . ' >= :date_from
|
||||||
|
AND ' . $effectiveDateSql . ' <= :date_to
|
||||||
|
AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')',
|
||||||
|
array_merge(
|
||||||
|
[
|
||||||
|
'date_from' => $dateFrom . ' 00:00:00',
|
||||||
|
'date_to' => $dateTo . ' 23:59:59',
|
||||||
|
],
|
||||||
|
$statusParams
|
||||||
|
),
|
||||||
|
$data['errors'],
|
||||||
|
'after_status_filter'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$data['counts']['after_status_filter'] = $data['counts']['after_channel_filter'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, int> $values
|
||||||
|
* @return array{0:string,1:array<string,int>}
|
||||||
|
*/
|
||||||
|
private function buildIntegerInClause(string $prefix, array $values): array
|
||||||
|
{
|
||||||
|
$placeholders = [];
|
||||||
|
$params = [];
|
||||||
|
foreach (array_values($values) as $index => $value) {
|
||||||
|
$key = $prefix . '_' . $index;
|
||||||
|
$placeholders[] = ':' . $key;
|
||||||
|
$params[$key] = (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [implode(', ', $placeholders), $params];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $values
|
||||||
|
* @return array{0:string,1:array<string,string>}
|
||||||
|
*/
|
||||||
|
private function buildStringInClause(string $prefix, array $values): array
|
||||||
|
{
|
||||||
|
$placeholders = [];
|
||||||
|
$params = [];
|
||||||
|
foreach (array_values($values) as $index => $value) {
|
||||||
|
$key = $prefix . '_' . $index;
|
||||||
|
$placeholders[] = ':' . $key;
|
||||||
|
$params[$key] = (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [implode(', ', $placeholders), $params];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $params
|
||||||
|
* @param array<int, string> $errors
|
||||||
|
*/
|
||||||
|
private function safeCount(string $sql, array $params, array &$errors, string $label): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return (int) $stmt->fetchColumn();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$errors[] = $label . ': ' . $exception->getMessage();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
|
||||||
|
{
|
||||||
|
$rawStatusSql = $this->rawStatusSql($orderAlias);
|
||||||
|
|
||||||
|
return 'LOWER(
|
||||||
|
CASE
|
||||||
|
WHEN ' . $orderAlias . '.source = "allegro"
|
||||||
|
AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
|
||||||
|
AND ' . $mappingAlias . '.orderpro_status_code <> ""
|
||||||
|
THEN ' . $mappingAlias . '.orderpro_status_code
|
||||||
|
ELSE ' . $rawStatusSql . '
|
||||||
|
END
|
||||||
|
)';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function effectiveDateSql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
return 'COALESCE(
|
||||||
|
' . $orderAlias . '.ordered_at,
|
||||||
|
' . $orderAlias . '.source_created_at,
|
||||||
|
' . $orderAlias . '.source_updated_at,
|
||||||
|
' . $orderAlias . '.fetched_at
|
||||||
|
)';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function channelSql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
if ($this->hasOrdersColumn('integration_id')) {
|
||||||
|
return '(CASE
|
||||||
|
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
|
||||||
|
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN CONCAT("shoppro:", COALESCE(CAST(' . $orderAlias . '.integration_id AS CHAR) COLLATE utf8mb4_unicode_ci, "0"))
|
||||||
|
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
|
||||||
|
END) COLLATE utf8mb4_unicode_ci';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '(CASE
|
||||||
|
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
|
||||||
|
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN "shoppro:0"
|
||||||
|
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
|
||||||
|
END) COLLATE utf8mb4_unicode_ci';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shopproChannelLabel(int $integrationId): string
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return 'shopPRO (brak integracji)';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT name
|
||||||
|
FROM integrations
|
||||||
|
WHERE id = :id
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$stmt->execute(['id' => $integrationId]);
|
||||||
|
$name = $stmt->fetchColumn();
|
||||||
|
} catch (Throwable) {
|
||||||
|
$name = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($name) && trim($name) !== '') {
|
||||||
|
return trim($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'shopPRO #' . $integrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function netAmountSql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
$netColumn = null;
|
||||||
|
if ($this->hasOrdersColumn('total_without_tax')) {
|
||||||
|
$netColumn = $orderAlias . '.total_without_tax';
|
||||||
|
} elseif ($this->hasOrdersColumn('total_net')) {
|
||||||
|
$netColumn = $orderAlias . '.total_net';
|
||||||
|
}
|
||||||
|
|
||||||
|
$grossColumn = null;
|
||||||
|
if ($this->hasOrdersColumn('total_with_tax')) {
|
||||||
|
$grossColumn = $orderAlias . '.total_with_tax';
|
||||||
|
} elseif ($this->hasOrdersColumn('total_gross')) {
|
||||||
|
$grossColumn = $orderAlias . '.total_gross';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: gdy netto z zrodla jest puste (shopPRO nie wysyla netto), wyliczamy z brutto/1.23.
|
||||||
|
// TODO(STAT-NET): docelowo pobierac netto z shopPRO na poziomie zamowienia lub liczyc z order_items po faktycznym tax_rate.
|
||||||
|
if ($netColumn !== null && $grossColumn !== null) {
|
||||||
|
return 'CASE
|
||||||
|
WHEN ' . $netColumn . ' IS NOT NULL AND ' . $netColumn . ' > 0 THEN ' . $netColumn . '
|
||||||
|
WHEN ' . $grossColumn . ' IS NOT NULL AND ' . $grossColumn . ' > 0 THEN ROUND(' . $grossColumn . ' / 1.23, 2)
|
||||||
|
ELSE 0
|
||||||
|
END';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($netColumn !== null) {
|
||||||
|
return 'COALESCE(' . $netColumn . ', 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($grossColumn !== null) {
|
||||||
|
return 'ROUND(COALESCE(' . $grossColumn . ', 0) / 1.23, 2)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function grossAmountSql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
if ($this->hasOrdersColumn('total_with_tax')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.total_with_tax, 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasOrdersColumn('total_gross')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.total_gross, 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasOrdersColumn('total_without_tax')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.total_without_tax, 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasOrdersColumn('total_net')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.total_net, 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function rawStatusSql(string $orderAlias): string
|
||||||
|
{
|
||||||
|
if ($this->hasOrdersColumn('status_code')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.status_code, "")';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasOrdersColumn('external_status_id')) {
|
||||||
|
return 'COALESCE(' . $orderAlias . '.external_status_id, "")';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasOrdersColumn(string $column): bool
|
||||||
|
{
|
||||||
|
if ($column === 'integration_id' && self::$hasOrdersIntegrationId !== null) {
|
||||||
|
return self::$hasOrdersIntegrationId;
|
||||||
|
}
|
||||||
|
if ($column === 'total_without_tax' && self::$hasOrdersTotalWithoutTax !== null) {
|
||||||
|
return self::$hasOrdersTotalWithoutTax;
|
||||||
|
}
|
||||||
|
if ($column === 'total_net' && self::$hasOrdersTotalNet !== null) {
|
||||||
|
return self::$hasOrdersTotalNet;
|
||||||
|
}
|
||||||
|
if ($column === 'total_with_tax' && self::$hasOrdersTotalWithTax !== null) {
|
||||||
|
return self::$hasOrdersTotalWithTax;
|
||||||
|
}
|
||||||
|
if ($column === 'total_gross' && self::$hasOrdersTotalGross !== null) {
|
||||||
|
return self::$hasOrdersTotalGross;
|
||||||
|
}
|
||||||
|
if ($column === 'status_code' && self::$hasOrdersStatusCode !== null) {
|
||||||
|
return self::$hasOrdersStatusCode;
|
||||||
|
}
|
||||||
|
if ($column === 'external_status_id' && self::$hasOrdersExternalStatusId !== null) {
|
||||||
|
return self::$hasOrdersExternalStatusId;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare(
|
||||||
|
'SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = :table_name
|
||||||
|
AND COLUMN_NAME = :column_name'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'table_name' => 'orders',
|
||||||
|
'column_name' => $column,
|
||||||
|
]);
|
||||||
|
$exists = ((int) $stmt->fetchColumn()) > 0;
|
||||||
|
} catch (Throwable) {
|
||||||
|
$exists = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($column === 'total_without_tax') {
|
||||||
|
self::$hasOrdersTotalWithoutTax = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'total_net') {
|
||||||
|
self::$hasOrdersTotalNet = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'integration_id') {
|
||||||
|
self::$hasOrdersIntegrationId = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'total_with_tax') {
|
||||||
|
self::$hasOrdersTotalWithTax = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'total_gross') {
|
||||||
|
self::$hasOrdersTotalGross = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'status_code') {
|
||||||
|
self::$hasOrdersStatusCode = $exists;
|
||||||
|
}
|
||||||
|
if ($column === 'external_status_id') {
|
||||||
|
self::$hasOrdersExternalStatusId = $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $exists;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user