feat(120): alert component unification

Phase 120 - Plan 01:
- Reusable PHP alert component (resources/views/components/alert.php)
  with inline SVG icon per type, optional dismiss button.
- Missing .alert--info SCSS variant added (#eff6ff/#bfdbfe/#1e3a8a) -
  fixes black-on-white alert after Fakturownia test connection.
- Flash::push(type, message) + Flash::all() with BC for set/get;
  legacy key heuristic (error/.save/warning -> typed entries).
- Central flash renderer in 3 layouts (app/auth/public) iterating
  Flash::all() through component (.alerts-stack wrap).
- Vanilla JS alert-dismiss.js with idempotent guard and delegated
  click handler on [data-alert-dismiss].
- 36 views migrated off inline <div class="alert alert--TYPE">;
  .flash--error/success removed from views (orders/show, shipments/prepare).
- SCSS rebuilt: public/assets/css/{app,login}.css.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 18:47:41 +02:00
parent 3a2c419c25
commit 933dfcc67b
51 changed files with 1109 additions and 210 deletions

View File

@@ -65,7 +65,7 @@ HTTP Request
### Order Lifecycle
1. **Import** — Cron handler → API client → `OrderImportService``OrdersRepository::insertOrder()``AutomationService::executeForNewOrder()`
2. **Re-import (Phase 111 + 112)**`OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). **Phase 112-01 (delta-only re-import):** przy `created=false` repo nie wywoluje `replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory``order_items.id` i flagi lokalne (np. `project_generated` z Phase 97) pozostaja stabilne. `updateOrderDelta()` aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Anulowanie ze zrodla (`is_canceled_by_buyer=1` lub zmapowany pull `status_code='anulowane'`) nadpisuje preservacje statusu. Identical-payload guard (`normalizePayloadJson`) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji.
2. **Re-import (Phase 111 + 112 + 119)**`OrderImportRepository::upsertOrderAggregate` wykrywa tranzycje `payment_status` z 0/1 na 2 i zwraca `payment_transition=true`. `AllegroOrderImportService` i `ShopproOrdersSyncService` na tej fladze emituja `payment.status_changed`, co przez chain reguly automatyzacji #7 zmienia `status_code` na `w_realizacji`. Logika preservacji `status_code` z Phase 62 pozostaje rozdzielona (`statusOverwriteAllowed` = `currentStatus='nieoplacone' && newPaymentStatus===2`). **Phase 112-01 (delta-only re-import):** przy `created=false` repo nie wywoluje `replaceAddresses/replaceItems/replaceNotes/replaceShipments/replaceStatusHistory``order_items.id` i flagi lokalne (np. `project_generated` z Phase 97) pozostaja stabilne. `updateOrderDelta()` aktualizuje wylacznie `status_code` (warunkowo, z propagacja anulowania), `payment_status`, `total_paid`, `is_canceled_by_buyer`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`. Anulowanie ze zrodla (`is_canceled_by_buyer=1` lub zmapowany pull `status_code='anulowane'`) nadpisuje preservacje statusu. Identical-payload guard (`normalizePayloadJson`) pomija UPDATE gdy znormalizowany payload nie rozni sie od DB i brak innych tranzycji. **Phase 119-01 (total_paid protection):** gdy `paymentStatusUnchanged=true` (`oldPaymentStatus === newPaymentStatus`), `updateOrderDelta()` nie dolacza `total_paid` do UPDATE — chroni reczne korekty kwoty (np. zwroty czesciowe). `is_canceled_by_buyer` jest pomijane analogicznie, chyba ze `cancelledBySource=true` (cancel propagation ze zrodla zawsze wymusza wpis flagi). Pozostale pola (`status_code`, `payment_status`, `source_updated_at`, `payload_json`, `fetched_at`, `updated_at`) zachowuja niezmieniony kontrakt z Phase 112-01.
3. **Status update**`OrdersController::updateStatus()``OrdersRepository::updateStatus()` → automation check
4. **Status sync** — Cron → `AllegroStatusSyncService` / `ShopproStatusSyncService` → carrier API
@@ -203,6 +203,28 @@ tests/
### IntegrationsHubController
- Nowy parametr konstruktora `FakturowniaIntegrationRepository $fakturownia` i nowa metoda `buildFakturowniaRow()` agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
## Phase 118 — Fakturownia Single Instance
### FakturowniaIntegrationRepository
- Zarzadza jedna globalna konfiguracja `fakturownia_integration_settings.id=1` i jednym rekordem `integrations.type='fakturownia'`.
- `getSettings()` zasila formularz i hub integracji; `saveSettings()` zapisuje prefix, token, department/defaults i aktywnosc.
- `getIntegrationId()` jest zrodlem prawdy dla delegowanych `invoice_configs.integration_id`.
- `findAll()` zostaje kompatybilnym wrapperem zwracajacym liste z jednym elementem.
### FakturowniaIntegrationController + UI
- `/settings/integrations/fakturownia` pokazuje jeden formularz i test polaczenia.
- Legacy `/new` i `/edit` przekierowuja do globalnej konfiguracji; delete nie jest oferowany w UI.
- Hub integracji pokazuje jedna instancje Fakturowni, bez licznika kont.
### Invoice Config Delegation
- `InvoiceConfigRepository::save()` przy `is_delegated=1` ignoruje wieloinstancyjny wybor i ustawia globalny Fakturownia integration id.
- UI konfiguracji faktury pokazuje status globalnej konfiguracji zamiast selecta kont.
- `invoice_configs.integration_id` zostaje dla kompatybilnosci z `InvoiceService` i istniejaca historia faktur.
### Migration 20260512_000109
- Wybiera aktywna instancje Fakturowni; fallback: uzywana w `invoice_configs`, potem najnizsze id.
- Przepina delegowane `invoice_configs.integration_id` na zachowany rekord i usuwa nadmiarowe rekordy Fakturowni po przepieciu zaleznosci.
## Phase 115 — Wystawianie faktury z zamowienia
### InvoiceService (`src/Modules/Accounting/InvoiceService.php`)
@@ -302,6 +324,38 @@ tests/
### IntegrationsHubController
- Dodaje wiersz SMSPLANET do `/settings/integrations` ze statusem konfiguracji, sekretu, aktywnosci i ostatniego testu.
## Phase 120 — Alert Component Unification
### Alert component (`resources/views/components/alert.php`)
- Reusable alert renderer with params: `$type` (info|success|warning|danger; fallback 'info'), `$message` (escaped) lub `$messageHtml` (trusted), `$dismissible` (default true), `$role` ('alert'|'status').
- Renders inline SVG icon per type + body + optional dismiss button. Markup: `<div class="alert alert--TYPE" data-alert>...<button data-alert-dismiss>...</button></div>`.
- Used via `include __DIR__ . '/../components/alert.php'` po ustawieniu lokalnych `$type/$message/$dismissible`.
### SCSS — `.alert` w `resources/scss/shared/_ui-components.scss`
- `.alert` jest teraz flex (icon + body + dismiss). Dodane: `.alert__icon`, `.alert__body`, `.alert__dismiss`.
- Nowy wariant `.alert--info` (blue: border #bfdbfe, bg #eff6ff, color #1e3a8a) — wczesniej brakowal i renderowal sie jako czarny tekst na bialym tle.
- Wariantow `--success/--warning/--danger` nie zmieniono kolorystycznie.
- Wrapper `.alerts-stack` (gap 8px) do stackowania wielu alertow z layoutu.
### JS — `public/assets/js/modules/alert-dismiss.js`
- Vanilla JS, idempotent guard (`window.__alertDismissBound`).
- Delegated click handler na `[data-alert-dismiss]` — usuwa najblizszy `[data-alert]` z DOM bez przeladowania.
- Ladowany globalnie w `layouts/app.php`, `layouts/auth.php`, `layouts/public.php`.
### Flash — `App\Core\Support\Flash` rozszerzenie
- Nowa kolejka typowana `$_SESSION['_flash_queue']` z entries `{type, message}`.
- `Flash::push(string $type, string $message): void` — append do kolejki (whitelist info/success/warning/danger, fallback info).
- `Flash::all(): array` — zwraca i czysci kolejke + skanuje legacy `_flash` (heurystyka klucza: `error/fail/danger` → danger, `warning` → warning, `success/.save/.created/.deleted/.toggled` → success, reszta → info). BC zachowany: `Flash::set/get` dziala bez zmian.
### Centralny renderer flash w layoutach
- `layouts/app.php`, `layouts/auth.php`, `layouts/public.php` na poczatku glownego content area iteruja `Flash::all()` i wlaczaja komponent `alert.php` per wpis (wrap `.alerts-stack`).
- Kontrolery NIE wymagaly zmian — pre-fetched `Flash::get('module.key', '')` przekazany do widoku jako lokalna zmienna jest dalej renderowany inline przez widok (przez ten sam komponent). Centralny renderer przejmuje wpisy `Flash::push(...)` oraz nieskonsumowane legacy entries.
### Migracja widokow
- Wszystkie inline `<div class="alert alert--TYPE">...</div>` w widokach (36 plikow razem ze `shipments/prepare.php` i `orders/show.php`) zastapione przez `<?php $type=...; $message=...; $dismissible=...; include dirname(__DIR__) . '/components/alert.php'; ?>`.
- `.flash--error` / `.flash--success` w `orders/show.php` i `shipments/prepare.php` zastapione komponentem (klasa `.flash--*` w SCSS pozostaje bez uzycia, deferred cleanup).
- Wyjatek: `settings/email-mailboxes.php` ma JS-generowane alerty (`resultDiv.className = 'mt-12 alert alert--success'`) z dynamicznej odpowiedzi AJAX test polaczenia SMTP — uzywaja klas SCSS bez markupu komponentu (out of scope dla tej fazy).
## Phase 114 — Accounting Configs Refactor
### Sekcja Ksiegowosc — struktura URL

View File

@@ -1,5 +1,53 @@
# Technical Changelog
## 2026-05-12 - Phase 120 Plan 01: Alert Component Unification
**Co zrobiono:**
- Dodano komponent `resources/views/components/alert.php` (params: `$type` info|success|warning|danger, `$message`/`$messageHtml`, `$dismissible`, `$role`) renderujacy `.alert` z inline SVG ikona, body i opcjonalnym przyciskiem dismiss.
- SCSS `_ui-components.scss`: `.alert` zmieniony na flex z `.alert__icon`/`.alert__body`/`.alert__dismiss`; dodany brakujacy wariant `.alert--info` (#eff6ff/#bfdbfe/#1e3a8a); dodany wrapper `.alerts-stack`.
- Vanilla JS `public/assets/js/modules/alert-dismiss.js` z idempotent guardem i delegated handlerem na `[data-alert-dismiss]`.
- `Flash` rozszerzony o `push(string $type, string $message): void` i `all(): array` z BC dla `set/get``all()` konsumuje typowana kolejke + skanuje legacy `_flash` z heurystyka klucza (error/danger/fail → danger, warning → warning, success/.save/.created/.deleted/.toggled → success, reszta → info).
- Layouty `app.php`, `auth.php`, `public.php` na poczatku content area iteruja `Flash::all()` przez komponent w `.alerts-stack` i ladja `alert-dismiss.js`.
- Migracja 36 widokow (34 z planu + `orders/show.php` + `shipments/prepare.php`): inline `<div class="alert alert--TYPE">` zastapione `include components/alert.php`; `.flash--error`/`.flash--success` w starych widokach takze przeniesione na komponent.
**Dlaczego:**
- Po teste polaczenia Fakturowni alert `OK (HTTP 200)` mial klase `alert--info`, ale `.alert--info` nie istnial — efekt: czarny tekst na bialym tle bez ikony. Zglosenie operatora po Phase 118.
- Wiele wzorcow alertow w 36 widokach roznilo sie szczegolami (mt-12/role/struktura), brak komponentu wielokrotnego uzytku. Po Phase 120 jeden plik (`components/alert.php`) jest jedynym renderem markupu alert + centralny renderer flash w 3 layoutach przyjmuje przyszle `Flash::push()` bez koniecznosci powtarzania kodu w widokach.
**Boundaries (zachowane):**
- Kontrolery NIE zmieniane — `Flash::set/get` BC; wzorce `Flash::set('module.key', '...')` + view local `$flashXxx` dzialaja jak dotychczas (widok renderuje przez komponent).
- Modul `resources/modules/jquery-alerts` (dialogi `OrderProAlerts`) niezmieniany — osobny system.
- `email-mailboxes.php` JS-generowane alerty AJAX testu SMTP — pozostawione bez zmian (uzywaja `.alert` SCSS, brak markupu komponentu — out of scope).
- `.flash--*` SCSS nie usuniety (deferred cleanup) — widoki juz go nie uzywaja.
- Build SCSS → CSS poza zakresem; `public/assets/css/app.css` musi byc zregenerowany lokalnie po wdrozeniu.
## 2026-05-12 - Phase 119 Plan 01: Re-import total_paid Protection
**Co zrobiono:**
- `OrderImportRepository::updateOrderDelta()` przebudowane na dynamiczny SQL builder: `total_paid` i `is_canceled_by_buyer` są dołączane do UPDATE warunkowo. Gdy `payment_status` w bazie == `payment_status` z payloadu źródła, `total_paid` NIE jest aktualizowany. `is_canceled_by_buyer` jest pomijany w tej samej sytuacji, chyba że źródło flaguje anulowanie (`$cancelledBySource=true`) — wtedy zawsze wpisywane.
- `upsertOrderAggregate()` wylicza `$paymentStatusUnchanged` przed wywołaniem delty i propaguje wraz z `$cancelledBySource`.
- Test PHPUnit `tests/Unit/OrderImportRepositoryTest.php` z 3 scenariuszami (preserve / transition / cancel propagation). Uruchomienie odroczone — `vendor/` nieobecne w środowisku, składnia PHP zweryfikowana przez `php -l`.
**Dlaczego:**
- Incydent zamówienia #976: operator usunął 2 pozycje Girlanda i zwrócił klientowi 28,00 PLN, obniżając `total_paid` ze 119,00 na 91,00. Audyt re-importu (Phase 112-01 delta-only) wykazał, że istniejące `updateOrderDelta` nadpisywało `total_paid` z payloadu źródła przy każdym wywołaniu, jeśli identical-payload guard nie zadziałał. Ręczne korekty kwoty były ulotne.
**Boundaries (zachowane):**
- `paymentTransition` (Phase 111) i `statusOverwriteAllowed` (Phase 62) działają bez zmian.
- Cancel propagation (`$cancelledBySource` override) z Phase 112-01 nadal wymusza `status_code='anulowane'` i — od Phase 119 — `is_canceled_by_buyer=1` nawet gdy `payment_status` stabilne.
- Identical-payload no-op guard (Phase 112-01) nadal pierwsza linia obrony — Phase 119 dotyczy tylko ścieżki gdy guard nie zadziałał (payload się różni, ale `payment_status` stabilne).
## 2026-05-12 - Phase 118 Plan 01: Fakturownia Single Instance
**Co zrobiono:**
- Dodano migracje `20260512_000109_fakturownia_single_instance.sql`, ktora wybiera aktywna instancje Fakturowni, przepina delegowane `invoice_configs.integration_id` na jeden globalny rekord i usuwa nadmiarowe konta Fakturowni po przepieciu zaleznosci.
- Przebudowano `FakturowniaIntegrationRepository` na jedna globalna konfiguracje (`getSettings`, `saveSettings`, `getIntegrationId`, `getCredentials`) z kompatybilnym `findAll()` zwracajacym jeden element.
- Uproszczono `FakturowniaIntegrationController` i widok `/settings/integrations/fakturownia` do pojedynczego formularza konfiguracji i testu polaczenia.
- Hub integracji pokazuje Fakturownie jako jedna instancje, bez licznika kont.
- Zapis delegowanej konfiguracji faktury ustawia `invoice_configs.integration_id` na globalny rekord Fakturowni; UI konfiguracji faktury nie pokazuje juz selecta kont.
**BREAKING / migracja:**
- Po migracji nie ma juz wielu kont Fakturowni. Jesli baza miala wiele rekordow `integrations.type='fakturownia'`, zachowany zostaje aktywny rekord (fallback: uzywany przez konfiguracje faktur, potem najnizsze id), a pozostale sa usuwane.
## 2026-05-12 - Phase 117 Plan 01: SMSPLANET Integration Settings + Test SMS
**Co zrobiono:**