diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 555e459..cd0cf0d 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -14,7 +14,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów |-----------|-------| | Version | 3.7.0-dev | | Status | v3.7 in progress — Phases 113-117 shipped (Fakturownia + HostedSMS/SMSPLANET settings/test SMS) | -| Last Updated | 2026-05-12 | +| Last Updated | 2026-05-12 (Phase 120 closed) | ## Requirements @@ -121,6 +121,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Wystawianie faktury z zamowienia: toggle `orders.invoice_requested` w zakladce Platnosci + auto-set z importu (Allegro `invoice.required` / shopPRO 5-key parser); formularz z auto-fillem NIP przez MF Biala Liste (publiczne API); dual flow lokalny (Dompdf + atomowy `invoice_number_counters`) / delegowany (POST do Fakturowni przed INSERT, redirect 302 do natywnego PDF); lista `/settings/accounting/invoices/issued` z filtrami; snapshot pattern w `invoices` JSON; PHP 8.5-compatible (curl_close removed) — Phase 115 - [x] Integracja HostedSMS: pojedyncza globalna konfiguracja w `/settings/integrations/hostedsms`, szyfrowane haslo, karta w hubie integracji i realna wysylka testowego SMS z edytowalna trescia oraz czytelnym statusem MessageId — Phase 116 - [x] Integracja SMSPLANET: pojedyncza globalna konfiguracja w `/settings/integrations/smsplanet`, szyfrowane sekrety, autoryzacja Bearer token albo key + password, karta w hubie integracji i realna wysylka testowego SMS — Phase 117 +- [x] Re-import ochrona `total_paid`: gdy `payment_status` sie nie zmienia, `updateOrderDelta()` nie nadpisuje `total_paid` (ani `is_canceled_by_buyer`, chyba ze cancel ze zrodla); chroni reczne korekty operatora (zwroty czesciowe). Dynamic SQL SET builder + 3 testy PHPUnit (Reflection + sqlite) — Phase 119 +- [x] Ujednolicony moduł alertów UI: reusable PHP komponent `resources/views/components/alert.php` z inline SVG ikoną per typ (info/success/warning/danger), opcjonalnym dismiss button (vanilla JS, idempotent); brakujący `.alert--info` (#eff6ff/#bfdbfe/#1e3a8a); `Flash::push/all` z BC dla `set/get` (heurystyka klucza legacy); centralny renderer flash w 3 layoutach (app/auth/public); 36 widoków zmigrowanych off inline alert markup; `.flash--*` usunięte z widoków — Phase 120 ### Deferred @@ -228,6 +230,10 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | PHP 8.5: zakaz `curl_close()` w nowym kodzie | Deprecated od 8.5 (no-op od 8.0). Wycieka HTML `
Deprecated...` przed JSON response -> "json is not valid" w fetch().json(). Pattern dla wszystkich httpGet/httpPost helperow w `src/Core/Http/` i `src/Modules/Settings/*ApiClient` | 2026-05-10 | Active | | Auto-set `orders.invoice_requested` tylko przy `created=true` w imporcie | Delta-only re-import (Phase 112) zachowuje stabilnosc manualnej flagi operatora. Re-import nie nadpisuje stanu lokalnego, w tym manualnego "Klient prosi o fakture" | 2026-05-10 | Active | | `OrderProAlerts.confirm` ZAWSZE options-object API (`{title, message, onConfirm, danger}`) | Phase 114 ustalil. Phase 115 uzyl w invoice_form.php. Pozycyjne wywolanie cicho fail'uje - callback ginie. Pattern obowiazuje dla wszystkich nowych confirm dialogow | 2026-05-10 | Active | +| Alerty stronowe: jedyny renderer markupu to `resources/views/components/alert.php` (params: `$type`, `$message`/`$messageHtml`, `$dismissible`, `$role`) | Phase 120: 36 widoków zunifikowane; ikona SVG + dismiss `[data-alert-dismiss]`; SCSS `.alert` jest flex z `__icon/__body/__dismiss`. Pattern dla wszystkich nowych alertów stronowych. Nie używać `
` inline | 2026-05-12 | Active | +| Flash dual API: `Flash::push(type, message)` (preferred, typed) + `Flash::set/get(key, value)` (BC) | Phase 120: layouty (app/auth/public) iterują `Flash::all()` automatycznie. Kontrolery mogą używać dowolnego API; legacy klucze są mapowane heurystyką (`.save/.created/.deleted/.toggled` → success, `error/fail/danger` → danger, `warning` → warning, reszta → info) | 2026-05-12 | Active | +| `OrderProAlerts.confirm` ZAWSZE options-object API + Alert component zawsze przez `include` | Phase 120 ustalil format komponentu z `extract` (locals `$type`, `$message`, `$dismissible`). Trusted HTML przez `$messageHtml` z `unset()` po użyciu (`isset` persiste w PHP `include` scope) | 2026-05-12 | Active | +| `$messageHtml` w alert component musi być `unset()` po każdym include | PHP `include` widzi zmienne kontekstu z extracted scope; bez `unset` kolejny include w tym samym widoku falszywie wykrywa `isset($messageHtml)`. Pattern dla wszystkich miejsc używających `$messageHtml` (4 widoki: invoice_form, receipt-create, printing, statistics/orders) | 2026-05-12 | Active | ## Success Metrics @@ -259,6 +265,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-05-12 after Phase 117 (SMSPLANET Integration Settings + Test SMS) completion; v3.7 milestone in progress* +*Last updated: 2026-05-12 after Phase 120 (Alert Component Unification) closure; v3.7 milestone in progress* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 9807dc7..4af583d 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -17,6 +17,9 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt | 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia + NIP lookup MF Biala Lista) | 1/1 | Complete (2026-05-10) | | 116 | HostedSMS Integration Settings + Test SMS | 1/1 | Complete (2026-05-12) | | 117 | SMSPLANET Integration Settings + Test SMS | 1/1 | Complete (2026-05-12; migration/manual SMS verification pending) | +| 118 | Fakturownia Single Instance | 1/1 | Applied (2026-05-12; migration/manual Fakturownia verification pending) | +| 119 | Re-import total_paid Protection | 1/1 | Complete (2026-05-12; phpunit run + manual shoppro smoke pending env) | +| 120 | Alert Component Unification | 1/1 | Complete (2026-05-12; CSS rebuilt; smoke tests pending operator) | Planowane kolejne fazy v3.7 (kandydaci, do rozplanowania): - Eksport XLSX listy wystawionych faktur (analogicznie do paragonow) @@ -498,4 +501,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-05-12 - Phase 117 (SMSPLANET Integration Settings + Test SMS) complete with environment verification gaps; v3.7 milestone in progress* +*Last updated: 2026-05-12 - Phase 118 (Fakturownia Single Instance) applied with environment verification gaps; v3.7 milestone in progress* diff --git a/.paul/STATE.md b/.paul/STATE.md index 8ba8a92..43a225e 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,34 +5,37 @@ See: .paul/PROJECT.md (updated 2026-05-07) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v3.7 Invoices + operational integrations - Phase 117 SMSPLANET settings/test SMS unified; migration/live SMS verification remains environment-dependent. +**Current focus:** v3.7 Invoices + operational integrations - Phase 118 Fakturownia single-instance APPLY complete; DB/manual verification remains environment-dependent. ## Current Position Milestone: v3.7 Invoices (Fakturownia integration) - In progress -Phase: 117 of TBD (SMSPLANET Integration Settings + Test SMS) - Complete -Plan: 117-01 unified -Status: Phase 117 complete; migration/manual SMS test pending because local DB is unavailable -Last activity: 2026-05-12 - Unified SMSPLANET settings/test integration +Phase: 120 of TBD (Alert Component Unification) - Complete +Plan: 120-01 complete (UNIFY closed) +Status: Loop closed; CSS rebuilt inline; manual smoke tests pending operator (fakturownia test, login error, dismiss). +Last activity: 2026-05-12 - UNIFY complete for .paul/phases/120-alert-component-unification/120-01-PLAN.md Progress: -- Milestone v3.7: [########--] ~80% (Phase 113 + 114 + 115 + 116 + 117 closed; environment verification gaps documented) -- Phase 117: [##########] 100% - Implementation unified; migration/live SMS verification pending externally +- Milestone v3.7: [##########] ~90% (Phase 113-117 closed; Phase 118 applied; Phase 119 complete; Phase 120 complete) +- Phase 120: [##########] 100% - Complete ## Loop Position Current loop state: ``` PLAN -> APPLY -> UNIFY - done done done [Phase 117 closed with environment gaps documented] + done done done [Loop closed - ready for transition] ``` ## Session Continuity Last session: 2026-05-12 -Stopped at: Phase 117 unified; local migration/manual SMS verification pending -Next action: Start local MySQL, run `C:\xampp\php\php.exe bin\migrate.php`, verify SMSPLANET settings/test SMS, then plan the next v3.7 candidate or close the milestone. -Resume file: .paul/phases/117-smsplanet-integration/117-01-SUMMARY.md +Stopped at: Phase 120 UNIFY closed +Next action: Phase transition (git commit feat(120): alert component unification) then choose next phase candidate from v3.7 backlog or pause. +Resume file: .paul/phases/120-alert-component-unification/120-01-SUMMARY.md + +## Pending parallel work +- Phase 118 still awaiting UNIFY (.paul/phases/118-fakturownia-single-instance/118-01-SUMMARY.md exists; DB verification gated on local MySQL). ## Git State @@ -47,6 +50,7 @@ Branch: main - Recznie odtworzyc istniejace reguly automatyzacji z grupowymi kluczami (BREAKING z 108-02). - HostedSMS inbound replies: requires DCS/HostedSMS activation before implementation. - Phase 117 follow-up: run migration when XAMPP MySQL is online and manually test real SMSPLANET sends for Bearer token and key + password. +- Phase 119 follow-up: `composer install` + `vendor/bin/phpunit tests/Unit/OrderImportRepositoryTest.php` to run the 3 new tests; manual smoke test re-syncing order #976 from shoppro to confirm `total_paid=91.00` persists across re-import. ## Deferred to Next Milestones diff --git a/.paul/changelog/2026-05-12.md b/.paul/changelog/2026-05-12.md index 842639e..9b60ddd 100644 --- a/.paul/changelog/2026-05-12.md +++ b/.paul/changelog/2026-05-12.md @@ -11,6 +11,16 @@ - Dodano klienta SMSPLANET (`POST https://api2.smsplanet.pl/sms`) z obsluga Bearer token oraz `key` + `password`, bez parametru `test=1` dla testow realnych. - Poprawiono uklad checkboxow i radio buttonow na ekranie integracji SMSPLANET przez wspolny komponent SCSS. - Odnotowano blokery weryfikacji: lokalny MySQL odmawial polaczenia, `vendor\bin\phpunit` i `sonar-scanner` nie byly dostepne. +- [Phase 119, Plan 01] Re-import zamowien chroni `total_paid` przed nadpisaniem gdy `payment_status` sie nie zmienia (incydent #976: operator zwrocil 28,00 PLN klientowi). +- `OrderImportRepository::updateOrderDelta()` przepisane na dynamic SET builder z warunkowymi `total_paid` i `is_canceled_by_buyer`; cancel propagation ze zrodla nadal wymusza wpis flagi. +- Test PHPUnit `tests/Unit/OrderImportRepositoryTest.php` z 3 scenariuszami (preserve / transition / cancel) - syntax-checked, run odroczony do `composer install`. +- Operacyjnie: zamowienie #976 poprawione recznie w bazie (delete pozycji Girlanda, total_with_tax/total_paid 119->91, wpis do `order_activity_log`). +- [Phase 120, Plan 01] Ujednolicony moduł alertów: reusable komponent PHP `components/alert.php` z ikoną SVG i dismiss, brakujący wariant `.alert--info` (#eff6ff/#bfdbfe/#1e3a8a) - naprawa czarnego tekstu po teście Fakturowni. +- `Flash::push(type, message)` + `Flash::all()` z BC dla `set/get`; heurystyka klucza legacy (error/.save/warning/success). +- Centralny renderer flash w layoutach `app.php`, `auth.php`, `public.php` (foreach Flash::all() → component) - przyszłe `Flash::push()` zadziała bez ifów w widokach. +- Vanilla JS `alert-dismiss.js` z idempotent guardem + delegated click handlerem. +- 36 widoków zmigrowanych z inline `
` / `.flash--*` na komponent (34 z planu + odkryte `orders/show.php` i `shipments/prepare.php`). +- CSS przebudowane via `npx sass --style=compressed`: `public/assets/css/app.css` (63 560 B), `login.css` (7 409 B). ## Zmienione pliki @@ -42,3 +52,49 @@ - `src/Modules/Settings/SmsplanetApiClient.php` - `src/Modules/Settings/SmsplanetIntegrationController.php` - `src/Modules/Settings/SmsplanetIntegrationRepository.php` +- `.paul/phases/119-reimport-total-paid-protection/119-01-PLAN.md` +- `.paul/phases/119-reimport-total-paid-protection/119-01-SUMMARY.md` +- `src/Modules/Orders/OrderImportRepository.php` +- `tests/Unit/OrderImportRepositoryTest.php` +- `.paul/phases/120-alert-component-unification/120-01-PLAN.md` +- `.paul/phases/120-alert-component-unification/120-01-SUMMARY.md` +- `resources/views/components/alert.php` +- `public/assets/js/modules/alert-dismiss.js` +- `resources/scss/shared/_ui-components.scss` +- `public/assets/css/app.css` +- `public/assets/css/login.css` +- `src/Core/Support/Flash.php` +- `resources/views/layouts/app.php` +- `resources/views/layouts/auth.php` +- `resources/views/layouts/public.php` +- `resources/views/settings/fakturownia.php` +- `resources/views/settings/accounting-invoice-edit.php` +- `resources/views/settings/accounting-receipt-edit.php` +- `resources/views/settings/accounting-receipts.php` +- `resources/views/settings/accounting-invoices.php` +- `resources/views/settings/accounting.php` +- `resources/views/settings/allegro.php` +- `resources/views/settings/apaczka.php` +- `resources/views/settings/company.php` +- `resources/views/settings/cron.php` +- `resources/views/settings/database.php` +- `resources/views/settings/delivery-status-form.php` +- `resources/views/settings/delivery-statuses.php` +- `resources/views/settings/email-mailboxes.php` +- `resources/views/settings/email-templates.php` +- `resources/views/settings/email-templates-form.php` +- `resources/views/settings/integrations.php` +- `resources/views/settings/printing.php` +- `resources/views/settings/project-mappings.php` +- `resources/views/settings/shoppro.php` +- `resources/views/settings/statuses.php` +- `resources/views/orders/list.php` +- `resources/views/orders/show.php` +- `resources/views/orders/receipt-create.php` +- `resources/views/shipments/prepare.php` +- `resources/views/accounting/invoice_form.php` +- `resources/views/automation/index.php` +- `resources/views/automation/form.php` +- `resources/views/users/index.php` +- `resources/views/statistics/orders.php` +- `resources/views/auth/login.php` diff --git a/.paul/codebase/architecture.md b/.paul/codebase/architecture.md index 7e60234..72b2e03 100644 --- a/.paul/codebase/architecture.md +++ b/.paul/codebase/architecture.md @@ -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: `
...
`. +- 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 `
...
` w widokach (36 plikow razem ze `shipments/prepare.php` i `orders/show.php`) zastapione przez ``. +- `.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 diff --git a/.paul/codebase/tech_changelog.md b/.paul/codebase/tech_changelog.md index b9aed9f..10d4b9a 100644 --- a/.paul/codebase/tech_changelog.md +++ b/.paul/codebase/tech_changelog.md @@ -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 `
` 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:** diff --git a/.paul/phases/120-alert-component-unification/120-01-PLAN.md b/.paul/phases/120-alert-component-unification/120-01-PLAN.md new file mode 100644 index 0000000..437c15c --- /dev/null +++ b/.paul/phases/120-alert-component-unification/120-01-PLAN.md @@ -0,0 +1,290 @@ +--- +phase: 120-alert-component-unification +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - resources/scss/shared/_ui-components.scss + - resources/views/components/alert.php + - public/assets/js/modules/alert-dismiss.js + - resources/views/layouts/app.php + - resources/views/layouts/auth.php + - resources/views/layouts/public.php + - src/Core/Support/Flash.php + - resources/views/settings/fakturownia.php + - resources/views/settings/accounting-invoice-edit.php + - resources/views/settings/smsplanet.php + - resources/views/settings/hostedsms.php + - resources/views/settings/accounting.php + - resources/views/settings/accounting-receipt-edit.php + - resources/views/settings/accounting-receipts.php + - resources/views/settings/accounting-invoices.php + - resources/views/accounting/invoice_form.php + - resources/views/users/index.php + - resources/views/statistics/orders.php + - resources/views/settings/statuses.php + - resources/views/settings/shoppro.php + - resources/views/settings/printing.php + - resources/views/settings/project-mappings.php + - resources/views/settings/integrations.php + - resources/views/settings/email-templates.php + - resources/views/settings/inpost.php + - resources/views/settings/email-templates-form.php + - resources/views/settings/email-mailboxes.php + - resources/views/settings/delivery-statuses.php + - resources/views/settings/database.php + - resources/views/settings/delivery-status-form.php + - resources/views/settings/cron.php + - resources/views/settings/company.php + - resources/views/settings/apaczka.php + - resources/views/settings/allegro.php + - resources/views/orders/receipt-create.php + - resources/views/orders/list.php + - resources/views/automation/index.php + - resources/views/automation/form.php + - resources/views/auth/login.php +autonomous: true +delegation: off +--- + + +## Goal +Ujednolicic alerty/flash messages w calej aplikacji: reusable komponent widoku, brakujacy wariant `--info`, ikony i dismiss button, centralny renderer flash w layoutach. Po teście połączenia w `/settings/integrations/fakturownia` alert "OK (HTTP 200)" pojawia sie jako wyraznie wystylizowany `alert--info` z ikona i mozliwoscia zamkniecia. + +## Purpose +Obecnie 34 widoki uzywaja inline `
` z roznym wzorcem; brak `.alert--info` powoduje czarny tekst na bialym tle (bug widoczny w fakturownia.php). Brak centralnego renderowania flash zmusza kazdy controller+widok do recznego przekazywania zmiennych `$flashSave/$flashTest/$flashError`. Jedno zrodlo prawdy upraszcza UX i przyszle utrzymanie. + +## Output +- `resources/views/components/alert.php` (komponent z paramami `$type`, `$message`, `$dismissible`) +- `resources/scss/shared/_ui-components.scss` z `.alert--info`, ikonami i stylami dismiss +- `public/assets/js/modules/alert-dismiss.js` (vanilla JS, idempotent guard) +- `src/Core/Support/Flash.php` rozszerzone o `push()` + `all()` z zachowaniem `set()/get()` (BC) +- Centralny render flash w `layouts/app.php` (+ `auth.php`, `public.php`) +- Migracja 34 widokow na komponent + zamiana `.flash--*` w login.php + + + + +- **Zakres** — Jak szeroki zakres migracji widokow w tej fazie? + → Odpowiedz: Komponent + migracja wszystkich (34 widoki). +- **Funkcje** — Jakie warianty i funkcje ma obslugiwac komponent alert? + → Odpowiedz: Wariant info (brakujacy), Ikona per typ (info/success/warning/danger), Przycisk dismiss (×). Auto-hide pominiete. +- **Flash legacy** — Czy ujednolicic rowniez stary wzorzec `.flash--error` (login.php) z nowym `.alert--*`? + → Odpowiedz: Tak, zastapic `.flash--*` przez `.alert--*`. +- **Flash render** — Czy wyswietlanie alertow z sesji (Flash::get) zrobic tez przez komponent (centralny render np. w layoucie)? + → Odpowiedz: Centralny renderer flash w layoucie app.php (i auth/public). + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md +@.paul/codebase/architecture.md + +## Source Files +@resources/scss/shared/_ui-components.scss +@resources/views/settings/fakturownia.php +@resources/views/layouts/app.php +@resources/views/auth/login.php +@src/Core/Support/Flash.php + + + + +## AC-1: Komponent alert + brakujacy wariant info +```gherkin +Given uzytkownik otwiera /settings/integrations/fakturownia i wykonuje udany test polaczenia +When kontroler ustawia Flash::push('info', 'OK (HTTP 200)') i widok renderuje sie ponownie +Then na gorze strony pokazuje sie alert z niebieskim tlem (#eff6ff), niebieska ramka, ikona "i" po lewej, tekstem "OK (HTTP 200)" w czytelnym kontraście oraz przyciskiem × po prawej; klikniecie × usuwa alert z DOM bez przeladowania +``` + +## AC-2: Centralny renderer flash w layoutach +```gherkin +Given kontroler wywoluje Flash::push('success', 'Zapisano') albo legacy Flash::set('module.save', 'Zapisano') +When renderowany jest widok dziedziczacy z layouts/app.php (lub auth.php / public.php) +Then layout automatycznie wyswietla wszystkie zakolejkowane flash entries u gory glownego obszaru, bez koniecznosci ifow w widoku; po pobraniu Flash::all() kolejne zadanie nie pokazuje juz tych wpisow +``` + +## AC-3: Migracja widokow i flash--error +```gherkin +Given kazdy z 34 widokow wymienionych w files_modified +When przegladam ich kod +Then zaden nie zawiera inline `
` z zmiennymi flash (zamiast tego uzywa komponentu lub renderowanie jest delegowane do layoutu); login.php nie zawiera klasy `.flash--error` (zostala zastapiona alert--danger przez komponent); wizualnie alerty na fakturownia/hostedsms/smsplanet/login wygladaja identycznie i spojnie +``` + + + + + + + Task 1: Komponent alert + SCSS info/ikony/dismiss + JS + + resources/scss/shared/_ui-components.scss, + resources/views/components/alert.php, + public/assets/js/modules/alert-dismiss.js, + resources/views/layouts/app.php, + resources/views/layouts/auth.php, + resources/views/layouts/public.php + + + 1) SCSS — w `_ui-components.scss` po istniejacych `.alert--success/warning/danger` dodac: + - `.alert` — display: flex; gap: 10px; align-items: flex-start; (zachowac padding/border-radius/border/font-size/min-height) + - `.alert__icon` — flex: 0 0 18px; line-height: 1; svg width/height 18px; color: inherit + - `.alert__body` — flex: 1; line-height: 1.4 + - `.alert__dismiss` — margin-left: auto; background: none; border: 0; cursor: pointer; padding: 2px 6px; color: inherit; opacity: 0.6; &:hover { opacity: 1 } + - `.alert--info` — border-color: #bfdbfe; background: #eff6ff; color: #1e3a8a + - Subtelnie wzmocnic kontrast tla istniejacych wariantow tylko jezeli wymaga to drobnej zmiany (np. zostawic jak jest dla --success/warning/danger). + 2) `resources/views/components/alert.php` — komponent renderujacy 1 alert. Params (przez extract): + - `$type` (string, default 'info') — jeden z: info|success|warning|danger + - `$message` (string) — escapowany przez `$e()`; alternatywnie `$messageHtml` (trusted HTML, opcjonalnie) + - `$dismissible` (bool, default true) + - Markup: `
- +
diff --git a/resources/views/orders/receipt-create.php b/resources/views/orders/receipt-create.php index 3f014a5..7e688b3 100644 --- a/resources/views/orders/receipt-create.php +++ b/resources/views/orders/receipt-create.php @@ -22,7 +22,9 @@ $hasExistingReceipts = $existingReceiptsList !== [];
-
+ Uwaga! Do tego zamówienia wystawiono już paragon(ów):
    @@ -35,7 +37,10 @@ $hasExistingReceipts = $existingReceiptsList !== [];
-
+ +
diff --git a/resources/views/orders/show.php b/resources/views/orders/show.php index 75b0919..87537db 100644 --- a/resources/views/orders/show.php +++ b/resources/views/orders/show.php @@ -134,10 +134,10 @@ foreach ($addressesList as $address) {
-
+
-
+
diff --git a/resources/views/settings/accounting-invoice-edit.php b/resources/views/settings/accounting-invoice-edit.php index 4a9e0a2..31d4a3f 100644 --- a/resources/views/settings/accounting-invoice-edit.php +++ b/resources/views/settings/accounting-invoice-edit.php @@ -1,7 +1,7 @@ |null $config */ $config = is_array($config ?? null) ? $config : null; -$accounts = is_array($fakturowniaAccounts ?? null) ? $fakturowniaAccounts : []; +$fakturowniaSettings = is_array($fakturowniaSettings ?? null) ? $fakturowniaSettings : []; $isEdit = $config !== null; $cid = (int) ($config['id'] ?? 0); @@ -13,8 +13,10 @@ $orderReference = (string) ($config['order_reference'] ?? 'none'); $paymentToDays = (int) ($config['payment_to_days'] ?? 7); $defaultKind = (string) ($config['default_kind'] ?? 'vat'); $isDelegated = ((int) ($config['is_delegated'] ?? 0)) === 1; -$integrationId = (int) ($config['integration_id'] ?? 0); $isActive = $isEdit ? ((int) ($config['is_active'] ?? 0)) === 1 : true; +$fakturowniaConfigured = trim((string) ($fakturowniaSettings['account_prefix'] ?? '')) !== '' + && !empty($fakturowniaSettings['has_api_token']); +$fakturowniaActive = !empty($fakturowniaSettings['is_active']); $success = trim((string) ($successMessage ?? '')); $error = trim((string) ($errorMessage ?? '')); @@ -25,10 +27,10 @@ $error = trim((string) ($errorMessage ?? ''));

- +
-
+
@@ -105,24 +107,15 @@ $error = trim((string) ($errorMessage ?? ''));
diff --git a/resources/views/settings/accounting-invoices.php b/resources/views/settings/accounting-invoices.php index a32550f..3bb7ad5 100644 --- a/resources/views/settings/accounting-invoices.php +++ b/resources/views/settings/accounting-invoices.php @@ -11,10 +11,10 @@ $error = trim((string) ($errorMessage ?? ''));

Zarzadzaj konfiguracjami wystawiania faktur. Mozesz dodac wiele konfiguracji (np. dla roznych dzialalnosci) i opcjonalnie delegowac wystawianie do Fakturowni.

- +
-
+
diff --git a/resources/views/settings/accounting-receipt-edit.php b/resources/views/settings/accounting-receipt-edit.php index 0b26279..4a88ca8 100644 --- a/resources/views/settings/accounting-receipt-edit.php +++ b/resources/views/settings/accounting-receipt-edit.php @@ -21,10 +21,10 @@ $error = trim((string) ($errorMessage ?? ''));

- +
-
+
diff --git a/resources/views/settings/accounting-receipts.php b/resources/views/settings/accounting-receipts.php index 7c9f233..40a52a4 100644 --- a/resources/views/settings/accounting-receipts.php +++ b/resources/views/settings/accounting-receipts.php @@ -11,10 +11,10 @@ $error = trim((string) ($errorMessage ?? ''));

Zarzadzaj konfiguracjami wystawiania paragonow.

- +
-
+
diff --git a/resources/views/settings/accounting.php b/resources/views/settings/accounting.php index 758881f..d72f47e 100644 --- a/resources/views/settings/accounting.php +++ b/resources/views/settings/accounting.php @@ -8,10 +8,10 @@ $error = trim((string) ($errorMessage ?? ''));

Wybierz typ dokumentu ktorego konfiguracje chcesz zarzadzac.

- +
-
+
diff --git a/resources/views/settings/allegro.php b/resources/views/settings/allegro.php index eac07c7..f7c8264 100644 --- a/resources/views/settings/allegro.php +++ b/resources/views/settings/allegro.php @@ -40,15 +40,15 @@ foreach ($pullStatusMappings as $pm) {

- +
-
+
- +
@@ -359,7 +359,7 @@ foreach ($pullStatusMappings as $pm) {

-
+
diff --git a/resources/views/settings/apaczka.php b/resources/views/settings/apaczka.php index b1480b8..8dad0d8 100644 --- a/resources/views/settings/apaczka.php +++ b/resources/views/settings/apaczka.php @@ -11,11 +11,11 @@ $updatedAt = trim((string) ($integration['updated_at'] ?? ''));

- +
-
+
diff --git a/resources/views/settings/company.php b/resources/views/settings/company.php index 7c518c0..7868ec0 100644 --- a/resources/views/settings/company.php +++ b/resources/views/settings/company.php @@ -7,10 +7,10 @@ $s = is_array($settings ?? null) ? $settings : [];

- +
-
+
diff --git a/resources/views/settings/cron.php b/resources/views/settings/cron.php index 0ef4f3e..dccf2b9 100644 --- a/resources/views/settings/cron.php +++ b/resources/views/settings/cron.php @@ -12,11 +12,11 @@ $pastTotal = max(0, (int) ($pastPagination['total'] ?? 0));

- +
-
+
-
+
-
+
@@ -91,7 +91,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';

Przesylka

-
+
@@ -106,7 +106,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
Metoda z zamowienia: :
-
+
@@ -116,7 +116,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
-
+
diff --git a/resources/views/statistics/orders.php b/resources/views/statistics/orders.php index 7a84767..a8751aa 100644 --- a/resources/views/statistics/orders.php +++ b/resources/views/statistics/orders.php @@ -109,13 +109,18 @@ foreach ($channelOptions as $channelOption) {
- + + diff --git a/resources/views/users/index.php b/resources/views/users/index.php index f9316af..438d4f3 100644 --- a/resources/views/users/index.php +++ b/resources/views/users/index.php @@ -7,15 +7,11 @@

- +
-
- -
+
diff --git a/src/Core/Support/Flash.php b/src/Core/Support/Flash.php index 4525fef..3381d80 100644 --- a/src/Core/Support/Flash.php +++ b/src/Core/Support/Flash.php @@ -6,6 +6,8 @@ namespace App\Core\Support; final class Flash { private const FLASH_KEY = '_flash'; + private const QUEUE_KEY = '_flash_queue'; + private const ALLOWED_TYPES = ['info', 'success', 'warning', 'danger']; public static function set(string $key, mixed $value): void { @@ -35,4 +37,85 @@ final class Flash return $value; } + + public static function push(string $type, string $message): void + { + $normalizedType = in_array($type, self::ALLOWED_TYPES, true) ? $type : 'info'; + + if (!isset($_SESSION[self::QUEUE_KEY]) || !is_array($_SESSION[self::QUEUE_KEY])) { + $_SESSION[self::QUEUE_KEY] = []; + } + + $_SESSION[self::QUEUE_KEY][] = [ + 'type' => $normalizedType, + 'message' => $message, + ]; + } + + /** + * Returns and clears all queued flash entries (typed queue + legacy key/value map). + * + * @return list + */ + public static function all(): array + { + $entries = []; + + if (isset($_SESSION[self::QUEUE_KEY]) && is_array($_SESSION[self::QUEUE_KEY])) { + foreach ($_SESSION[self::QUEUE_KEY] as $entry) { + if (!is_array($entry)) { + continue; + } + $type = is_string($entry['type'] ?? null) ? $entry['type'] : 'info'; + $message = is_string($entry['message'] ?? null) ? $entry['message'] : ''; + if ($message === '') { + continue; + } + if (!in_array($type, self::ALLOWED_TYPES, true)) { + $type = 'info'; + } + $entries[] = ['type' => $type, 'message' => $message]; + } + unset($_SESSION[self::QUEUE_KEY]); + } + + if (isset($_SESSION[self::FLASH_KEY]) && is_array($_SESSION[self::FLASH_KEY])) { + foreach ($_SESSION[self::FLASH_KEY] as $key => $value) { + if (!is_string($value) || $value === '') { + continue; + } + $entries[] = [ + 'type' => self::inferTypeFromKey((string) $key), + 'message' => $value, + ]; + } + unset($_SESSION[self::FLASH_KEY]); + } + + return $entries; + } + + private static function inferTypeFromKey(string $key): string + { + $lower = strtolower($key); + + if (str_contains($lower, 'error') || str_contains($lower, 'fail') || str_contains($lower, 'danger')) { + return 'danger'; + } + if (str_contains($lower, 'warning') || str_contains($lower, 'warn')) { + return 'warning'; + } + if ( + str_contains($lower, 'success') + || str_ends_with($lower, '.save') + || str_contains($lower, 'saved') + || str_contains($lower, '.created') + || str_contains($lower, '.deleted') + || str_contains($lower, '.toggled') + ) { + return 'success'; + } + + return 'info'; + } }