feat(110): statistics summary
Phase 110 complete: - add Statistics -> Podsumowanie page - add monthly order count and value charts per integration plus total - use Chart.js with table fallback and 04-2026 default history start - update PAUL and DOCS technical documentation
This commit is contained in:
@@ -12,8 +12,8 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 3.3.0 |
|
||||
| Status | v3.3 shipped - UI Filters complete |
|
||||
| Version | 3.4.0 |
|
||||
| Status | v3.4 shipped - Statistics Summary complete |
|
||||
| Last Updated | 2026-04-28 |
|
||||
|
||||
## Requirements
|
||||
@@ -113,6 +113,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Idempotentna jednorazowa wysylka e-mail per zamowienie: tabela deduplikacji `automation_email_once_deliveries` (UNIQUE KEY rule_id+action_id+order_id), checkbox "Wyslij tylko raz" w konfiguracji akcji, markSent() tylko po sukcesie — Phase 107
|
||||
- [x] Delivery Status Management: tabela `delivery_statuses` z CRUD panelem `/settings/delivery-statuses`, `DeliveryStatus::setRepository()` z DB fallbackiem, integracja DB-driven w dropdownach automatyzacji (warunek shipment_status + akcja update_shipment_status), osobna podstrona formularza CRUD (BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji) — Phase 108
|
||||
- [x] Checkbox dropdown multi-select filters: `/statistics/orders` korzysta z progresywnie ulepszanych selectow multiple z checkboxami, opcja "Wszystkie" i zachowanym kontraktem GET — Phase 109
|
||||
- [x] Podsumowanie statystyk: `Statystyki -> Podsumowanie` z miesiecznymi wykresami liczby i wartosci zamowien per integracja plus `Razem`, Chart.js i fallback tabelaryczny — Phase 110
|
||||
|
||||
### Deferred
|
||||
|
||||
@@ -121,7 +122,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
- [ ] (brak — v3.3 zakonczony, oczekiwanie na kolejny milestone)
|
||||
- [ ] (brak — v3.4 zakonczony, oczekiwanie na kolejny milestone)
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
@@ -198,6 +199,7 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
||||
| DeliveryStatus::setRepository() pattern: DB fallback dla static final class | Operator dodaje status w UI bez zmian kodu; `getAllOptions()`/`label()`/`getColor()` ladują z DB gdy repo ustawione, fallback na hardcoded ALL_STATUSES/LABEL_PL | 2026-04-27 | Active |
|
||||
| Drop backward compat dla starych grupowych kluczy automatyzacji (Phase 108-02) | Kolizja semantyczna: stary `picked_up` mapował na `delivered`, nowy klucz DB `picked_up` to "Odebrana przez kuriera" — odwrotne końce cyklu. Hybrid evaluation by silently dawała wrong matches | 2026-04-27 | Active |
|
||||
| Path params w controllerach via `$request->input('id')` (nie jako argumenty metody) | Konwencja routera projektu: handler wywoływany z jednym argumentem `$request`, params siedzą jako attributes — `ReceiptController::show()` jako wzorzec | 2026-04-27 | Active |
|
||||
| Statistics Summary Chart.js CDN + start `2026-04-01` | Interaktywne wykresy bez zmiany build pipeline; historia podsumowania ma zaczynac sie od `04-2026` mimo starszych danych | 2026-04-28 | Active |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
@@ -229,6 +231,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-04-28 after v3.3 UI Filters milestone completion (Phase 109)*
|
||||
*Last updated: 2026-04-28 after v3.4 Statistics Summary milestone completion (Phase 110)*
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
|
||||
|
||||
## Current Milestone
|
||||
|
||||
Brak aktywnego milestone - v3.3 zamkniety. Nastepny milestone do zaplanowania.
|
||||
Brak aktywnego milestone - v3.4 zamkniety. Nastepny milestone do zaplanowania.
|
||||
|
||||
## Next Milestone
|
||||
|
||||
@@ -19,6 +19,19 @@ Kandydaci w kolejce:
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v3.4 Statistics Summary - 2026-04-28 (1 phase, 1 plan)</summary>
|
||||
|
||||
Dodano pierwsza pozycje `Statystyki -> Podsumowanie` z miesiecznymi wykresami liczby i wartosci zamowien. Kazda integracja ma osobna serie, a dodatkowa seria `Razem` sumuje miesiac. Domyslny start historii to `04-2026`.
|
||||
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 110 | Statistics Summary | 1/1 | Complete |
|
||||
|
||||
Archive: `.paul/phases/110-statistics-summary/`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v3.3 UI Filters - 2026-04-28 (1 phase, 1 plan)</summary>
|
||||
|
||||
@@ -441,4 +454,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-04-27 - v3.2 Delivery Status Management milestone closed (Phase 108, 2 plans)*
|
||||
*Last updated: 2026-04-28 - v3.4 Statistics Summary milestone closed (Phase 110)*
|
||||
|
||||
@@ -5,42 +5,42 @@
|
||||
See: .paul/PROJECT.md (updated 2026-04-28)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** Brak aktywnego milestone - v3.3 zamkniety
|
||||
**Current focus:** Brak aktywnego milestone - v3.4 zamkniety
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.3 - COMPLETE (UI Filters)
|
||||
Phase: 109 of 109 - COMPLETE
|
||||
Plan: 109-01 - COMPLETE
|
||||
Version: 3.3.0
|
||||
Status: v3.3 shipped - gotowy do nastepnego milestone
|
||||
Milestone: v3.4 Statistics Summary - COMPLETE
|
||||
Phase: 110 of 110 - COMPLETE
|
||||
Plan: 110-01 - COMPLETE
|
||||
Version: 3.4.0
|
||||
Status: v3.4 shipped - gotowy do nastepnego milestone
|
||||
|
||||
Last activity: 2026-04-28 - UNIFY Phase 109 / v3.3 milestone complete
|
||||
Last activity: 2026-04-28 - UNIFY Phase 110 / v3.4 milestone complete
|
||||
|
||||
Progress:
|
||||
- Milestone v3.3: [##########] 100% (1/1 phases, 1/1 plans)
|
||||
- Milestone v3.4: [##########] 100% (1/1 phases, 1/1 plans)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
v3.3 milestone:
|
||||
Phase 109 (Checkbox Multiselect Filters):
|
||||
Plan 109-01: PLAN done APPLY done UNIFY done
|
||||
-> Phase 109 closed
|
||||
-> v3.3 milestone closed
|
||||
v3.4 milestone:
|
||||
Phase 110 (Statistics Summary):
|
||||
Plan 110-01: PLAN done APPLY done UNIFY done
|
||||
-> Phase 110 closed
|
||||
-> v3.4 milestone closed
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-28
|
||||
Stopped at: v3.3 milestone closed
|
||||
Stopped at: v3.4 milestone closed
|
||||
Next action: /paul:milestone - wybor i zaplanowanie nastepnego milestone
|
||||
Resume file: .paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md
|
||||
Resume file: .paul/phases/110-statistics-summary/110-01-SUMMARY.md
|
||||
|
||||
## Git State
|
||||
|
||||
Last commit: feat(109): checkbox multiselect filters
|
||||
Last commit: feat(110): statistics summary
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
@@ -61,4 +61,16 @@ Feature branches merged: none
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| sonar-scanner (required) | o | Wymagany po APPLY 108-01 i 108-02 — odlozony |
|
||||
| sonar-scanner (required) | o | Wymagany po APPLY 108-01 i 108-02 - odlozony |
|
||||
|
||||
## Skill Audit (Phase 110)
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| sonar-scanner (required) | yes | Skan uruchomiony po APPLY; raport wyslany do SonarQube. |
|
||||
|
||||
## Phase 110 Notes
|
||||
|
||||
- Local HTTP verification blocked by MySQL/XAMPP connection refused.
|
||||
- PHPUnit not run: `composer` unavailable in PATH and `vendor/` absent.
|
||||
- Sonar issue import to `DOCS/todo.md` not performed because SonarQube MCP/resources are unavailable in this session.
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 110, Plan 01] Dodano `Statystyki -> Podsumowanie` z miesiecznymi wykresami liczby i wartosci zamowien.
|
||||
- Wykresy korzystaja z Chart.js 4.4.8 CDN, maja serie per integracja oraz linie `Razem`.
|
||||
- Ustawiono domyslny poczatek historii na `2026-04-01` (`04-2026`) mimo starszych danych.
|
||||
- Na desktopie wykresy sa obok siebie, a pod nimi dwie tabele fallback rowniez obok siebie.
|
||||
- [Phase 109, Plan 01] Wdrozono checkbox dropdown multi-select filters na `/statistics/orders`.
|
||||
- Zachowano kontrakt GET `channels[]` i `status_groups[]` przez synchronizacje z natywnym `<select multiple>`.
|
||||
- Zaktualizowano `paul:plan`, aby korzystala z `.paul/codebase/architecture.md` i `.paul/codebase/db_schema.md`.
|
||||
@@ -13,11 +17,23 @@
|
||||
- `.paul/ROADMAP.md`
|
||||
- `.paul/STATE.md`
|
||||
- `.paul/codebase/architecture.md`
|
||||
- `.paul/codebase/db_schema.md`
|
||||
- `.paul/codebase/tech_changelog.md`
|
||||
- `.paul/phases/109-checkbox-multiselect-filters/109-01-PLAN.md`
|
||||
- `.paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md`
|
||||
- `.paul/phases/110-statistics-summary/110-01-PLAN.md`
|
||||
- `.paul/phases/110-statistics-summary/110-01-SUMMARY.md`
|
||||
- `DOCS/ARCHITECTURE.md`
|
||||
- `DOCS/DB_SCHEMA.md`
|
||||
- `DOCS/TECH_CHANGELOG.md`
|
||||
- `public/assets/css/app.css`
|
||||
- `public/assets/js/modules/checkbox-multiselect.js`
|
||||
- `public/assets/js/modules/statistics-summary-charts.js`
|
||||
- `resources/lang/pl.php`
|
||||
- `resources/scss/app.scss`
|
||||
- `resources/views/layouts/app.php`
|
||||
- `resources/views/statistics/orders.php`
|
||||
- `resources/views/statistics/summary.php`
|
||||
- `routes/web.php`
|
||||
- `src/Modules/Statistics/OrdersStatisticsController.php`
|
||||
- `src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||
|
||||
@@ -42,7 +42,7 @@ HTTP Request
|
||||
| **Settings** | 51+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Apaczka/InPost config, status mappings |
|
||||
| **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh |
|
||||
| **Printing** | 4 | `PrintApiController`, `PrintJobRepository`, `ApiKeyMiddleware` | REST API for Windows print client |
|
||||
| **Statistics** | 2 | `OrdersStatisticsController`, `OrdersStatisticsRepository` | Dashboard aggregates |
|
||||
| **Statistics** | 3 | `OrdersStatisticsController`, `OrdersStatisticsRepository`, `statistics-summary-charts.js` | Daily order statistics and monthly summary charts |
|
||||
| **Info** | 1 | `InfoController` | Health check |
|
||||
|
||||
## Frontend Enhancement Modules
|
||||
@@ -51,9 +51,16 @@ HTTP Request
|
||||
- Loaded globally from `resources/views/layouts/app.php`.
|
||||
- Enhances native `<select multiple data-checkbox-multiselect>` controls after `DOMContentLoaded`.
|
||||
- Keeps the original select in the form, synchronizes option `selected` state, and preserves native GET/POST names such as `channels[]` and `status_groups[]`.
|
||||
- Used by `/statistics/orders` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
|
||||
- Used by `/statistics/orders` and `/statistics/summary` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
|
||||
- Progressive enhancement: if JavaScript fails, the native multi-select remains visible.
|
||||
|
||||
### Statistics Summary Charts (`public/assets/js/modules/statistics-summary-charts.js`)
|
||||
- Loaded globally from `resources/views/layouts/app.php` after Chart.js 4.4.8 CDN; activates only when `#js-statistics-summary-data` exists.
|
||||
- Reads JSON produced by `OrdersStatisticsController::summary()` and renders two interactive Chart.js line charts on `/statistics/summary`.
|
||||
- Chart 1 displays monthly order counts per selected integration plus a `Razem` line.
|
||||
- Chart 2 displays monthly gross order values per selected integration plus a `Razem` line.
|
||||
- The PHP view keeps table fallbacks under both charts, so the data remains visible if JavaScript fails.
|
||||
|
||||
## Key Data Flows
|
||||
|
||||
### Order Lifecycle
|
||||
@@ -61,6 +68,13 @@ HTTP Request
|
||||
2. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
||||
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
|
||||
|
||||
### Statistics Summary
|
||||
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
||||
2. **Filters** — controller reuses statistics filter semantics: date range, `channels[]`, `status_groups[]`, default status groups excluding cancelled; default history starts at `2026-04-01`.
|
||||
3. **Aggregation** — `OrdersStatisticsRepository::aggregateByMonth()` groups existing `orders` rows by `YYYY-MM` and channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report.
|
||||
4. **View model** — controller builds per-integration series and total series for order count and gross value charts.
|
||||
5. **Render** — `resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
|
||||
|
||||
### Shipment Flow
|
||||
1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()`
|
||||
2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
|
||||
|
||||
@@ -850,3 +850,14 @@ Default keys: `cron_run_on_web`, `cron_web_limit`, `gs1_api_login`, `gs1_prefix`
|
||||
| Audit via JSON | `payload_json` snapshots in orders, shipments, receipts |
|
||||
| Migrations | `database/migrations/YYYYMMDD_NNNNNN_description.sql` |
|
||||
| Deferred indexes | `idx_order_addresses_order_type`, `idx_shipment_packages_order_delivery` — apply at >50k orders |
|
||||
|
||||
## Reporting Usage
|
||||
|
||||
**Statistics Summary (`/statistics/summary`)** — no dedicated reporting tables.
|
||||
- Reads existing `orders` rows and groups by month using the same effective order date used by `/statistics/orders`.
|
||||
- Default summary history starts at April 2026 (`2026-04-01`), even if older rows exist.
|
||||
- Splits series by channel key: Allegro as one series and each shopPRO integration by `orders.integration_id`.
|
||||
- Uses `integrations.name` only for display labels when available.
|
||||
- Filters by selected status groups through `order_status_groups` and `order_statuses`.
|
||||
- Uses existing gross amount columns via `OrdersStatisticsRepository::grossAmountSql()`.
|
||||
- No schema migration was introduced for Phase 110.
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
|
||||
|
||||
**Co zrobiono:**
|
||||
- `/statistics/summary` - nowy widok podsumowania w menu `Statystyki -> Podsumowanie`.
|
||||
- `OrdersStatisticsController::summary()` - buduje miesieczny view-model dla wykresow liczby i wartosci zamowien.
|
||||
- `OrdersStatisticsRepository::aggregateByMonth()` - agreguje istniejace zamowienia po miesiacu i kanale/integracji.
|
||||
- `public/assets/js/modules/statistics-summary-charts.js` - renderer dwoch interaktywnych wykresow liniowych oparty o Chart.js 4.4.8 CDN.
|
||||
- `resources/views/statistics/summary.php` - filtry zgodne z raportem dziennym, dwa wykresy obok siebie na desktopie oraz dwie tabele fallback pod nimi.
|
||||
- Domyslny poczatek historii ustawiony na `2026-04-01` (`04-2026`) mimo starszych danych.
|
||||
|
||||
**Dlaczego:**
|
||||
- Operator potrzebuje szybkiego trendu miesiecznego przed przejsciem do szczegolowych dziennych statystyk.
|
||||
- Wykresy uzywaja obecnych tabel `orders`, `integrations`, `order_status_groups` i `order_statuses`, wiec migracja DB nie jest potrzebna.
|
||||
- Seria `Razem` jest liczona z tych samych danych co serie integracji, co ulatwia sprawdzenie sum miesiecznych.
|
||||
|
||||
## 2026-04-28 - Phase 109 Plan 01: Checkbox Multiselect Filters
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
266
.paul/phases/110-statistics-summary/110-01-PLAN.md
Normal file
266
.paul/phases/110-statistics-summary/110-01-PLAN.md
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
phase: 110-statistics-summary
|
||||
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/summary.php
|
||||
- resources/lang/pl.php
|
||||
- public/assets/js/modules/statistics-summary-charts.js
|
||||
- resources/scss/app.scss
|
||||
- public/assets/css/app.css
|
||||
- DOCS/ARCHITECTURE.md
|
||||
- DOCS/DB_SCHEMA.md
|
||||
- DOCS/TECH_CHANGELOG.md
|
||||
- .paul/codebase/architecture.md
|
||||
- .paul/codebase/db_schema.md
|
||||
- .paul/codebase/tech_changelog.md
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Add a new first submenu item `Statystyki -> Podsumowanie` with two monthly line charts:
|
||||
- monthly order count per integration plus a total line,
|
||||
- monthly order value per integration plus a total line.
|
||||
|
||||
## Purpose
|
||||
The operator needs a quick trend view before opening detailed daily order statistics. The summary should show how each sales integration contributes month by month and how the total business volume changes over time.
|
||||
|
||||
## Output
|
||||
- New authenticated endpoint `/statistics/summary`.
|
||||
- Sidebar order: `Podsumowanie` first, existing `Zamowienia` second.
|
||||
- New summary view with compact filters and two charts.
|
||||
- Backend monthly aggregation using existing orders data, grouped by integration/channel.
|
||||
- Technical documentation updates. No database migration is expected.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
@AGENTS.md
|
||||
@.paul/codebase/architecture.md
|
||||
@.paul/codebase/db_schema.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/105-orders-statistics/105-01-SUMMARY.md
|
||||
@.paul/phases/109-checkbox-multiselect-filters/109-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Statistics/OrdersStatisticsController.php
|
||||
@src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||
@resources/views/statistics/orders.php
|
||||
@resources/views/layouts/app.php
|
||||
@routes/web.php
|
||||
@resources/lang/pl.php
|
||||
@resources/scss/app.scss
|
||||
@package.json
|
||||
|
||||
## Notes
|
||||
- `DOCS/DB_SCHEMA.md` and `DOCS/ARCHITECTURE.md` are required by `AGENTS.md`, but they are currently missing in the repo. APPLY should create them or reconcile them from `.paul/codebase/*` before documenting this feature.
|
||||
- Existing statistics code already has filter parsing patterns, channel labels, status group defaults, collation handling, and amount fallback logic. Reuse those rather than creating a separate query style.
|
||||
- `SPECIAL-FLOWS.md` requires `sonar-scanner` after APPLY and before UNIFY; frontend-design is optional for this UI work.
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | After APPLY, before UNIFY | o |
|
||||
| /frontend-design | optional | During chart UI implementation/review | o |
|
||||
|
||||
**BLOCKING:** `sonar-scanner` must be run before UNIFY unless the environment blocks it.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Sidebar Navigation
|
||||
```gherkin
|
||||
Given the user is authenticated
|
||||
When the sidebar statistics section is visible
|
||||
Then the first item under "Statystyki" is "Podsumowanie"
|
||||
And the second item remains "Zamowienia"
|
||||
And clicking "Podsumowanie" opens `/statistics/summary`
|
||||
And the active state highlights "Podsumowanie" on that page
|
||||
```
|
||||
|
||||
## AC-2: Monthly Order Count Chart
|
||||
```gherkin
|
||||
Given orders exist across multiple months and integrations
|
||||
When the user opens `/statistics/summary`
|
||||
Then the page shows an order count chart grouped by month
|
||||
And each integration has its own line
|
||||
And an additional "Razem" line sums all selected integrations per month
|
||||
```
|
||||
|
||||
## AC-3: Monthly Order Value Chart
|
||||
```gherkin
|
||||
Given orders exist across multiple months and integrations
|
||||
When the user opens `/statistics/summary`
|
||||
Then the page shows an order value chart grouped by month
|
||||
And each integration has its own line
|
||||
And an additional "Razem" line sums all selected integrations per month
|
||||
And values are displayed as money with two decimal places
|
||||
```
|
||||
|
||||
## AC-4: Existing Statistics Semantics Reused
|
||||
```gherkin
|
||||
Given the user changes the date range, channel selection, or status group selection
|
||||
When the summary page reloads
|
||||
Then both charts use the same selected filters
|
||||
And channel labels match existing statistics labels
|
||||
And default status groups still exclude the cancelled group
|
||||
```
|
||||
|
||||
## AC-5: Empty and Degraded States
|
||||
```gherkin
|
||||
Given the selected filters return no orders
|
||||
When the user opens `/statistics/summary`
|
||||
Then the page shows a clear empty state instead of broken charts
|
||||
|
||||
Given JavaScript fails to load
|
||||
When the user opens `/statistics/summary`
|
||||
Then the monthly data remains visible in an HTML table fallback
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add monthly statistics backend and route</name>
|
||||
<files>src/Modules/Statistics/OrdersStatisticsController.php, src/Modules/Statistics/OrdersStatisticsRepository.php, routes/web.php</files>
|
||||
<action>
|
||||
Extend the existing Statistics module with a new summary action:
|
||||
- Add `summary(Request $request): Response` to `OrdersStatisticsController`.
|
||||
- Reuse or extract existing filter helpers for date range, channel options, selected channels, status groups, selected status groups, and status-code resolution.
|
||||
- Use a useful monthly default range: first day of January of the current year through the current date, unless GET filters are supplied.
|
||||
- Add repository method `aggregateByMonth(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array`.
|
||||
- The query must reuse the existing effective date, channel, gross amount, and status SQL semantics from `aggregateByDay()`.
|
||||
- Group rows by `DATE_FORMAT(effective_date, "%Y-%m")` and channel key.
|
||||
- Build a controller view-model with ordered month labels, per-channel count series, per-channel value series, and total series for both charts.
|
||||
- Add route `GET /statistics/summary` behind `AuthMiddleware` using the existing controller instance.
|
||||
|
||||
Avoid: no schema changes, no raw user input in SQL strings, no duplicate channel/status logic that can drift from `/statistics/orders`.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l src/Modules/Statistics/OrdersStatisticsController.php`
|
||||
- `php -l src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||
- `php -l routes/web.php`
|
||||
- Manual check that `/statistics/summary` returns HTTP 200 for an authenticated session.
|
||||
</verify>
|
||||
<done>AC-2, AC-3, and AC-4 satisfied at backend/view-model level.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add summary UI, charts, menu item, and translations</name>
|
||||
<files>resources/views/layouts/app.php, resources/views/statistics/summary.php, resources/lang/pl.php, public/assets/js/modules/statistics-summary-charts.js, resources/scss/app.scss, public/assets/css/app.css</files>
|
||||
<action>
|
||||
Create the visible statistics summary experience:
|
||||
- Add `Podsumowanie` translation keys under navigation and `statistics.summary.*`.
|
||||
- In the sidebar statistics group, place `Podsumowanie` before `Zamowienia`, link it to `/statistics/summary`, and use `activeStatistics='summary'`.
|
||||
- Create `resources/views/statistics/summary.php` with compact filters matching `/statistics/orders`: date from/to, channels multiselect, status groups multiselect, submit/reset actions.
|
||||
- Render two chart panels: order count and order value. Each panel must include a `<canvas>` or SVG/chart container plus a table fallback with monthly rows and per-series columns.
|
||||
- Add `public/assets/js/modules/statistics-summary-charts.js` to render the two charts from JSON embedded in the page after escaping through `json_encode(..., JSON_HEX_*)`.
|
||||
- Keep chart rendering small and local. Do not add a package dependency unless implementation discovers existing project-approved chart tooling.
|
||||
- Include the module in `resources/views/layouts/app.php` with `filemtime()` cache busting.
|
||||
- Add compact SCSS in `resources/scss/app.scss`, then build `public/assets/css/app.css` with `npm run build:css`.
|
||||
|
||||
Avoid: no inline CSS in PHP views, no native `alert()`/`confirm()`, no decorative oversized dashboard layout. The page should stay dense and operational.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l resources/views/layouts/app.php`
|
||||
- `php -l resources/views/statistics/summary.php`
|
||||
- `php -l resources/lang/pl.php`
|
||||
- `npm run build:css`
|
||||
- Browser/manual check confirms two non-empty charts render when data exists and fallback tables are present in the DOM.
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3, and AC-5 satisfied at UI level.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Update technical documentation</name>
|
||||
<files>DOCS/ARCHITECTURE.md, DOCS/DB_SCHEMA.md, DOCS/TECH_CHANGELOG.md, .paul/codebase/architecture.md, .paul/codebase/db_schema.md, .paul/codebase/tech_changelog.md</files>
|
||||
<action>
|
||||
Update documentation required by `AGENTS.md` and PAUL:
|
||||
- If root `DOCS/` files are still missing, create `DOCS/ARCHITECTURE.md`, `DOCS/DB_SCHEMA.md`, and `DOCS/TECH_CHANGELOG.md` using the current `.paul/codebase/*` documents as the baseline where appropriate.
|
||||
- Document the new `/statistics/summary` route, controller action, repository monthly aggregation method, chart frontend module, and sidebar change in architecture docs.
|
||||
- Document that the feature uses existing `orders`, `integrations`, `order_status_groups`, and `order_statuses` tables without migration changes.
|
||||
- Add a dated technical changelog entry explaining the new summary charts and why no DB schema change was needed.
|
||||
- Keep `.paul/codebase/*` in sync with the same architecture/schema/changelog facts.
|
||||
</action>
|
||||
<verify>
|
||||
- `git diff -- DOCS .paul/codebase` shows architecture/changelog updates and no false DB migration claim.
|
||||
- Documentation mentions `/statistics/summary`, `aggregateByMonth()`, and `statistics-summary-charts.js`.
|
||||
</verify>
|
||||
<done>Documentation requirements from `AGENTS.md` and PAUL are satisfied.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>`Statystyki -> Podsumowanie` with two monthly charts</what-built>
|
||||
<how-to-verify>
|
||||
1. Open `/statistics/summary`.
|
||||
2. Confirm the sidebar shows `Podsumowanie` as the first item under `Statystyki`.
|
||||
3. Confirm chart 1 shows monthly order counts with one line per integration plus `Razem`.
|
||||
4. Confirm chart 2 shows monthly order values with one line per integration plus `Razem`.
|
||||
5. Change filters and confirm both charts update consistently.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue to UNIFY, or describe visual/data issues to fix.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Import/synchronization modules for Allegro/shopPRO.
|
||||
- Existing `/statistics/orders` route contract and GET parameter names.
|
||||
- Database migrations unless a blocker proves the existing schema cannot support the charts.
|
||||
- Runtime DB env behavior: do not wire `DB_HOST_REMOTE` into application runtime.
|
||||
|
||||
## SCOPE LIMITS
|
||||
- This plan adds only summary charts for orders. It does not add exports, product charts, inventory charts, or accounting reports.
|
||||
- Values chart uses the existing gross amount semantics from current order statistics unless the user explicitly asks for net values.
|
||||
- Chart interactivity is limited to readable lines/legend/tooltips if implemented locally; advanced drill-down is out of scope.
|
||||
- No new native alerts or confirms.
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l src/Modules/Statistics/OrdersStatisticsController.php`
|
||||
- [ ] `php -l src/Modules/Statistics/OrdersStatisticsRepository.php`
|
||||
- [ ] `php -l routes/web.php`
|
||||
- [ ] `php -l resources/views/layouts/app.php`
|
||||
- [ ] `php -l resources/views/statistics/summary.php`
|
||||
- [ ] `php -l resources/lang/pl.php`
|
||||
- [ ] `npm run build:css`
|
||||
- [ ] Manual/browser check: `/statistics/summary` renders two charts and table fallbacks.
|
||||
- [ ] Manual/browser check: sidebar order and active states are correct.
|
||||
- [ ] Manual data check: monthly total equals sum of integration values for at least one month.
|
||||
- [ ] Documentation updated in `DOCS/` and `.paul/codebase/`.
|
||||
- [ ] `sonar-scanner` run before UNIFY, or blocker recorded if unavailable.
|
||||
- [ ] All acceptance criteria met.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `Statystyki -> Podsumowanie` is the first statistics submenu item.
|
||||
- `/statistics/summary` displays two monthly charts: order count and order value.
|
||||
- Each chart includes separate integration series and a `Razem` series.
|
||||
- Filters are consistent with existing order statistics behavior.
|
||||
- No database schema change is introduced.
|
||||
- Technical documentation is updated.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/110-statistics-summary/110-01-SUMMARY.md`.
|
||||
</output>
|
||||
197
.paul/phases/110-statistics-summary/110-01-SUMMARY.md
Normal file
197
.paul/phases/110-statistics-summary/110-01-SUMMARY.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
phase: 110-statistics-summary
|
||||
plan: 01
|
||||
subsystem: statistics
|
||||
tags: [statistics, charts, chartjs, monthly-summary, ui, reporting]
|
||||
requires:
|
||||
- phase: 105-orders-statistics
|
||||
provides: daily statistics filters, channel semantics, status group filtering
|
||||
- phase: 109-checkbox-multiselect-filters
|
||||
provides: reusable checkbox multiselect filter enhancement
|
||||
provides:
|
||||
- /statistics/summary monthly summary page
|
||||
- monthly order count chart per integration plus total
|
||||
- monthly order value chart per integration plus total
|
||||
- DOCS baseline synchronized from .paul/codebase
|
||||
affects: [statistics, reporting, sidebar-navigation]
|
||||
tech-stack:
|
||||
added: [Chart.js 4.4.8 CDN]
|
||||
patterns: [monthly statistics aggregation, Chart.js view-model, table fallback]
|
||||
key-files:
|
||||
created:
|
||||
- resources/views/statistics/summary.php
|
||||
- public/assets/js/modules/statistics-summary-charts.js
|
||||
- DOCS/ARCHITECTURE.md
|
||||
- DOCS/DB_SCHEMA.md
|
||||
- DOCS/TECH_CHANGELOG.md
|
||||
modified:
|
||||
- src/Modules/Statistics/OrdersStatisticsController.php
|
||||
- src/Modules/Statistics/OrdersStatisticsRepository.php
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
- resources/lang/pl.php
|
||||
- resources/scss/app.scss
|
||||
- public/assets/css/app.css
|
||||
key-decisions:
|
||||
- "Default summary history starts at 2026-04-01 even if older orders exist."
|
||||
- "Charts use Chart.js 4.4.8 from CDN for interactive legend and tooltips."
|
||||
- "Gross order value uses existing statistics gross amount semantics."
|
||||
patterns-established:
|
||||
- "Monthly statistics use the same channel/status/effective-date helpers as daily statistics."
|
||||
- "Charts are backed by HTML table fallbacks."
|
||||
duration: ~90min
|
||||
started: 2026-04-28T22:20:00+02:00
|
||||
completed: 2026-04-28T23:50:00+02:00
|
||||
---
|
||||
|
||||
# Phase 110 Plan 01: Statistics Summary
|
||||
|
||||
Monthly statistics summary shipped with two interactive Chart.js line charts, compact filters, per-integration series, total series, and table fallbacks.
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~90min |
|
||||
| Started | 2026-04-28T22:20:00+02:00 |
|
||||
| Completed | 2026-04-28T23:50:00+02:00 |
|
||||
| Tasks | 3 auto tasks + 1 human verification checkpoint |
|
||||
| Files modified | 18 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Sidebar Navigation | Pass | `Podsumowanie` added first under `Statystyki`; `/statistics/summary` route registered with active state. |
|
||||
| AC-2: Monthly Order Count Chart | Pass | Count chart model contains one dataset per selected integration plus `Razem`; rendered by Chart.js. |
|
||||
| AC-3: Monthly Order Value Chart | Pass | Value chart model contains one dataset per selected integration plus `Razem`; values formatted as money in tooltips/table. |
|
||||
| AC-4: Existing Statistics Semantics Reused | Pass | Controller reuses channel/status filter flow; repository reuses effective date/channel/status/gross SQL helpers. |
|
||||
| AC-5: Empty and Degraded States | Pass | Empty state shown when no data; tables remain visible as fallback below charts. |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Added authenticated `/statistics/summary` page with monthly order count and order gross value charts.
|
||||
- Added `OrdersStatisticsRepository::aggregateByMonth()` using prepared parameters and existing statistics SQL helper semantics.
|
||||
- Added `OrdersStatisticsController::summary()` and a chart/table view-model with total lines.
|
||||
- Updated sidebar navigation, translations, SCSS, compiled CSS, and Chart.js frontend module.
|
||||
- Created root `DOCS/` technical documentation required by `AGENTS.md` and synchronized it with `.paul/codebase/`.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Task-level atomic commits were not used in this inline APPLY. A single phase commit is created during transition.
|
||||
|
||||
| Task | Commit | Type | Description |
|
||||
|------|--------|------|-------------|
|
||||
| Task 1: Add monthly statistics backend and route | phase commit | feat | Controller summary action, monthly aggregation, `/statistics/summary` route |
|
||||
| Task 2: Add summary UI, charts, menu item, and translations | phase commit | feat | Summary view, sidebar link, Chart.js charts, SCSS/CSS |
|
||||
| Task 3: Update technical documentation | phase commit | docs | `DOCS/` and `.paul/codebase/` updates |
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Statistics/OrdersStatisticsController.php` | Modified | Added summary action, monthly default range from `2026-04-01`, chart/table view-model builders. |
|
||||
| `src/Modules/Statistics/OrdersStatisticsRepository.php` | Modified | Added `aggregateByMonth()` with existing statistics SQL semantics. |
|
||||
| `routes/web.php` | Modified | Registered authenticated `GET /statistics/summary`. |
|
||||
| `resources/views/layouts/app.php` | Modified | Added `Podsumowanie` sidebar link and Chart.js/module scripts. |
|
||||
| `resources/views/statistics/summary.php` | Created | Summary filters, two charts, and two fallback tables. |
|
||||
| `resources/lang/pl.php` | Modified | Added navigation and `statistics.summary.*` translations. |
|
||||
| `public/assets/js/modules/statistics-summary-charts.js` | Created | Chart.js renderer for count/value line charts. |
|
||||
| `resources/scss/app.scss` | Modified | Added compact two-column desktop layout and chart/table styles. |
|
||||
| `public/assets/css/app.css` | Modified | Compiled CSS output. |
|
||||
| `DOCS/ARCHITECTURE.md` | Created | Root architecture docs baseline plus summary feature. |
|
||||
| `DOCS/DB_SCHEMA.md` | Created | Root schema docs baseline plus reporting usage note. |
|
||||
| `DOCS/TECH_CHANGELOG.md` | Created | Root technical changelog baseline plus phase 110 entry. |
|
||||
| `.paul/codebase/architecture.md` | Modified | Documented route, controller flow, Chart.js module. |
|
||||
| `.paul/codebase/db_schema.md` | Modified | Documented no-migration reporting usage. |
|
||||
| `.paul/codebase/tech_changelog.md` | Modified | Added phase 110 technical changelog. |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Start history at `2026-04-01` | User requested `04-2026` as hard start despite older data. | Default summary excludes older months unless code changes later. |
|
||||
| Use Chart.js 4.4.8 CDN | User requested an interactive JS library; project already uses CDN for Quill. | No npm dependency or build pipeline change. |
|
||||
| Use gross order values | Existing statistics gross semantics are stable and already documented. | Value chart matches current order statistics totals. |
|
||||
| Keep table fallback | Required degraded state and useful for verification. | Data remains visible without Chart.js/JS. |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope adjustments from user feedback | 3 | Improved UI and changed default date semantics. |
|
||||
| Environment blockers | 3 | Verification limits only; code/build checks passed. |
|
||||
|
||||
**Total impact:** Controlled deviations; feature outcome matches latest user request.
|
||||
|
||||
### Adjusted During APPLY
|
||||
|
||||
**Chart renderer changed to Chart.js**
|
||||
- Found during user feedback after APPLY.
|
||||
- Fix: Added Chart.js CDN and rewrote `statistics-summary-charts.js` to use Chart.js legends/tooltips.
|
||||
- Verification: `node --check`, PHP lint, CSS build, Sonar scan.
|
||||
|
||||
**Desktop layout split**
|
||||
- Found during user feedback after APPLY.
|
||||
- Fix: Added two-column chart grid and two-column table grid from 1100px.
|
||||
- Verification: PHP lint and CSS build.
|
||||
|
||||
**Default history start**
|
||||
- Found during user feedback after APPLY.
|
||||
- Fix: Set summary default `date_from` to `2026-04-01`.
|
||||
- Verification: PHP lint.
|
||||
|
||||
### Deferred Items
|
||||
|
||||
- SonarQube issue import into `DOCS/todo.md` was not performed because no SonarQube MCP resources/tools are available in this Codex session.
|
||||
- Full authenticated HTTP verification was not possible locally because MySQL/XAMPP refused the DB connection.
|
||||
- PHPUnit was not run because `composer` is not available in PATH and `vendor/` is absent.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Local bootstrap failed with MySQL connection refused. | Started PHP dev server anyway; documented DB blocker. |
|
||||
| `composer test` unavailable. | Documented test blocker; ran available lint/build/static checks. |
|
||||
| Sonar MCP unavailable. | Ran `sonar-scanner` successfully and documented inability to fetch issue details. |
|
||||
|
||||
## Verification Results
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| `php -l src/Modules/Statistics/OrdersStatisticsController.php` | Pass |
|
||||
| `php -l src/Modules/Statistics/OrdersStatisticsRepository.php` | Pass |
|
||||
| `php -l routes/web.php` | Pass |
|
||||
| `php -l resources/views/layouts/app.php` | Pass |
|
||||
| `php -l resources/views/statistics/summary.php` | Pass |
|
||||
| `php -l resources/lang/pl.php` | Pass |
|
||||
| `node --check public/assets/js/modules/statistics-summary-charts.js` | Pass |
|
||||
| `npm run build:css` | Pass |
|
||||
| `sonar-scanner` | Pass; report uploaded to SonarQube |
|
||||
| Local `/statistics/summary` HTTP 200 | Blocked by local MySQL connection refused |
|
||||
| `composer test` / PHPUnit | Blocked: `composer` not in PATH and `vendor/` absent |
|
||||
|
||||
## Skill Audit
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| `sonar-scanner` | yes | Ran successfully after APPLY changes. |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Statistics summary page exists and is wired into sidebar navigation.
|
||||
- Monthly aggregation pattern can be reused for future reporting charts.
|
||||
- Root `DOCS/` docs now exist for future AGENTS.md-compliant changes.
|
||||
|
||||
**Concerns:**
|
||||
- Chart.js is CDN-based; if offline operation becomes required, vendor locally or add npm build support.
|
||||
- Local verification depends on XAMPP/MySQL being available.
|
||||
- Sonar issue import still needs a working SonarQube MCP/tool path.
|
||||
|
||||
**Blockers:**
|
||||
- None for closing Phase 110.
|
||||
|
||||
---
|
||||
*Phase: 110-statistics-summary, Plan: 01*
|
||||
*Completed: 2026-04-28*
|
||||
175
DOCS/ARCHITECTURE.md
Normal file
175
DOCS/ARCHITECTURE.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Architecture
|
||||
|
||||
## Request Flow
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
→ public/index.php
|
||||
→ bootstrap/app.php (loads config, registers PDO, services)
|
||||
→ Application::boot() (loads routes/web.php)
|
||||
→ Router::dispatch(Request) (matches URL, runs middleware pipeline)
|
||||
→ [Middleware] (AuthMiddleware, ApiKeyMiddleware)
|
||||
→ Controller::method() (parse input → call repository/service → render)
|
||||
→ Template::render() (PHP native, layout composition)
|
||||
→ Response::send()
|
||||
```
|
||||
|
||||
## Layer Map
|
||||
|
||||
| Layer | Location | Responsibility |
|
||||
|-------|----------|----------------|
|
||||
| Entry | `public/index.php` | Bootstrap only |
|
||||
| Routes | `routes/web.php` (581 lines) | All ~80 routes; manual DI wiring |
|
||||
| Core | `src/Core/` (25 files) | Framework infrastructure |
|
||||
| Controllers | `src/Modules/*/Controller.php` | Request parsing → response |
|
||||
| Services | `src/Modules/*/Service.php` | Business logic |
|
||||
| Repositories | `src/Modules/*/Repository.php` | PDO data access (34+ repos) |
|
||||
| Views | `resources/views/` | PHP templates with `$e()` / `$t()` |
|
||||
| Components | `resources/views/components/` | Reusable UI blocks |
|
||||
| Frontend modules | `public/assets/js/modules/` | Small vanilla JS enhancements loaded by layout |
|
||||
|
||||
## Module Inventory (`src/Modules/`)
|
||||
|
||||
| Module | Files | Key Classes | Purpose |
|
||||
|--------|-------|-------------|---------|
|
||||
| **Auth** | 3 | `AuthController`, `AuthMiddleware`, `AuthService` | Login/logout, session |
|
||||
| **Users** | 2 | `UserController`, `UserRepository` | User CRUD |
|
||||
| **Orders** | 3 | `OrdersController` (1187 LOC), `OrdersRepository` (1221 LOC) | Order list, detail, status, payment, correlated subquery for return-risk |
|
||||
| **Shipments** | 17 | `ShipmentController`, provider services + tracking services | Shipment creation, label download, tracking polling |
|
||||
| **Accounting** | 5 | `AccountingController`, `ReceiptService`, `ReceiptRepository` | Receipts, invoices, PDF, Excel export |
|
||||
| **Email** | 3 | `EmailSendingService`, `VariableResolver`, `AttachmentGenerator` | Template-based email with PDF attachments |
|
||||
| **Automation** | 6 | `AutomationService` (834 LOC), `AutomationRepository`, `AutomationExecutionLogRepository` | Event→condition→action rules, email triggers |
|
||||
| **Settings** | 51+ | Integration controllers, OAuth clients, API clients, mappers | Allegro/shopPRO/Apaczka/InPost config, status mappings |
|
||||
| **Cron** | 12 | `CronRepository`, `CronHandlerFactory`, handler classes | Scheduled imports, syncs, token refresh |
|
||||
| **Printing** | 4 | `PrintApiController`, `PrintJobRepository`, `ApiKeyMiddleware` | REST API for Windows print client |
|
||||
| **Statistics** | 3 | `OrdersStatisticsController`, `OrdersStatisticsRepository`, `statistics-summary-charts.js` | Daily order statistics and monthly summary charts |
|
||||
| **Info** | 1 | `InfoController` | Health check |
|
||||
|
||||
## Frontend Enhancement Modules
|
||||
|
||||
### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`)
|
||||
- Loaded globally from `resources/views/layouts/app.php`.
|
||||
- Enhances native `<select multiple data-checkbox-multiselect>` controls after `DOMContentLoaded`.
|
||||
- Keeps the original select in the form, synchronizes option `selected` state, and preserves native GET/POST names such as `channels[]` and `status_groups[]`.
|
||||
- Used by `/statistics/orders` and `/statistics/summary` filters to display a compact trigger, checkbox dropdown, "Wszystkie" bulk toggle, and selected count.
|
||||
- Progressive enhancement: if JavaScript fails, the native multi-select remains visible.
|
||||
|
||||
### Statistics Summary Charts (`public/assets/js/modules/statistics-summary-charts.js`)
|
||||
- Loaded globally from `resources/views/layouts/app.php` after Chart.js 4.4.8 CDN; activates only when `#js-statistics-summary-data` exists.
|
||||
- Reads JSON produced by `OrdersStatisticsController::summary()` and renders two interactive Chart.js line charts on `/statistics/summary`.
|
||||
- Chart 1 displays monthly order counts per selected integration plus a `Razem` line.
|
||||
- Chart 2 displays monthly gross order values per selected integration plus a `Razem` line.
|
||||
- The PHP view keeps table fallbacks under both charts, so the data remains visible if JavaScript fails.
|
||||
|
||||
## Key Data Flows
|
||||
|
||||
### Order Lifecycle
|
||||
1. **Import** — Cron handler → API client → `OrderImportService` → `OrdersRepository::insertOrder()` → `AutomationService::executeForNewOrder()`
|
||||
2. **Status update** — `OrdersController::updateStatus()` → `OrdersRepository::updateStatus()` → automation check
|
||||
3. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
|
||||
|
||||
### Statistics Summary
|
||||
1. **Request** — `/statistics/summary` → `OrdersStatisticsController::summary()`
|
||||
2. **Filters** — controller reuses statistics filter semantics: date range, `channels[]`, `status_groups[]`, default status groups excluding cancelled; default history starts at `2026-04-01`.
|
||||
3. **Aggregation** — `OrdersStatisticsRepository::aggregateByMonth()` groups existing `orders` rows by `YYYY-MM` and channel key, using the same effective date/channel/status/gross amount SQL helpers as the daily report.
|
||||
4. **View model** — controller builds per-integration series and total series for order count and gross value charts.
|
||||
5. **Render** — `resources/views/statistics/summary.php` renders filters, chart JSON, two canvas targets, and table fallbacks.
|
||||
|
||||
### Shipment Flow
|
||||
1. **Create** — `ShipmentController::create()` → `ShipmentProviderRegistry` → carrier `ShipmentService::createShipment()` → `ShipmentPackageRepository::insert()`
|
||||
2. **Track** — Cron `ShipmentTrackingHandler` → `ShipmentTrackingRegistry` → carrier tracking API → `ShipmentPackageRepository::updateDeliveryStatus()`
|
||||
|
||||
### Receipt / Invoice
|
||||
1. **Generate** — `ReceiptController::store()` → `ReceiptService::generateReceipt()` → `ReceiptRepository::insert()` + Dompdf PDF
|
||||
2. **Email** — `EmailSendingService::send()` → `VariableResolver::resolve()` → `AttachmentGenerator::generatePdf()` → PHPMailer SMTP
|
||||
|
||||
### Automation Rules
|
||||
1. **Setup** — `AutomationController` → `AutomationRepository::insertRule()`
|
||||
2. **Trigger** — `AutomationService::executeForOrder()` → evaluates trigger (`order_status_changed`, `order_status_aged`) → runs action (send email, update status)
|
||||
3. **Log** — `AutomationExecutionLogRepository` tracks every run
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
| Handler | Task |
|
||||
|---------|------|
|
||||
| `AllegroOrdersImportHandler` | Fetch new Allegro orders |
|
||||
| `AllegroStatusSyncHandler` | Push status changes to Allegro |
|
||||
| `AllegroTokenRefreshHandler` | OAuth token refresh (24h expiry) |
|
||||
| `ShopproOrdersImportHandler` | Fetch new shopPRO orders |
|
||||
| `ShopproStatusSyncHandler` | Push status to shopPRO |
|
||||
| `ShopproPaymentStatusSyncHandler` | Sync payment statuses |
|
||||
| `ShipmentTrackingHandler` | Poll carrier tracking APIs |
|
||||
| `OrderStatusAgedHandler` | Trigger automation for stuck statuses |
|
||||
| `AutomationHistoryCleanupHandler` | Purge old automation logs |
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Manual constructor injection in `routes/web.php` — no DI container library. Example:
|
||||
|
||||
```php
|
||||
$ordersController = new OrdersController(
|
||||
$template, $translator, $auth,
|
||||
$app->orders(), $shipmentPackageRepository,
|
||||
$receiptRepository, $receiptConfigRepository, ...
|
||||
);
|
||||
```
|
||||
|
||||
All production classes are `final` — prevents accidental inheritance.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
bootstrap/ app.php (service wiring, config loading)
|
||||
bin/ migrate.php, cron.php (CLI entry points)
|
||||
config/ app.php, database.php
|
||||
database/
|
||||
migrations/ 84 SQL files (YYYYMMDD_NNNNNN_description.sql)
|
||||
drafts/ WIP migrations
|
||||
public/
|
||||
index.php HTTP entry point
|
||||
.htaccess Apache rewrite rules
|
||||
assets/css/ Compiled CSS (app.css, login.css, modules/)
|
||||
assets/js/ jquery-alerts.js, global-search.js, automation-form.js
|
||||
resources/
|
||||
views/ PHP templates by module + components/ layouts/
|
||||
scss/ SCSS sources (app.scss, login.scss, modules/_*.scss)
|
||||
modules/ jquery-alerts JS+SCSS source
|
||||
lang/pl/ Polish translations
|
||||
routes/
|
||||
web.php All routes (581 lines)
|
||||
src/
|
||||
Core/ Framework (25 files)
|
||||
Modules/ 13 feature modules (~200+ PHP files)
|
||||
storage/
|
||||
logs/ app.log
|
||||
sessions/ PHP session files
|
||||
cache/ PHPUnit cache, etc.
|
||||
tests/
|
||||
Unit/ PHPUnit tests (7+ service test files)
|
||||
bootstrap.php PSR-4 autoloader for tests
|
||||
```
|
||||
|
||||
## Phase 108 — Delivery Status Management
|
||||
|
||||
### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`)
|
||||
- CRUD dla tabeli `delivery_statuses`
|
||||
- Per-request static cache (`private static ?array $cache`)
|
||||
- Blokuje edycję/usunięcie statusów systemowych (`is_system=1`)
|
||||
- Blokuje usunięcie statusów używanych w `delivery_status_mappings` lub `shipment_packages`
|
||||
|
||||
### DeliveryStatusesController (`src/Modules/Settings/DeliveryStatusesController.php`)
|
||||
- Panel `/settings/delivery-statuses`
|
||||
- Dwie zakładki via `?tab=` param: `statuses` (CRUD) i `mapping` (embed mapowania)
|
||||
- Wstrzykuje `DeliveryStatusRepository` i `DeliveryStatusMappingRepository`
|
||||
|
||||
### DeliveryStatus::setRepository() (dynamic loading)
|
||||
- Wywoływane raz w `routes/web.php` po bootstrap
|
||||
- `label()`, `getAllOptions()`, `getAllStatuses()`, `getColor()` ładują z DB gdy repo ustawione
|
||||
- Fallback na hardcoded stałe gdy repo niedostępne
|
||||
|
||||
### AutomationController + AutomationService (Phase 108 Plan 02)
|
||||
- `AutomationController::buildShipmentStatusOptions()` — buduje listę opcji `[key => ['label' => ...]]` z `DeliveryStatus::getAllOptions()` (DB-driven)
|
||||
- Walidacja `shipment_status` warunku i `update_shipment_status` akcji w `parseConditionValue()`/`parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
|
||||
- `AutomationService::evaluateShipmentStatusCondition()` — bezpośrednie porównanie kluczy DB (usunięto mapping grupowy `SHIPMENT_STATUS_OPTION_MAP`)
|
||||
- `AutomationService::resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target
|
||||
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) nie matchują się — operator musi je odtworzyć przy użyciu nowych kluczy DB
|
||||
863
DOCS/DB_SCHEMA.md
Normal file
863
DOCS/DB_SCHEMA.md
Normal file
@@ -0,0 +1,863 @@
|
||||
# Database Schema
|
||||
|
||||
**Updated:** 2026-04-28 | **Total tables:** 55 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
|
||||
|
||||
---
|
||||
|
||||
## Auth / Users
|
||||
|
||||
**users** — System user accounts with authentication
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK, AUTO_INCREMENT |
|
||||
| `name` | VARCHAR(120) | NO | |
|
||||
| `email` | VARCHAR(190) | NO | UNIQUE |
|
||||
| `password_hash` | VARCHAR(255) | NO | |
|
||||
| `remember_token` | VARCHAR(255) | YES | SHA256 of cookie token |
|
||||
| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
---
|
||||
|
||||
## Products
|
||||
|
||||
**products** — Main product catalog
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `uuid` | CHAR(36) | NO | UNIQUE |
|
||||
| `type` | ENUM('simple','variant_parent') | NO | DEFAULT 'simple' |
|
||||
| `sku` | VARCHAR(128) | YES | UNIQUE |
|
||||
| `ean` | VARCHAR(32) | YES | |
|
||||
| `status` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `promoted` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `new_to_date` | DATE | YES | |
|
||||
| `additional_message` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `additional_message_required` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `additional_message_text` | TEXT | YES | |
|
||||
| `vat` | DECIMAL(5,2) | YES | |
|
||||
| `weight` | DECIMAL(10,3) | YES | |
|
||||
| `price_brutto` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
|
||||
| `price_brutto_promo` | DECIMAL(12,2) | YES | |
|
||||
| `price_netto` | DECIMAL(12,2) | YES | |
|
||||
| `price_netto_promo` | DECIMAL(12,2) | YES | |
|
||||
| `quantity` | DECIMAL(12,3) | NO | DEFAULT 0.000 |
|
||||
| `producer_id` | INT UNSIGNED | YES | |
|
||||
| `producer_name` | VARCHAR(255) | YES | |
|
||||
| `product_unit_id` | INT UNSIGNED | YES | |
|
||||
| `custom_fields_json` | TEXT | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | ON UPDATE CURRENT_TIMESTAMP |
|
||||
| `deleted_at` | DATETIME | YES | Soft delete |
|
||||
|
||||
Indexes: `products_status_idx`, `products_type_idx`, `products_updated_at_idx`, `products_ean_idx`
|
||||
|
||||
**product_translations** — Localized product names/descriptions
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
|
||||
| `lang` | VARCHAR(8) | NO | |
|
||||
| `name` | VARCHAR(255) | NO | |
|
||||
| `short_description` | TEXT | YES | |
|
||||
| `description` | LONGTEXT | YES | |
|
||||
| `meta_title` | VARCHAR(255) | YES | |
|
||||
| `meta_description` | VARCHAR(255) | YES | |
|
||||
| `meta_keywords` | VARCHAR(255) | YES | |
|
||||
| `seo_link` | VARCHAR(255) | YES | |
|
||||
| `security_information` | MEDIUMTEXT | YES | GPSR data |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(product_id, lang)`
|
||||
|
||||
**product_images** — Product image storage references
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
|
||||
| `storage_path` | VARCHAR(255) | NO | |
|
||||
| `alt` | VARCHAR(255) | YES | |
|
||||
| `sort_order` | INT | NO | DEFAULT 0 |
|
||||
| `is_main` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**product_categories** — Product–category associations (m2m)
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `product_id` | INT UNSIGNED | FK → products(id) CASCADE |
|
||||
| `category_id` | INT UNSIGNED | |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
PK: `(product_id, category_id)`
|
||||
|
||||
**product_variants** — Variants for `variant_parent` products
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
|
||||
| `permutation_hash` | VARCHAR(191) | NO | |
|
||||
| `sku` | VARCHAR(128) | YES | UNIQUE |
|
||||
| `ean` | VARCHAR(32) | YES | |
|
||||
| `status` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `stock_0_buy` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `price_brutto` | DECIMAL(12,2) | YES | |
|
||||
| `price_netto` | DECIMAL(12,2) | YES | |
|
||||
| `weight` | DECIMAL(10,3) | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(product_id, permutation_hash)`
|
||||
|
||||
**product_variant_attributes** — Variant–attribute value mapping
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `variant_id` | INT UNSIGNED | FK → product_variants(id) CASCADE |
|
||||
| `attribute_id` | INT UNSIGNED | FK → attributes(id) RESTRICT |
|
||||
| `value_id` | INT UNSIGNED | FK → attribute_values(id) RESTRICT |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
PK: `(variant_id, attribute_id)`
|
||||
|
||||
**attributes** — Attribute definitions (e.g., size, color)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `type` | TINYINT UNSIGNED | NO | DEFAULT 1 |
|
||||
| `status` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**attribute_translations** — Localized attribute names
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `attribute_id` | INT UNSIGNED | FK → attributes(id) CASCADE |
|
||||
| `lang` | VARCHAR(8) | |
|
||||
| `name` | VARCHAR(255) | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
PK: `(attribute_id, lang)`
|
||||
|
||||
**attribute_values** — Possible values per attribute
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `attribute_id` | INT UNSIGNED | NO | FK → attributes(id) CASCADE |
|
||||
| `status` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `is_default` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `impact_on_price` | DECIMAL(12,2) | YES | |
|
||||
| `sort_order` | INT | NO | DEFAULT 0 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**attribute_value_translations** — Localized value names
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `value_id` | INT UNSIGNED | FK → attribute_values(id) CASCADE |
|
||||
| `lang` | VARCHAR(8) | |
|
||||
| `name` | VARCHAR(255) | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
PK: `(value_id, lang)`
|
||||
|
||||
**product_change_log** — Audit trail for product modifications
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
|
||||
| `user_id` | INT UNSIGNED | YES | FK → users(id) SET NULL |
|
||||
| `change_type` | VARCHAR(64) | NO | |
|
||||
| `before_json` | JSON | YES | |
|
||||
| `after_json` | JSON | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**sales_channels** — Selling channel definitions
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `code` | VARCHAR(64) | NO | UNIQUE |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `type` | VARCHAR(64) | NO | |
|
||||
| `status` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**product_channel_map** — Link products to external channel offers
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_id` | INT UNSIGNED | NO | FK → products(id) CASCADE |
|
||||
| `channel_id` | INT UNSIGNED | NO | FK → sales_channels(id) CASCADE |
|
||||
| `integration_id` | INT UNSIGNED | YES | FK → integrations(id) SET NULL |
|
||||
| `external_product_id` | VARCHAR(128) | YES | |
|
||||
| `external_variant_id` | VARCHAR(128) | YES | |
|
||||
| `sync_state` | VARCHAR(32) | NO | DEFAULT 'not_linked' |
|
||||
| `link_type` | VARCHAR(32) | NO | DEFAULT 'manual' |
|
||||
| `link_status` | VARCHAR(32) | NO | DEFAULT 'active' |
|
||||
| `confidence` | TINYINT UNSIGNED | YES | |
|
||||
| `linked_at` | DATETIME | YES | |
|
||||
| `unlinked_at` | DATETIME | YES | |
|
||||
| `sync_meta_json` | JSON | YES | |
|
||||
| `last_sync_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(product_id, channel_id, external_product_id, external_variant_id)`
|
||||
|
||||
**channel_offers** — External channel offer snapshots
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `integration_id` | INT UNSIGNED | NO | FK → integrations(id) CASCADE |
|
||||
| `channel_id` | INT UNSIGNED | NO | FK → sales_channels(id) CASCADE |
|
||||
| `external_product_id` | VARCHAR(128) | NO | |
|
||||
| `external_variant_id` | VARCHAR(128) | YES | |
|
||||
| `external_offer_id` | VARCHAR(128) | YES | |
|
||||
| `name` | VARCHAR(255) | NO | |
|
||||
| `sku` | VARCHAR(128) | YES | |
|
||||
| `ean` | VARCHAR(32) | YES | |
|
||||
| `price_brutto` | DECIMAL(12,2) | YES | |
|
||||
| `quantity` | DECIMAL(12,3) | YES | |
|
||||
| `currency` | VARCHAR(8) | YES | |
|
||||
| `offer_status` | VARCHAR(32) | NO | DEFAULT 'active' |
|
||||
| `source_updated_at` | DATETIME | YES | |
|
||||
| `last_seen_at` | DATETIME | NO | |
|
||||
| `payload_json` | JSON | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(integration_id, external_product_id, external_variant_id)`
|
||||
|
||||
**product_link_events** — Audit log for product-channel linking changes
|
||||
|
||||
**product_link_alerts** — Alerts for product-channel link issues (ENUM status: `active`, `resolved`)
|
||||
|
||||
**product_integration_translations** — Integration-specific product description overrides
|
||||
|
||||
---
|
||||
|
||||
## Orders
|
||||
|
||||
**orders** — Imported orders from sales channels
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `internal_order_number` | VARCHAR(11) | YES | UNIQUE, auto-assigned |
|
||||
| `integration_id` | INT UNSIGNED | NO | FK → integrations(id) CASCADE |
|
||||
| `external_order_id` | VARCHAR(64) | NO | |
|
||||
| `external_order_number` | VARCHAR(128) | YES | |
|
||||
| `status` | VARCHAR(64) | YES | Internal status code |
|
||||
| `currency` | CHAR(3) | YES | |
|
||||
| `total_gross` | DECIMAL(12,2) | YES | |
|
||||
| `total_net` | DECIMAL(12,2) | YES | |
|
||||
| `total_with_tax` | DECIMAL(12,2) | YES | |
|
||||
| `total_paid` | DECIMAL(12,2) | YES | |
|
||||
| `buyer_email` | VARCHAR(190) | YES | |
|
||||
| `buyer_name` | VARCHAR(190) | YES | |
|
||||
| `buyer_phone` | VARCHAR(64) | YES | |
|
||||
| `payment_method` | VARCHAR(128) | YES | |
|
||||
| `payment_status` | VARCHAR(64) | YES | |
|
||||
| `external_payment_type_id` | VARCHAR(128) | YES | |
|
||||
| `delivery_method` | VARCHAR(128) | YES | |
|
||||
| `delivery_price` | DECIMAL(12,2) | YES | |
|
||||
| `delivery_tracking_number` | VARCHAR(128) | YES | |
|
||||
| `notes` | TEXT | YES | |
|
||||
| `external_created_at` | DATETIME | YES | |
|
||||
| `external_updated_at` | DATETIME | YES | |
|
||||
| `last_status_checked_at` | DATETIME | YES | |
|
||||
| `payload_json` | JSON | YES | Full raw API payload |
|
||||
| `fetched_at` | DATETIME | NO | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(integration_id, external_order_id)`
|
||||
|
||||
**order_items** — Line items within orders
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `external_item_id` | VARCHAR(64) | YES | |
|
||||
| `name` | VARCHAR(255) | NO | |
|
||||
| `sku` | VARCHAR(128) | YES | |
|
||||
| `ean` | VARCHAR(64) | YES | |
|
||||
| `quantity` | DECIMAL(12,3) | NO | DEFAULT 0 |
|
||||
| `price_gross` | DECIMAL(12,2) | YES | |
|
||||
| `price_net` | DECIMAL(12,2) | YES | |
|
||||
| `vat` | DECIMAL(6,2) | YES | |
|
||||
| `personalization` | TEXT | YES | Customer personalization data for design generation |
|
||||
| `project_generated` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `project_generated_at` | DATETIME | YES | |
|
||||
| `payload_json` | JSON | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**order_activity_log** — Event log for order state changes
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | BIGINT UNSIGNED | NO | PK |
|
||||
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `event_type` | VARCHAR(32) | NO | |
|
||||
| `summary` | VARCHAR(255) | NO | |
|
||||
| `details_json` | JSON | YES | |
|
||||
| `actor_type` | VARCHAR(16) | NO | DEFAULT 'system' |
|
||||
| `actor_name` | VARCHAR(128) | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**order_payments** — Payments linked to orders
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `source_payment_id` | VARCHAR(64) | YES | |
|
||||
| `external_payment_id` | VARCHAR(64) | YES | |
|
||||
| `payment_type_id` | VARCHAR(64) | NO | |
|
||||
| `payment_date` | DATETIME | YES | |
|
||||
| `amount` | DECIMAL(12,2) | YES | |
|
||||
| `currency` | CHAR(3) | YES | |
|
||||
| `comment` | VARCHAR(255) | YES | |
|
||||
| `payload_json` | JSON | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(order_id, source_payment_id)`
|
||||
|
||||
---
|
||||
|
||||
## Order Statuses
|
||||
|
||||
**order_status_groups** — Status group categories (e.g., "Nowe", "W realizacji")
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `name` | VARCHAR(120) | NO | |
|
||||
| `code` | VARCHAR(64) | NO | UNIQUE |
|
||||
| `color_hex` | CHAR(7) | NO | DEFAULT '#64748b' |
|
||||
| `sort_order` | INT | NO | DEFAULT 0 |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**order_statuses** — Individual statuses within groups
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `group_id` | INT UNSIGNED | NO | FK → order_status_groups(id) CASCADE |
|
||||
| `name` | VARCHAR(120) | NO | |
|
||||
| `code` | VARCHAR(64) | NO | UNIQUE |
|
||||
| `sort_order` | INT | NO | DEFAULT 0 |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**order_status_mappings** — Map external (e.g., shopPRO) statuses to internal ones
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | INT UNSIGNED | PK |
|
||||
| `integration_id` | INT UNSIGNED | FK → integrations(id) CASCADE |
|
||||
| `shoppro_status_code` | VARCHAR(64) | |
|
||||
| `shoppro_status_name` | VARCHAR(128) | |
|
||||
| `orderpro_status_code` | VARCHAR(64) | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
UNIQUE: `(integration_id, shoppro_status_code)`
|
||||
|
||||
**integration_order_sync_state** — Track order fetch progress per integration
|
||||
|
||||
**integration_order_status_sync_state** — Track status sync progress per integration and direction
|
||||
|
||||
---
|
||||
|
||||
## Shipments & Delivery
|
||||
|
||||
**shipment_packages** — Prepared shipments with tracking
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | BIGINT UNSIGNED | NO | PK |
|
||||
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `provider` | VARCHAR(32) | NO | DEFAULT 'allegro_wza' |
|
||||
| `delivery_method_id` | VARCHAR(128) | YES | |
|
||||
| `credentials_id` | VARCHAR(128) | YES | |
|
||||
| `command_id` | VARCHAR(64) | YES | |
|
||||
| `shipment_id` | VARCHAR(64) | YES | |
|
||||
| `tracking_number` | VARCHAR(128) | YES | |
|
||||
| `status` | VARCHAR(32) | NO | DEFAULT 'draft' |
|
||||
| `delivery_status` | VARCHAR(32) | NO | DEFAULT 'unknown' |
|
||||
| `delivery_status_raw` | VARCHAR(128) | YES | Provider's original status string |
|
||||
| `delivery_status_updated_at` | DATETIME | YES | |
|
||||
| `carrier_id` | VARCHAR(64) | YES | |
|
||||
| `package_type` | VARCHAR(16) | NO | DEFAULT 'PACKAGE' |
|
||||
| `weight_kg` | DECIMAL(8,3) | YES | |
|
||||
| `length_cm` | DECIMAL(8,1) | YES | |
|
||||
| `width_cm` | DECIMAL(8,1) | YES | |
|
||||
| `height_cm` | DECIMAL(8,1) | YES | |
|
||||
| `insurance_amount` | DECIMAL(12,2) | YES | |
|
||||
| `cod_amount` | DECIMAL(12,2) | YES | |
|
||||
| `label_format` | VARCHAR(8) | NO | DEFAULT 'PDF' |
|
||||
| `label_path` | VARCHAR(512) | YES | |
|
||||
| `receiver_point_id` | VARCHAR(64) | YES | Parcel locker ID |
|
||||
| `sender_point_id` | VARCHAR(64) | YES | |
|
||||
| `reference_number` | VARCHAR(128) | YES | |
|
||||
| `error_message` | TEXT | YES | |
|
||||
| `payload_json` | JSON | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
Indexes: `shipment_packages_order_idx`, `shipment_packages_status_idx`, `shipment_packages_tracking_idx`, `idx_delivery_status`
|
||||
|
||||
**shipment_presets** — Predefined shipment configurations (saved templates)
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | BIGINT UNSIGNED | PK |
|
||||
| `name` | VARCHAR(100) | |
|
||||
| `color` | VARCHAR(7) | DEFAULT '#3b82f6' |
|
||||
| `carrier` | VARCHAR(32) | |
|
||||
| `provider_code` | VARCHAR(32) | |
|
||||
| `delivery_method_id` | VARCHAR(128) | |
|
||||
| `credentials_id` | VARCHAR(128) | DEFAULT '' |
|
||||
| `carrier_id` | VARCHAR(64) | DEFAULT '' |
|
||||
| `package_type` | VARCHAR(16) | DEFAULT 'PACKAGE' |
|
||||
| `length_cm` | DECIMAL(8,1) | DEFAULT 25.0 |
|
||||
| `width_cm` | DECIMAL(8,1) | DEFAULT 20.0 |
|
||||
| `height_cm` | DECIMAL(8,1) | DEFAULT 8.0 |
|
||||
| `weight_kg` | DECIMAL(8,3) | DEFAULT 1.000 |
|
||||
| `sender_point_id` | VARCHAR(64) | DEFAULT '' |
|
||||
| `label_format` | VARCHAR(8) | DEFAULT 'PDF' |
|
||||
| `sort_order` | INT UNSIGNED | DEFAULT 0 |
|
||||
| `created_at` | TIMESTAMP | |
|
||||
| `updated_at` | TIMESTAMP | |
|
||||
|
||||
**delivery_statuses** — Normalized delivery status definitions (Phase 108)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `key` | VARCHAR(50) | NO | UNIQUE |
|
||||
| `label_pl` | VARCHAR(100) | NO | Polish label |
|
||||
| `color` | VARCHAR(7) | NO | DEFAULT '#6c757d' |
|
||||
| `sort_order` | TINYINT UNSIGNED | NO | DEFAULT 0 |
|
||||
| `is_terminal` | TINYINT(1) | NO | DEFAULT 0 — marks final states |
|
||||
| `is_system` | TINYINT(1) | NO | DEFAULT 0 — system-managed |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**delivery_status_mappings** — Map provider-specific raw statuses to normalized keys (Phase 108)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `provider` | VARCHAR(32) | NO | |
|
||||
| `raw_status` | VARCHAR(64) | NO | |
|
||||
| `normalized_status` | VARCHAR(32) | NO | FK ref → delivery_statuses.key |
|
||||
| `description` | VARCHAR(255) | NO | DEFAULT '' |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(provider, raw_status)`
|
||||
|
||||
---
|
||||
|
||||
## Integrations
|
||||
|
||||
**integrations** — Integration configurations for external services
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `type` | VARCHAR(32) | NO | e.g., 'allegro', 'shoppro', 'apaczka', 'inpost' |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `base_url` | VARCHAR(255) | NO | |
|
||||
| `api_key_encrypted` | TEXT | YES | AES-encrypted |
|
||||
| `timeout_seconds` | SMALLINT UNSIGNED | NO | DEFAULT 10 |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `orders_fetch_enabled` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `orders_fetch_start_date` | DATE | YES | |
|
||||
| `order_status_sync_direction` | VARCHAR(32) | NO | DEFAULT 'shoppro_to_orderpro' |
|
||||
| `last_test_status` | VARCHAR(16) | YES | |
|
||||
| `last_test_http_code` | SMALLINT UNSIGNED | YES | |
|
||||
| `last_test_message` | VARCHAR(255) | YES | |
|
||||
| `last_test_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(type, name)`
|
||||
|
||||
**integration_test_logs** — API test results log
|
||||
|
||||
**allegro_integration_settings** — Allegro OAuth tokens and API config
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK → integrations(id) CASCADE |
|
||||
| `environment` | VARCHAR(16) | NO | DEFAULT 'sandbox' |
|
||||
| `client_id` | VARCHAR(128) | YES | |
|
||||
| `client_secret_encrypted` | TEXT | YES | |
|
||||
| `redirect_uri` | VARCHAR(255) | YES | |
|
||||
| `orders_fetch_enabled` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `orders_fetch_start_date` | DATE | YES | |
|
||||
| `access_token_encrypted` | MEDIUMTEXT | YES | AES-encrypted |
|
||||
| `refresh_token_encrypted` | MEDIUMTEXT | YES | AES-encrypted |
|
||||
| `token_type` | VARCHAR(32) | YES | |
|
||||
| `token_scope` | VARCHAR(255) | YES | |
|
||||
| `token_expires_at` | DATETIME | YES | |
|
||||
| `connected_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**allegro_order_status_mappings** — Allegro status → internal status
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | INT UNSIGNED | PK |
|
||||
| `allegro_status_code` | VARCHAR(64) | UNIQUE |
|
||||
| `allegro_status_name` | VARCHAR(120) | |
|
||||
| `orderpro_status_code` | VARCHAR(64) | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
**allegro_delivery_method_mappings** — Map order delivery method strings to Allegro services
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | INT UNSIGNED | PK |
|
||||
| `order_delivery_method` | VARCHAR(200) | UNIQUE |
|
||||
| `allegro_delivery_method_id` | VARCHAR(128) | |
|
||||
| `allegro_credentials_id` | VARCHAR(128) | |
|
||||
| `allegro_carrier_id` | VARCHAR(128) | |
|
||||
| `allegro_service_name` | VARCHAR(255) | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
**apaczka_integration_settings** — Apaczka API credentials
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | TINYINT UNSIGNED | PK (fixed 1 row) |
|
||||
| `integration_id` | INT UNSIGNED | UNIQUE, FK → integrations(id) |
|
||||
| `api_key_encrypted` | TEXT | |
|
||||
| `created_at` | DATETIME | |
|
||||
| `updated_at` | DATETIME | |
|
||||
|
||||
**inpost_integration_settings** — InPost ShipX settings
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | TINYINT UNSIGNED | NO | PK (fixed 1 row) |
|
||||
| `integration_id` | INT UNSIGNED | YES | UNIQUE, FK → integrations(id) |
|
||||
| `api_token_encrypted` | TEXT | YES | |
|
||||
| `organization_id` | VARCHAR(50) | YES | |
|
||||
| `environment` | ENUM('sandbox','production') | NO | DEFAULT 'sandbox' |
|
||||
| `default_dispatch_method` | ENUM('pop','parcel_locker','courier') | NO | DEFAULT 'pop' |
|
||||
| `default_dispatch_point` | VARCHAR(50) | YES | |
|
||||
| `default_insurance` | DECIMAL(10,2) | YES | |
|
||||
| `default_locker_size` | ENUM('small','medium','large') | NO | DEFAULT 'small' |
|
||||
| `default_courier_length` | SMALLINT UNSIGNED | YES | DEFAULT 20 |
|
||||
| `default_courier_width` | SMALLINT UNSIGNED | YES | DEFAULT 15 |
|
||||
| `default_courier_height` | SMALLINT UNSIGNED | YES | DEFAULT 8 |
|
||||
| `label_format` | ENUM('Pdf','Zpl','Epl') | NO | DEFAULT 'Pdf' |
|
||||
| `weekend_delivery` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `auto_insurance_value` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `multi_parcel` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
---
|
||||
|
||||
## Accounting / Receipts
|
||||
|
||||
**receipt_configs** — Receipt generation configurations
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `number_format` | VARCHAR(64) | NO | DEFAULT 'PAR/%N/%M/%Y' |
|
||||
| `numbering_type` | ENUM('monthly','yearly') | NO | DEFAULT 'monthly' |
|
||||
| `is_named` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `sale_date_source` | ENUM('order_date','payment_date','issue_date') | NO | DEFAULT 'issue_date' |
|
||||
| `order_reference` | ENUM('none','orderpro','integration') | NO | DEFAULT 'none' |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**receipts** — Generated receipts / invoices
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `config_id` | INT UNSIGNED | NO | FK → receipt_configs(id) RESTRICT |
|
||||
| `receipt_number` | VARCHAR(64) | NO | UNIQUE |
|
||||
| `issue_date` | DATETIME | NO | |
|
||||
| `sale_date` | DATETIME | NO | |
|
||||
| `seller_data_json` | JSON | NO | Snapshot of company data at issue time |
|
||||
| `buyer_data_json` | JSON | YES | |
|
||||
| `items_json` | JSON | NO | |
|
||||
| `total_net` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
|
||||
| `total_gross` | DECIMAL(12,2) | NO | DEFAULT 0.00 |
|
||||
| `order_reference_value` | VARCHAR(128) | YES | |
|
||||
| `created_by` | INT UNSIGNED | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**receipt_number_counters** — Sequential numbering per config/period
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `config_id` | INT UNSIGNED | NO | FK → receipt_configs(id) CASCADE |
|
||||
| `year` | SMALLINT UNSIGNED | NO | |
|
||||
| `month` | TINYINT UNSIGNED | YES | NULL for yearly numbering |
|
||||
| `last_number` | INT UNSIGNED | NO | DEFAULT 0 |
|
||||
|
||||
UNIQUE: `(config_id, year, month)`
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
**email_mailboxes** — SMTP mailbox configurations
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `name` | VARCHAR(100) | NO | |
|
||||
| `smtp_host` | VARCHAR(255) | NO | |
|
||||
| `smtp_port` | SMALLINT UNSIGNED | NO | DEFAULT 587 |
|
||||
| `smtp_encryption` | ENUM('tls','ssl','none') | NO | DEFAULT 'tls' |
|
||||
| `smtp_username` | VARCHAR(255) | NO | |
|
||||
| `smtp_password_encrypted` | TEXT | NO | AES-encrypted |
|
||||
| `sender_email` | VARCHAR(255) | NO | |
|
||||
| `sender_name` | VARCHAR(200) | YES | |
|
||||
| `html_layout` | LONGTEXT | YES | Wrapper HTML for all emails from this mailbox |
|
||||
| `is_default` | TINYINT(1) | NO | DEFAULT 0 |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**email_templates** — Email message templates with variable placeholders
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `name` | VARCHAR(200) | NO | |
|
||||
| `subject` | VARCHAR(500) | NO | |
|
||||
| `body_html` | TEXT | NO | Supports `{{variable}}` placeholders |
|
||||
| `mailbox_id` | INT UNSIGNED | YES | FK → email_mailboxes(id) SET NULL |
|
||||
| `attachment1` | LONGTEXT | YES | |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
**email_logs** — Sent email history
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | BIGINT UNSIGNED | NO | PK |
|
||||
| `template_id` | INT UNSIGNED | YES | FK → email_templates(id) SET NULL |
|
||||
| `mailbox_id` | INT UNSIGNED | YES | FK → email_mailboxes(id) SET NULL |
|
||||
| `order_id` | INT UNSIGNED | YES | |
|
||||
| `recipient_email` | VARCHAR(255) | NO | |
|
||||
| `recipient_name` | VARCHAR(200) | YES | |
|
||||
| `subject` | VARCHAR(500) | NO | |
|
||||
| `body_html` | TEXT | NO | |
|
||||
| `attachments_json` | JSON | YES | |
|
||||
| `status` | ENUM('sent','failed','pending') | NO | DEFAULT 'pending' |
|
||||
| `error_message` | TEXT | YES | |
|
||||
| `sent_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
---
|
||||
|
||||
## Automation
|
||||
|
||||
**automation_rules** — Business rules for order event automation
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `event_type` | VARCHAR(64) | NO | e.g., 'order_status_changed', 'order_status_aged' |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
Index: `(event_type, is_active)`
|
||||
|
||||
**automation_conditions** — Conditions for automation rules
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
|
||||
| `condition_type` | VARCHAR(64) | NO | |
|
||||
| `condition_value` | JSON | NO | |
|
||||
| `sort_order` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
|
||||
|
||||
**automation_actions** — Actions executed when rules trigger
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
|
||||
| `action_type` | VARCHAR(64) | NO | e.g., 'send_email', 'update_status', 'create_receipt' |
|
||||
| `action_config` | JSON | NO | |
|
||||
| `sort_order` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
|
||||
|
||||
**automation_execution_logs** — Audit log for rule executions
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | BIGINT UNSIGNED | NO | PK |
|
||||
| `event_type` | VARCHAR(64) | NO | |
|
||||
| `rule_id` | INT UNSIGNED | YES | FK → automation_rules(id) SET NULL |
|
||||
| `rule_name` | VARCHAR(128) | NO | Snapshot at execution time |
|
||||
| `order_id` | INT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `execution_status` | VARCHAR(16) | NO | |
|
||||
| `result_message` | VARCHAR(500) | YES | |
|
||||
| `context_json` | JSON | YES | |
|
||||
| `executed_at` | DATETIME | NO | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**automation_email_once_deliveries** — Idempotency guard: email sent-once per rule+action+order (Phase 107)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | BIGINT UNSIGNED | NO | PK |
|
||||
| `rule_id` | INT UNSIGNED | NO | FK → automation_rules(id) CASCADE |
|
||||
| `action_id` | INT UNSIGNED | NO | FK → automation_actions(id) CASCADE |
|
||||
| `order_id` | BIGINT UNSIGNED | NO | FK → orders(id) CASCADE |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
UNIQUE: `(rule_id, action_id, order_id)`
|
||||
|
||||
---
|
||||
|
||||
## Print Queue
|
||||
|
||||
**print_api_keys** — API keys for remote print client authentication
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT | NO | PK |
|
||||
| `name` | VARCHAR(128) | NO | |
|
||||
| `key_hash` | VARCHAR(128) | NO | UNIQUE, SHA256 |
|
||||
| `key_prefix` | VARCHAR(8) | NO | Shown in UI for identification |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `last_used_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
|
||||
**print_jobs** — Print queue for remote label printing
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `order_id` | BIGINT UNSIGNED | NO | |
|
||||
| `package_id` | BIGINT UNSIGNED | NO | |
|
||||
| `label_path` | VARCHAR(255) | NO | |
|
||||
| `status` | ENUM('pending','printing','completed','failed') | NO | DEFAULT 'pending' |
|
||||
| `created_by` | INT UNSIGNED | NO | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `completed_at` | DATETIME | YES | |
|
||||
|
||||
---
|
||||
|
||||
## Cron & Scheduling
|
||||
|
||||
**cron_jobs** — Individual cron job queue entries
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `job_type` | VARCHAR(80) | NO | |
|
||||
| `status` | ENUM('pending','processing','completed','failed','cancelled') | NO | DEFAULT 'pending' |
|
||||
| `priority` | TINYINT UNSIGNED | NO | DEFAULT 100 |
|
||||
| `payload` | JSON | YES | |
|
||||
| `result` | JSON | YES | |
|
||||
| `attempts` | SMALLINT UNSIGNED | NO | DEFAULT 0 |
|
||||
| `max_attempts` | SMALLINT UNSIGNED | NO | DEFAULT 3 |
|
||||
| `last_error` | VARCHAR(500) | YES | |
|
||||
| `scheduled_at` | DATETIME | NO | |
|
||||
| `started_at` | DATETIME | YES | |
|
||||
| `completed_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
Index: `(status, priority, scheduled_at)`
|
||||
|
||||
**cron_schedules** — Recurring job definitions
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `job_type` | VARCHAR(80) | NO | UNIQUE |
|
||||
| `interval_seconds` | INT UNSIGNED | NO | |
|
||||
| `priority` | TINYINT UNSIGNED | NO | DEFAULT 100 |
|
||||
| `max_attempts` | SMALLINT UNSIGNED | NO | DEFAULT 3 |
|
||||
| `payload` | JSON | YES | |
|
||||
| `enabled` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `last_run_at` | DATETIME | YES | |
|
||||
| `next_run_at` | DATETIME | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
---
|
||||
|
||||
## Settings & Configuration
|
||||
|
||||
**app_settings** — Global key-value configuration store
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `setting_key` | VARCHAR(120) | NO | UNIQUE |
|
||||
| `setting_value` | TEXT | YES | |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
Default keys: `cron_run_on_web`, `cron_web_limit`, `gs1_api_login`, `gs1_prefix`, `products_sku_format`
|
||||
|
||||
**company_settings** — Single-record seller/company configuration (always id=1)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | TINYINT UNSIGNED | NO | PK, always 1 |
|
||||
| `company_name` | VARCHAR(200) | YES | |
|
||||
| `person_name` | VARCHAR(200) | YES | |
|
||||
| `street` | VARCHAR(200) | YES | |
|
||||
| `city` | VARCHAR(128) | YES | |
|
||||
| `postal_code` | VARCHAR(16) | YES | |
|
||||
| `country_code` | CHAR(2) | NO | DEFAULT 'PL' |
|
||||
| `phone` | VARCHAR(64) | YES | |
|
||||
| `email` | VARCHAR(128) | YES | |
|
||||
| `tax_number` | VARCHAR(64) | YES | NIP |
|
||||
| `bank_account` | VARCHAR(64) | YES | |
|
||||
| `bank_owner_name` | VARCHAR(200) | YES | |
|
||||
| `contact_person_first_name` | VARCHAR(100) | YES | |
|
||||
| `contact_person_last_name` | VARCHAR(100) | YES | |
|
||||
| `contact_person_phone` | VARCHAR(64) | YES | |
|
||||
| `contact_person_email` | VARCHAR(128) | YES | |
|
||||
| `default_package_length_cm` | DECIMAL(8,1) | NO | DEFAULT 25.0 |
|
||||
| `default_package_width_cm` | DECIMAL(8,1) | NO | DEFAULT 20.0 |
|
||||
| `default_package_height_cm` | DECIMAL(8,1) | NO | DEFAULT 8.0 |
|
||||
| `default_package_weight_kg` | DECIMAL(8,3) | NO | DEFAULT 1.000 |
|
||||
| `default_label_format` | VARCHAR(8) | NO | DEFAULT 'PDF' |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
---
|
||||
|
||||
## Design Generation
|
||||
|
||||
**project_mappings** — Map product name patterns to graphic generation scripts
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
| `id` | INT UNSIGNED | NO | PK |
|
||||
| `product_name_pattern` | VARCHAR(255) | NO | Pattern matched against order_items.name |
|
||||
| `script_name` | VARCHAR(255) | NO | Script filename in tools/generowanie/ |
|
||||
| `output_dir` | VARCHAR(500) | YES | |
|
||||
| `is_active` | TINYINT(1) | NO | DEFAULT 1 |
|
||||
| `created_at` | DATETIME | NO | |
|
||||
| `updated_at` | DATETIME | NO | |
|
||||
|
||||
---
|
||||
|
||||
## Schema Characteristics
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Engine | InnoDB (all tables) |
|
||||
| Charset | utf8mb4_unicode_ci |
|
||||
| Encrypted columns | `*_encrypted` suffix — AES via `IntegrationSecretCipher` |
|
||||
| Soft deletes | `products.deleted_at` only |
|
||||
| Audit via JSON | `payload_json` snapshots in orders, shipments, receipts |
|
||||
| Migrations | `database/migrations/YYYYMMDD_NNNNNN_description.sql` |
|
||||
| Deferred indexes | `idx_order_addresses_order_type`, `idx_shipment_packages_order_delivery` — apply at >50k orders |
|
||||
|
||||
## Reporting Usage
|
||||
|
||||
**Statistics Summary (`/statistics/summary`)** — no dedicated reporting tables.
|
||||
- Reads existing `orders` rows and groups by month using the same effective order date used by `/statistics/orders`.
|
||||
- Default summary history starts at April 2026 (`2026-04-01`), even if older rows exist.
|
||||
- Splits series by channel key: Allegro as one series and each shopPRO integration by `orders.integration_id`.
|
||||
- Uses `integrations.name` only for display labels when available.
|
||||
- Filters by selected status groups through `order_status_groups` and `order_statuses`.
|
||||
- Uses existing gross amount columns via `OrdersStatisticsRepository::grossAmountSql()`.
|
||||
- No schema migration was introduced for Phase 110.
|
||||
59
DOCS/TECH_CHANGELOG.md
Normal file
59
DOCS/TECH_CHANGELOG.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-04-28 - Phase 110 Plan 01: Statistics Summary
|
||||
|
||||
**Co zrobiono:**
|
||||
- `/statistics/summary` - nowy widok podsumowania w menu `Statystyki -> Podsumowanie`.
|
||||
- `OrdersStatisticsController::summary()` - buduje miesieczny view-model dla wykresow liczby i wartosci zamowien.
|
||||
- `OrdersStatisticsRepository::aggregateByMonth()` - agreguje istniejace zamowienia po miesiacu i kanale/integracji.
|
||||
- `public/assets/js/modules/statistics-summary-charts.js` - renderer dwoch interaktywnych wykresow liniowych oparty o Chart.js 4.4.8 CDN.
|
||||
- `resources/views/statistics/summary.php` - filtry zgodne z raportem dziennym, dwa wykresy obok siebie na desktopie oraz dwie tabele fallback pod nimi.
|
||||
- Domyslny poczatek historii ustawiony na `2026-04-01` (`04-2026`) mimo starszych danych.
|
||||
|
||||
**Dlaczego:**
|
||||
- Operator potrzebuje szybkiego trendu miesiecznego przed przejsciem do szczegolowych dziennych statystyk.
|
||||
- Wykresy uzywaja obecnych tabel `orders`, `integrations`, `order_status_groups` i `order_statuses`, wiec migracja DB nie jest potrzebna.
|
||||
- Seria `Razem` jest liczona z tych samych danych co serie integracji, co ulatwia sprawdzenie sum miesiecznych.
|
||||
|
||||
## 2026-04-28 - Phase 109 Plan 01: Checkbox Multiselect Filters
|
||||
|
||||
**Co zrobiono:**
|
||||
- `public/assets/js/modules/checkbox-multiselect.js` - nowy vanilla JS enhancer dla natywnych `<select multiple data-checkbox-multiselect>`.
|
||||
- `resources/views/layouts/app.php` - globalne podpiecie modulu z cache busting przez `filemtime()`.
|
||||
- `resources/views/statistics/orders.php` - filtry `channels[]` i `status_groups[]` oznaczone do progresywnego ulepszenia bez zmiany nazw pol formularza.
|
||||
- `resources/scss/app.scss` - kompaktowe style dropdownu z checkboxami i opcja "Wszystkie".
|
||||
|
||||
**Dlaczego:**
|
||||
- Natywne selecty multiple byly malo czytelne i zajmowaly za duzo miejsca w filtrach statystyk.
|
||||
- Zachowanie oryginalnego selecta w DOM utrzymuje obecny kontrakt GET i fallback bez JavaScript.
|
||||
- Brak zmian w schemacie DB i logice agregacji statystyk.
|
||||
|
||||
> Chronologiczny log zmian technicznych — co i dlaczego.
|
||||
|
||||
## 2026-04-27 — Phase 108 Plan 02: Automation Dropdowns z DB
|
||||
|
||||
**Co zrobiono:**
|
||||
- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy)
|
||||
- Dropdown statusów w warunku `shipment_status` i akcji `update_shipment_status` ładuje statusy z DB przez `DeliveryStatus::getAllOptions()`
|
||||
- Walidacja w `parseConditionValue()` i `parseActionConfig()` używa `DeliveryStatus::getAllStatuses()`
|
||||
- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition()` porównuje klucze bezpośrednio
|
||||
- `resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target (zamiast pierwszego z grupy)
|
||||
|
||||
**Dlaczego:**
|
||||
- Zamknięcie integracji z Plan 01 — operator dodaje status w `/settings/delivery-statuses` i jest on od razu dostępny w dropdownach automatyzacji bez deploymentu
|
||||
- Eliminacja kolizji semantycznej: stary klucz grupowy `picked_up` mapował na `delivered` (paczka odebrana przez klienta), nowy klucz DB `picked_up` to "Odebrana przez kuriera" (od nadawcy)
|
||||
- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`, oraz `picked_up`/`ready_for_pickup`/`cancelled` w starym znaczeniu) nie matchują — wymagają ręcznego odtworzenia z nowymi kluczami DB
|
||||
|
||||
## 2026-04-27 — Phase 108 Plan 01: Delivery Status Management
|
||||
|
||||
**Co zrobiono:**
|
||||
- Tabela `delivery_statuses` z seedem 11 statusów (migracja `20260427_000103`)
|
||||
- `DeliveryStatusRepository` — CRUD + per-request cache
|
||||
- `DeliveryStatus.php` — dynamiczne ładowanie statusów z DB (`setRepository()`)
|
||||
- Panel `/settings/delivery-statuses` z CRUD (zakładka "Statusy") i mapowaniem (zakładka "Mapowanie dostawy")
|
||||
- Sidebar: "Statusy" → "Statusy zamówień", nowe "Statusy przesyłek" z badge niezmapowanych
|
||||
- Badge przesyłek: inline CSS custom property `--status-color` dla niestandardowych statusów
|
||||
|
||||
**Dlaczego:**
|
||||
- Dodanie nowego statusu wymagało zmiany kodu + deploymentu; teraz z UI
|
||||
- Operator może definiować własne statusy znormalizowane bez ingerencji w kod
|
||||
File diff suppressed because one or more lines are too long
119
public/assets/js/modules/statistics-summary-charts.js
Normal file
119
public/assets/js/modules/statistics-summary-charts.js
Normal file
@@ -0,0 +1,119 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var palette = ['#2563eb', '#0f766e', '#c2410c', '#7c3aed', '#be123c', '#0369a1', '#4d7c0f', '#9333ea'];
|
||||
var totalColor = '#111827';
|
||||
|
||||
function parseData() {
|
||||
var node = document.getElementById('js-statistics-summary-data');
|
||||
if (!node) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(node.textContent || '{}');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function colorForSeries(item, index) {
|
||||
return item.key === 'total' ? totalColor : palette[index % palette.length];
|
||||
}
|
||||
|
||||
function moneyLabel(value) {
|
||||
return Number(value || 0).toLocaleString('pl-PL', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function datasetForSeries(item, index) {
|
||||
var color = colorForSeries(item, index);
|
||||
var isTotal = item.key === 'total';
|
||||
|
||||
return {
|
||||
label: item.label || item.key || '',
|
||||
data: item.values || [],
|
||||
borderColor: color,
|
||||
backgroundColor: color,
|
||||
borderWidth: isTotal ? 3 : 2,
|
||||
pointRadius: isTotal ? 4 : 3,
|
||||
pointHoverRadius: isTotal ? 6 : 5,
|
||||
tension: 0.25
|
||||
};
|
||||
}
|
||||
|
||||
function renderChart(container, chart) {
|
||||
if (!window.Chart || !container || !chart || !Array.isArray(chart.labels) || !Array.isArray(chart.series)) return;
|
||||
|
||||
var canvas = container.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
var isMoney = chart.valueType === 'money';
|
||||
var context = canvas.getContext('2d');
|
||||
|
||||
new window.Chart(context, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: chart.labels,
|
||||
datasets: chart.series.map(datasetForSeries)
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 18,
|
||||
boxHeight: 3,
|
||||
usePointStyle: false
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (item) {
|
||||
var label = item.dataset.label ? item.dataset.label + ': ' : '';
|
||||
return label + (isMoney ? moneyLabel(item.parsed.y) : Math.round(item.parsed.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return isMoney ? moneyLabel(value) : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var data = parseData();
|
||||
if (!data) return;
|
||||
|
||||
document.querySelectorAll('[data-statistics-chart]').forEach(function (container) {
|
||||
var key = container.getAttribute('data-statistics-chart');
|
||||
renderChart(container, data[key]);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
'orders' => 'Zamowienia',
|
||||
'orders_list' => 'Lista zamowien',
|
||||
'statistics' => 'Statystyki',
|
||||
'statistics_summary' => 'Podsumowanie',
|
||||
'statistics_orders' => 'Zamowienia',
|
||||
'marketplace' => 'Marketplace',
|
||||
'cron' => 'Harmonogram',
|
||||
@@ -221,6 +222,29 @@ return [
|
||||
],
|
||||
],
|
||||
'statistics' => [
|
||||
'summary' => [
|
||||
'title' => 'Podsumowanie statystyk',
|
||||
'description' => 'Miesieczne trendy ilosci i wartosci zamowien z podzialem na integracje.',
|
||||
'empty' => 'Brak danych dla wybranych filtrow.',
|
||||
'charts' => [
|
||||
'orders_count' => 'Ilosc zamowien miesiecznie',
|
||||
'orders_value' => 'Wartosc zamowien miesiecznie',
|
||||
],
|
||||
'filters' => [
|
||||
'date_from' => 'Data od',
|
||||
'date_to' => 'Data do',
|
||||
'channels' => 'Kanaly sprzedazy',
|
||||
'status_groups' => 'Grupy statusow',
|
||||
],
|
||||
'columns' => [
|
||||
'month' => 'Miesiac',
|
||||
'total' => 'Razem',
|
||||
],
|
||||
'actions' => [
|
||||
'apply_filters' => 'Filtruj',
|
||||
'reset_filters' => 'Wyczysc',
|
||||
],
|
||||
],
|
||||
'orders' => [
|
||||
'title' => 'Statystyki zamowien',
|
||||
'description' => 'Dzienne podsumowanie ilosci i kwot zamowien z podzialem na kanaly sprzedazy.',
|
||||
|
||||
@@ -1219,6 +1219,74 @@ h4.section-title {
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-summary-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.statistics-summary-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.statistics-summary-chart-grid,
|
||||
.statistics-summary-table-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.statistics-summary-chart-grid,
|
||||
.statistics-summary-table-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-summary-card {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.statistics-summary-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statistics-summary-chart {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.statistics-summary-chart canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.statistics-summary-fallback {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.statistics-summary-table {
|
||||
min-width: 640px;
|
||||
|
||||
th,
|
||||
td {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.orders-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="sidebar__group-links">
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'statistics' && $currentStatistics === 'summary' ? ' is-active' : '' ?>" href="/statistics/summary">
|
||||
<?= $e($t('navigation.statistics_summary')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'statistics' && $currentStatistics === 'orders' ? ' is-active' : '' ?>" href="/statistics/orders">
|
||||
<?= $e($t('navigation.statistics_orders')) ?>
|
||||
</a>
|
||||
@@ -188,6 +191,8 @@
|
||||
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/global-search.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/global-search.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/checkbox-multiselect.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/checkbox-multiselect.js') ?: 0 ?>"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<script src="/assets/js/modules/statistics-summary-charts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/statistics-summary-charts.js') ?: 0 ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var STORAGE_KEY = 'sidebarCollapsed';
|
||||
|
||||
208
resources/views/statistics/summary.php
Normal file
208
resources/views/statistics/summary.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$channelOptions = is_array($channelOptions ?? null) ? $channelOptions : [];
|
||||
$statusGroupOptions = is_array($statusGroupOptions ?? null) ? $statusGroupOptions : [];
|
||||
$summary = is_array($summary ?? null) ? $summary : [];
|
||||
|
||||
$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($summary['rows'] ?? null) ? $summary['rows'] : [];
|
||||
$hasData = (bool) ($summary['hasData'] ?? false);
|
||||
|
||||
$countChart = is_array($summary['countChart'] ?? null) ? $summary['countChart'] : ['labels' => [], 'series' => []];
|
||||
$valueChart = is_array($summary['valueChart'] ?? null) ? $summary['valueChart'] : ['labels' => [], 'series' => []];
|
||||
$chartPayload = [
|
||||
'count' => $countChart,
|
||||
'value' => $valueChart,
|
||||
];
|
||||
|
||||
$chartJson = json_encode(
|
||||
$chartPayload,
|
||||
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
);
|
||||
if (!is_string($chartJson)) {
|
||||
$chartJson = '{}';
|
||||
}
|
||||
|
||||
$displayMonth = static function (string $month): string {
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d', $month . '-01');
|
||||
if (!$date instanceof DateTimeImmutable) {
|
||||
return $month;
|
||||
}
|
||||
|
||||
return $date->format('m-Y');
|
||||
};
|
||||
?>
|
||||
|
||||
<section class="card statistics-summary-page">
|
||||
<div class="statistics-orders-head">
|
||||
<div>
|
||||
<h2 class="section-title"><?= $e($t('statistics.summary.title')) ?></h2>
|
||||
<p class="muted mt-12"><?= $e($t('statistics.summary.description')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/statistics/summary" class="statistics-orders-filters mt-16">
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('statistics.summary.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.summary.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.summary.filters.channels')) ?></span>
|
||||
<select class="form-control statistics-orders-multiselect js-checkbox-multiselect"
|
||||
name="channels[]"
|
||||
multiple
|
||||
size="6"
|
||||
data-checkbox-multiselect
|
||||
data-all-label="Wszystkie"
|
||||
data-empty-label="Nic nie wybrano"
|
||||
data-selected-label-singular="zaznaczono"
|
||||
data-selected-label-plural="zaznaczono">
|
||||
<?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.summary.filters.status_groups')) ?></span>
|
||||
<select class="form-control statistics-orders-multiselect js-checkbox-multiselect"
|
||||
name="status_groups[]"
|
||||
multiple
|
||||
size="6"
|
||||
data-checkbox-multiselect
|
||||
data-all-label="Wszystkie"
|
||||
data-empty-label="Nic nie wybrano"
|
||||
data-selected-label-singular="zaznaczono"
|
||||
data-selected-label-plural="zaznaczono">
|
||||
<?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.summary.actions.apply_filters')) ?></button>
|
||||
<a href="/statistics/summary" class="btn btn--secondary"><?= $e($t('statistics.summary.actions.reset_filters')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="statistics-summary-section mt-16">
|
||||
<?php if (!$hasData): ?>
|
||||
<div class="card statistics-summary-card">
|
||||
<p class="muted"><?= $e($t('statistics.summary.empty')) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="statistics-summary-chart-grid">
|
||||
<article class="card statistics-summary-card">
|
||||
<div class="statistics-summary-card__head">
|
||||
<h3 class="section-title"><?= $e($t('statistics.summary.charts.orders_count')) ?></h3>
|
||||
</div>
|
||||
<div class="statistics-summary-chart" data-statistics-chart="count">
|
||||
<canvas aria-label="<?= $e($t('statistics.summary.charts.orders_count')) ?>"></canvas>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card statistics-summary-card">
|
||||
<div class="statistics-summary-card__head">
|
||||
<h3 class="section-title"><?= $e($t('statistics.summary.charts.orders_value')) ?></h3>
|
||||
</div>
|
||||
<div class="statistics-summary-chart" data-statistics-chart="value">
|
||||
<canvas aria-label="<?= $e($t('statistics.summary.charts.orders_value')) ?>"></canvas>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="statistics-summary-table-grid mt-16">
|
||||
<article class="card statistics-summary-card">
|
||||
<div class="statistics-summary-card__head">
|
||||
<h3 class="section-title"><?= $e($t('statistics.summary.charts.orders_count')) ?></h3>
|
||||
</div>
|
||||
<div class="table-wrap statistics-summary-fallback">
|
||||
<table class="table statistics-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('statistics.summary.columns.month')) ?></th>
|
||||
<?php foreach ($countChart['series'] ?? [] as $series): ?>
|
||||
<th><?= $e((string) ($series['label'] ?? '')) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $rowIndex => $row): ?>
|
||||
<tr>
|
||||
<td><?= $e($displayMonth((string) ($row['month'] ?? ''))) ?></td>
|
||||
<?php foreach ($countChart['series'] ?? [] as $series): ?>
|
||||
<?php $values = is_array($series['values'] ?? null) ? $series['values'] : []; ?>
|
||||
<td><?= $e((string) ((int) ($values[$rowIndex] ?? 0))) ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card statistics-summary-card">
|
||||
<div class="statistics-summary-card__head">
|
||||
<h3 class="section-title"><?= $e($t('statistics.summary.charts.orders_value')) ?></h3>
|
||||
</div>
|
||||
<div class="table-wrap statistics-summary-fallback">
|
||||
<table class="table statistics-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('statistics.summary.columns.month')) ?></th>
|
||||
<?php foreach ($valueChart['series'] ?? [] as $series): ?>
|
||||
<th><?= $e((string) ($series['label'] ?? '')) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $rowIndex => $row): ?>
|
||||
<tr>
|
||||
<td><?= $e($displayMonth((string) ($row['month'] ?? ''))) ?></td>
|
||||
<?php foreach ($valueChart['series'] ?? [] as $series): ?>
|
||||
<?php $values = is_array($series['values'] ?? null) ? $series['values'] : []; ?>
|
||||
<td><?= $e(number_format((float) ($values[$rowIndex] ?? 0), 2, '.', ' ')) ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<script type="application/json" id="js-statistics-summary-data"><?= $chartJson ?></script>
|
||||
@@ -442,6 +442,7 @@ return static function (Application $app): void {
|
||||
$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/list', [$ordersController, 'index'], [$authMiddleware]);
|
||||
$router->get('/statistics/summary', [$ordersStatisticsController, 'summary'], [$authMiddleware]);
|
||||
$router->get('/statistics/orders', [$ordersStatisticsController, 'index'], [$authMiddleware]);
|
||||
$router->get('/orders/{id}', [$ordersController, 'show'], [$authMiddleware]);
|
||||
$router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]);
|
||||
|
||||
@@ -70,6 +70,41 @@ final class OrdersStatisticsController
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function summary(Request $request): Response
|
||||
{
|
||||
[$dateFrom, $dateTo] = $this->resolveSummaryDateRange($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->aggregateByMonth($dateFrom, $dateTo, $selectedChannels, $statusCodes);
|
||||
$summary = $this->buildSummary($dateFrom, $dateTo, $selectedChannels, $channelOptions, $aggregated);
|
||||
|
||||
$html = $this->template->render('statistics/summary', [
|
||||
'title' => $this->translator->get('statistics.summary.title'),
|
||||
'activeMenu' => 'statistics',
|
||||
'activeStatistics' => 'summary',
|
||||
'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,
|
||||
'summary' => $summary,
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
@@ -95,6 +130,31 @@ final class OrdersStatisticsController
|
||||
return [$dateFrom, $dateTo];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
private function resolveSummaryDateRange(Request $request): array
|
||||
{
|
||||
$now = new DateTimeImmutable('now');
|
||||
$defaultFrom = '2026-04-01';
|
||||
$defaultTo = $now->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) {
|
||||
@@ -298,6 +358,200 @@ final class OrdersStatisticsController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $selectedChannels
|
||||
* @param array<int, array{key:string,label:string}> $channelOptions
|
||||
* @param array<int, array{month:string,channel_key:string,orders_count:int,total_gross:float}> $aggregated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildSummary(
|
||||
string $dateFrom,
|
||||
string $dateTo,
|
||||
array $selectedChannels,
|
||||
array $channelOptions,
|
||||
array $aggregated
|
||||
): array {
|
||||
$channelLabels = $this->channelLabels($channelOptions);
|
||||
$rows = $this->emptySummaryRows($dateFrom, $dateTo, $selectedChannels);
|
||||
|
||||
foreach ($aggregated as $item) {
|
||||
$month = (string) ($item['month'] ?? '');
|
||||
$channelKey = (string) ($item['channel_key'] ?? '');
|
||||
if ($month === '' || $channelKey === '' || !isset($rows[$month]['channels'][$channelKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ordersCount = (int) ($item['orders_count'] ?? 0);
|
||||
$totalGross = (float) ($item['total_gross'] ?? 0);
|
||||
$rows[$month]['channels'][$channelKey] = [
|
||||
'orders_count' => $ordersCount,
|
||||
'total_gross' => $totalGross,
|
||||
];
|
||||
$rows[$month]['total_orders_count'] += $ordersCount;
|
||||
$rows[$month]['total_gross'] += $totalGross;
|
||||
}
|
||||
|
||||
return $this->summaryPayload($rows, $selectedChannels, $channelLabels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{key:string,label:string}> $channelOptions
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function channelLabels(array $channelOptions): array
|
||||
{
|
||||
$labels = [];
|
||||
foreach ($channelOptions as $option) {
|
||||
$key = (string) ($option['key'] ?? '');
|
||||
if ($key !== '') {
|
||||
$labels[$key] = (string) ($option['label'] ?? $key);
|
||||
}
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $selectedChannels
|
||||
* @return array<string, array{month:string,channels:array<string,array{orders_count:int,total_gross:float}>,total_orders_count:int,total_gross:float}>
|
||||
*/
|
||||
private function emptySummaryRows(string $dateFrom, string $dateTo, array $selectedChannels): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($this->monthRange($dateFrom, $dateTo) as $month) {
|
||||
$rows[$month] = [
|
||||
'month' => $month,
|
||||
'channels' => $this->emptySummaryChannels($selectedChannels),
|
||||
'total_orders_count' => 0,
|
||||
'total_gross' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $selectedChannels
|
||||
* @return array<string, array{orders_count:int,total_gross:float}>
|
||||
*/
|
||||
private function emptySummaryChannels(array $selectedChannels): array
|
||||
{
|
||||
$channels = [];
|
||||
foreach ($selectedChannels as $channelKey) {
|
||||
$channels[$channelKey] = [
|
||||
'orders_count' => 0,
|
||||
'total_gross' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{month:string,channels:array<string,array{orders_count:int,total_gross:float}>,total_orders_count:int,total_gross:float}> $rows
|
||||
* @param array<int, string> $selectedChannels
|
||||
* @param array<string, string> $channelLabels
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function summaryPayload(array $rows, array $selectedChannels, array $channelLabels): array
|
||||
{
|
||||
$months = array_keys($rows);
|
||||
$labels = array_map(fn (string $month): string => $this->displayMonth($month), $months);
|
||||
$countSeries = $this->summarySeries($rows, $selectedChannels, $channelLabels, 'orders_count');
|
||||
$valueSeries = $this->summarySeries($rows, $selectedChannels, $channelLabels, 'total_gross');
|
||||
|
||||
$countSeries[] = $this->totalSeries($rows, 'Razem', 'total_orders_count');
|
||||
$valueSeries[] = $this->totalSeries($rows, 'Razem', 'total_gross');
|
||||
|
||||
return [
|
||||
'months' => $months,
|
||||
'rows' => array_values($rows),
|
||||
'hasData' => $this->summaryHasData($rows),
|
||||
'countChart' => [
|
||||
'labels' => $labels,
|
||||
'series' => $countSeries,
|
||||
'valueType' => 'number',
|
||||
],
|
||||
'valueChart' => [
|
||||
'labels' => $labels,
|
||||
'series' => $valueSeries,
|
||||
'valueType' => 'money',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function displayMonth(string $month): string
|
||||
{
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d', $month . '-01');
|
||||
if (!$date instanceof DateTimeImmutable) {
|
||||
return $month;
|
||||
}
|
||||
|
||||
return $date->format('m-Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{channels:array<string,array{orders_count:int,total_gross:float}>}> $rows
|
||||
* @param array<int, string> $selectedChannels
|
||||
* @param array<string, string> $channelLabels
|
||||
* @return array<int, array{key:string,label:string,values:array<int,int|float>}>
|
||||
*/
|
||||
private function summarySeries(array $rows, array $selectedChannels, array $channelLabels, string $metric): array
|
||||
{
|
||||
$series = [];
|
||||
foreach ($selectedChannels as $channelKey) {
|
||||
$values = [];
|
||||
foreach ($rows as $row) {
|
||||
$channelStats = $row['channels'][$channelKey] ?? [];
|
||||
$values[] = $metric === 'orders_count'
|
||||
? (int) ($channelStats[$metric] ?? 0)
|
||||
: (float) ($channelStats[$metric] ?? 0);
|
||||
}
|
||||
|
||||
$series[] = [
|
||||
'key' => $channelKey,
|
||||
'label' => $channelLabels[$channelKey] ?? $channelKey,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $rows
|
||||
* @return array{key:string,label:string,values:array<int,int|float>}
|
||||
*/
|
||||
private function totalSeries(array $rows, string $label, string $metric): array
|
||||
{
|
||||
$values = [];
|
||||
foreach ($rows as $row) {
|
||||
$values[] = $metric === 'total_orders_count'
|
||||
? (int) ($row[$metric] ?? 0)
|
||||
: (float) ($row[$metric] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
'key' => 'total',
|
||||
'label' => $label,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{total_orders_count:int}> $rows
|
||||
*/
|
||||
private function summaryHasData(array $rows): bool
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
if ((int) ($row['total_orders_count'] ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $channelKeys
|
||||
* @return array<string, array{orders_count:int,total_net:float,total_gross:float}>
|
||||
@@ -333,6 +587,23 @@ final class OrdersStatisticsController
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function monthRange(string $dateFrom, string $dateTo): array
|
||||
{
|
||||
$start = (new DateTimeImmutable($dateFrom))->modify('first day of this month');
|
||||
$end = (new DateTimeImmutable($dateTo))->modify('first day of next month');
|
||||
$period = new DatePeriod($start, new DateInterval('P1M'), $end);
|
||||
|
||||
$months = [];
|
||||
foreach ($period as $date) {
|
||||
$months[] = $date->format('Y-m');
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
|
||||
@@ -238,6 +238,95 @@ final class OrdersStatisticsRepository
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $channels
|
||||
* @param array<int, string> $statusCodes
|
||||
* @return array<int, array{month:string,channel_key:string,orders_count:int,total_gross:float}>
|
||||
*/
|
||||
public function aggregateByMonth(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');
|
||||
$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);
|
||||
}
|
||||
|
||||
$monthSql = 'DATE_FORMAT(' . $effectiveDateSql . ', "%Y-%m")';
|
||||
$sql = 'SELECT
|
||||
' . $monthSql . ' AS month,
|
||||
' . $channelSql . ' AS channel_key,
|
||||
COUNT(*) AS orders_count,
|
||||
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 ' . $monthSql . ', ' . $channelSql . '
|
||||
ORDER BY ' . $monthSql . ' 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) {
|
||||
$month = trim((string) ($row['month'] ?? ''));
|
||||
$channelKey = trim((string) ($row['channel_key'] ?? ''));
|
||||
if ($month === '' || $channelKey === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'month' => $month,
|
||||
'channel_key' => $channelKey,
|
||||
'orders_count' => (int) ($row['orders_count'] ?? 0),
|
||||
'total_gross' => (float) ($row['total_gross'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $channels
|
||||
* @param array<int, string> $statusCodes
|
||||
|
||||
Reference in New Issue
Block a user