feat(124): sms templates CRUD + order picker

- Nowa tabela sms_templates (name + body + is_active) + minimalny CRUD.
- /settings/sms-templates: lista + formularz z paleta zmiennych (pill chips).
- Wydzielono Sms\SmsVariableResolver ze wspolna logika placeholderow;
  Email\VariableResolver staje sie cienka fasada — EmailSendingService bez zmian.
- Dropdown "Wybierz szablon" w zakladce SMS na /orders/{id} z fetch
  GET /orders/{id}/sms/template + OrderProAlerts.confirm przy nadpisaniu.
- Stopka SMSPLANET dalej doklejana wylacznie przez SmsConversationService
  (Phase 122 contract preserved).
- Sidebar Ustawien: nowy link "Szablony SMS".

Migration: 20260512_000112_create_sms_templates.sql (CREATE TABLE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:37:51 +02:00
parent 0227f2d072
commit 522c94a434
25 changed files with 1641 additions and 105 deletions

View File

@@ -414,3 +414,61 @@ tests/
- Edycja przez `<a href=".../edit?id=N">`, toggle/delete przez `<form>` z `_token` i `js-confirm-delete`.
- Wspolny pattern miedzy `accounting-receipts.php` i `accounting-invoices.php` (faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni).
## Phase 124 — SMS Templates
### SmsTemplateRepository (`src/Modules/Sms/SmsTemplateRepository.php`)
- CRUD na `sms_templates` (PDO prepared statements, ToggleableRepositoryTrait).
- `listAll()` (cala lista alfabetycznie po `name`), `listActive()` (tylko is_active=1, kolumny `id|name|body` do dropdownu w UI).
- `save(array): int` waliduje wymagane `name` + `body` (rzuca `RuntimeException` gdy puste); wykonuje INSERT albo UPDATE wg obecnosci `id` w payloadzie; zwraca id rekordu.
- `delete(int)`, `toggleStatus(int)` przez `toggleActive('sms_templates', $id)`.
### SmsVariableResolver (`src/Modules/Sms/SmsVariableResolver.php`)
- Wydzielony z `Email\VariableResolver` — wspolna logika zmiennych dla Email i SMS.
- `buildVariableMap(order, addresses, companySettings)` zwraca mape placeholderow: `zamowienie.*`, `kupujacy.*`, `adres.*`, `firma.*`, `przesylka.*` (`przesylka.numer`/`przesylka.link_sledzenia` z najnowszej paczki przez `ShipmentPackageRepository::findLatestByOrderId` + `DeliveryStatus::trackingUrl`).
- `resolve(template, variableMap)` zastepuje `{{group.var}}` wartoscia z mapy (puste gdy brak klucza).
### Email\VariableResolver (refaktor)
- Pozostaje final class z tym samym API publicznym (`buildVariableMap`/`resolve`) — `EmailSendingService` niezmieniony.
- Konstruktor: `(ShipmentPackageRepository $repo, ?SmsVariableResolver $inner = null)`. Gdy `$inner` nie podany, sam tworzy SmsVariableResolver — backward compat dla starego wiringu.
- Metody publiczne deleguja do `$this->inner` — zero duplikacji logiki zmiennych.
### SmsTemplateController (`src/Modules/Settings/SmsTemplateController.php`)
- Mirror `EmailTemplateController` bez Quill/skrzynki/zalacznika/duplikacji.
- Akcje: `index` (lista), `create`/`edit`/`save` (form CRUD), `delete`, `toggleStatus` (AJAX JSON), `getVariables` (JSON paleta dla ewentualnego dynamic palette).
- `VARIABLE_GROUPS` jako stala klasy — pelne 5 grup (zamowienie/kupujacy/adres/firma/przesylka) zgodnie ze wspolnym SmsVariableResolver.
- Routy: `/settings/sms-templates`, `/create`, `/edit`, `/save`, `/delete`, `/toggle`, `/variables`. CSRF `_token` na POST. Flash `settings.sms_templates.success|error`.
### OrdersController (rozszerzenie)
- Dodane optional params konstruktora: `?SmsTemplateRepository $smsTemplates`, `?SmsVariableResolver $smsVariableResolver`, `?CompanySettingsRepository $companySettingsRepo` (po istniejacych SMS params; default null = backward compat).
- `show()` przekazuje `$smsTemplates` (list active) do widoku jako `smsTemplates`.
- Nowa metoda `smsTemplate(Request)` -> `GET /orders/{id}/sms/template?template_id=N` -> JSON `{ok, body, name}` z rozwinietymi zmiennymi. 400/404/500 dla nieprawidlowych parametrow/braku rekordu.
### Widok `orders/show.php`
- Nad textarea `name="message"` (`#js-sms-message`) dodany conditional `<select data-sms-template-picker data-order-id data-message-target="js-sms-message">` z opcja domyslna + aktywne szablony (renderowany tylko gdy `$smsTemplatesList !== []`).
- Textarea ma teraz `id="js-sms-message"` — JS target.
### Frontend module `public/assets/js/modules/sms-template-picker.js`
- Vanilla JS, idempotent guard `window.__smsTemplatePickerBound` + per-element `dataset.smsPickerBound`.
- Na `change` selecta: fetch `/orders/{id}/sms/template?template_id=N`, podstaw body do textarea, fire `input` event.
- Gdy textarea ma juz tresc -> `OrderProAlerts.confirm({...})` options-object API (Phase 114 pattern). Po zatwierdzeniu nadpisuje, po anulowaniu resetuje select. Fallback na natywny `confirm()`.
- Ladowany globalnie z `layouts/app.php` (linia po `notifications.js`).
### Wspolny resolver — wiring DI (`routes/web.php`)
- `$smsVariableResolver = new SmsVariableResolver($shipmentPackageRepositoryForOrders);`
- `$variableResolver = new VariableResolver($shipmentPackageRepositoryForOrders, $smsVariableResolver);` (drugi argument opcjonalny dla BC).
- `$smsTemplateRepository = new SmsTemplateRepository($app->db());`
- `$smsTemplateController = new SmsTemplateController($template, $translator, $auth, $smsTemplateRepository);`
- `$ordersController` rozszerzony o 3 trailing params (smsTemplateRepository, smsVariableResolver, companySettingsRepository).
### SCSS — `_sms-templates.scss`
- Nowy partial `resources/scss/modules/_sms-templates.scss` z klasami `.sms-template-*` (active label, counter, body grid) oraz `.sms-var-panel/.sms-var-group/.sms-var-item` dla palety zmiennych.
- Import w `app.scss` po `customer-risk-alert`.
### Stopka — preserved Phase 122 contract
- Szablony SMS NIE zawieraja `default_footer` — operator wpisuje sama tresc.
- `SmsConversationService::buildFinalOutboundBody()` dokleja stopke raz przy `sendFromOrder()` (po wstawieniu szablonu i ewentualnej edycji przez operatora). Walidacja `MAX_SMS_LENGTH = 918` obowiazuje na finalnej tresci.
### BREAKING / migration
- Migracja `20260512_000112_create_sms_templates.sql``CREATE TABLE IF NOT EXISTS sms_templates` (DDL, brak SELECT no-op).
- Brak innych zmian schematu. `OrdersController` ctor: 3 NEW optional params (default null) — backwards compatible.