This commit is contained in:
2026-04-19 22:43:02 +02:00
parent 10cba24727
commit fd1e23eb26
23 changed files with 2320 additions and 72 deletions

View 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>

View 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*