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

@@ -0,0 +1,169 @@
---
phase: 124-sms-templates
plan: 01
subsystem: sms
tags: [sms, templates, smsplanet, orders, settings]
requires:
- phase: 117-smsplanet-integration
provides: SmsplanetIntegrationRepository, SmsplanetApiClient
- phase: 121-smsplanet-conversation-notifications
provides: SmsConversationService::sendFromOrder, SMS tab w /orders/{id}
- phase: 122-smsplanet-default-footer
provides: default_footer + MAX_SMS_LENGTH = 918 validation on final body
- phase: 14-email-templates
provides: email_templates CRUD pattern, VARIABLE_GROUPS structure
provides:
- sms_templates DB table + CRUD repository
- /settings/sms-templates panel (lista + formularz)
- shared SmsVariableResolver (extracted from Email\VariableResolver)
- GET /orders/{id}/sms/template endpoint (JSON, variables resolved per-order)
- Dropdown "Wybierz szablon" w zakladce SMS na /orders/{id}
- sms-template-picker.js (vanilla JS, OrderProAlerts.confirm pattern)
affects: [future SMS automation (send_sms action), invoice.created event]
tech-stack:
added: []
patterns:
- "Shared VariableResolver pattern: Email\\VariableResolver as thin facade delegating to Sms\\SmsVariableResolver"
- "Idempotent JS module guard: window.__smsTemplatePickerBound + dataset.smsPickerBound"
- "Pill-style variable chips with monospace code + descriptive label (.sms-var-item)"
key-files:
created:
- database/migrations/20260512_000112_create_sms_templates.sql
- src/Modules/Sms/SmsTemplateRepository.php
- src/Modules/Sms/SmsVariableResolver.php
- src/Modules/Settings/SmsTemplateController.php
- resources/views/settings/sms-templates.php
- resources/views/settings/sms-templates-form.php
- resources/scss/modules/_sms-templates.scss
- public/assets/js/modules/sms-template-picker.js
modified:
- src/Modules/Email/VariableResolver.php
- src/Modules/Orders/OrdersController.php
- routes/web.php
- resources/views/orders/show.php
- resources/views/layouts/app.php
- resources/lang/pl.php
- resources/scss/app.scss
key-decisions:
- "SMS template fields minimal: name + body + is_active (no subject/mailbox/attachment)"
- "Footer NOT in template — appended by SmsConversationService (Phase 122 contract preserved)"
- "Email\\VariableResolver becomes facade — preserves EmailSendingService contract, zero regression risk"
- "Variable picker = pill chips with {{var}} + description on same row (not dropdown like email)"
- "Action column uses flex+gap not inline-flex on td — robust against block-display <form> children"
patterns-established:
- "Shared resolver: keep facade in original namespace for BC, move logic to new namespace"
- "Idempotent JS modules: window.__moduleBound + dataset.moduleBound guards"
- "Template picker UX: dropdown insert + OrderProAlerts.confirm on non-empty target"
duration: ~90min
started: 2026-05-12T23:30:00Z
completed: 2026-05-13T00:30:00Z
---
# Phase 124 Plan 01 — SMS Templates — SUMMARY
**SMS templates CRUD w `/settings/sms-templates` + dropdown "Wybierz szablon" na zakladce SMS w szczegolach zamowienia — wstawia tresc z rozwinietymi zmiennymi `{{kupujacy.imie_nazwisko}}`, `{{przesylka.numer}}` itp. Wspolny `SmsVariableResolver` wydzielony z Email\\VariableResolver bez regresji w wysylce e-mail.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~90 min (PLAN + APPLY + UI fixes + UNIFY) |
| Started | 2026-05-12T23:30:00Z |
| Completed | 2026-05-13T00:30:00Z |
| Tasks | 5/5 ukonczone |
| Files modified | 17 (8 created + 9 modified) |
**Status:** UNIFY complete. Operator zaakceptowal UI po dwoch iteracjach UI fixes (chipy zmiennych + nowrap akcji w liscie).
## Files modified
| File | Change |
|------|--------|
| `database/migrations/20260512_000112_create_sms_templates.sql` | NEW — CREATE TABLE `sms_templates` + idx_active_name |
| `src/Modules/Sms/SmsTemplateRepository.php` | NEW — CRUD PDO repo z `ToggleableRepositoryTrait` |
| `src/Modules/Sms/SmsVariableResolver.php` | NEW — wspolny resolver wydzielony z Email\VariableResolver |
| `src/Modules/Email/VariableResolver.php` | REFACTOR — final class staje sie cienka fasada delegujaca do SmsVariableResolver; konstruktor optional 2nd arg dla BC |
| `src/Modules/Settings/SmsTemplateController.php` | NEW — index/create/edit/save/delete/toggleStatus/getVariables |
| `src/Modules/Orders/OrdersController.php` | EXTENDED — 3 optional ctor params + `smsTemplate()` endpoint + `smsTemplates` w show() view payload |
| `routes/web.php` | NEW use'y (SmsTemplateController, SmsTemplateRepository, SmsVariableResolver); wiring DI; 7 rout `/settings/sms-templates/*` + 1 ruta `/orders/{id}/sms/template`; OrdersController wiring +3 params |
| `resources/views/settings/sms-templates.php` | NEW — lista z toggle AJAX + js-confirm-delete |
| `resources/views/settings/sms-templates-form.php` | NEW — formularz CRUD z paleta zmiennych + licznikiem znakow |
| `resources/views/orders/show.php` | EXTENDED — dropdown `<select data-sms-template-picker>` nad textarea; textarea ma id `js-sms-message` |
| `resources/views/layouts/app.php` | sidebar sublink "Szablony SMS"; `<script>` tag dla sms-template-picker.js |
| `resources/lang/pl.php` | klucze `orders.details.sms.template_picker(_placeholder)` |
| `public/assets/js/modules/sms-template-picker.js` | NEW — vanilla JS, idempotent guard, fetch endpoint + OrderProAlerts.confirm przy nadpisaniu |
| `resources/scss/modules/_sms-templates.scss` | NEW — `.sms-template-*`, `.sms-var-*` klasy |
| `resources/scss/app.scss` | import nowego partiala |
| `.paul/codebase/db_schema.md` | sekcja `sms_templates` + Total tables 60 -> 61 |
| `.paul/codebase/architecture.md` | sekcja "Phase 124 — SMS Templates" |
| `.paul/codebase/tech_changelog.md` | wpis Phase 124 |
## Verification
- `php -l` przeszedl bez bledow na wszystkich zmienionych plikach PHP (11 plikow).
- `php bin/migrate.php` **nie uruchomione** — MySQL niedostepny w trakcie APPLY (sandbox/XAMPP offline). DDL jest prostym `CREATE TABLE IF NOT EXISTS`; operator uruchomi rownolegle z innymi pending migracjami.
- Browser smoke (CRUD `/settings/sms-templates`, dropdown na `/orders/{id}?tab=sms`, fetch endpoint, nadpisanie z confirm, wysylka SMS ze stopka raz) **pending operator** — wymaga zywego XAMPP + zalogowanego usera.
- SCSS `app.css` build **pending operator** (`npm run scss` / manualny rebuild) — `resources/scss/app.scss` ma nowy `@use modules/sms-templates`; bez rebuildu klasy `.sms-template-*` i `.sms-var-*` nie maja styli (form pozostaje funkcjonalny, ale paleta zmiennych bedzie bez ramki/koloru).
## Acceptance Criteria
| AC | Status |
|----|--------|
| AC-1: CRUD szablonow SMS w `/settings/sms-templates` | DONE (kod) — manual UAT pending |
| AC-2: Walidacja zapisu (puste name/body) | DONE — `SmsTemplateRepository::save()` rzuca RuntimeException; controller Flash danger + redirect |
| AC-3: Wspolny VariableResolver — bez regresji w Email | DONE — `Email\VariableResolver` deleguje, `EmailSendingService` niezmieniony |
| AC-4: Wstawianie szablonu w zakladce SMS z rozwinietymi zmiennymi | DONE — endpoint JSON + JS module z OrderProAlerts.confirm przy nadpisaniu |
| AC-5: Stopka doklejana raz, walidacja 918 znakow na finalnej tresci | DONE — szablon nie zawiera stopki; `SmsConversationService` niezmieniony (Phase 122 contract preserved) |
| AC-6: Sidebar link "Szablony SMS" z active state | DONE |
## Deviations from PLAN
| Type | Count | Impact |
|------|-------|--------|
| Scope additions (UI fixes po UAT) | 2 | Visual polish — bez zmiany kontraktu |
| Deferred | 0 | Brak |
| Plan items pominiete (low value) | 2 | Zero ryzyka |
### Deviations w trakcie APPLY
1. **`SmsConversationService::renderTemplate()` NIE zostala dodana.** Plan Task 2 zakladal nowa metode w Service, ale to nie dodaje wartosci — controller (`OrdersController::smsTemplate()`) uzywa `SmsVariableResolver` bezposrednio. Zachowuje kontrakt Service (Phase 122 contract preserved bez zmian sygnatury konstruktora). Zysk: mniej zmian w SmsConversationService, brak ryzyka regresji w Phase 121/122.
2. **`OrdersController` dostal `?CompanySettingsRepository` jako trzeci nowy optional param** — OrdersController nie mial wczesniej tego repo; zostal wstrzykniety razem z SmsTemplateRepository i SmsVariableResolver. Wiring w `routes/web.php` przekazuje `$companySettingsRepository` (juz istnial dla company-settings flow).
3. **Brak `preview` endpointu w SmsTemplateController** (plan wspominal o ewentualnym podgladzie z `order_id`). Pominiete — paleta zmiennych pod textarea + licznik znakow wystarcza.
### UI fixes po UAT (scope additions)
1. **Layout palety zmiennych** — pierwsza wersja (grid 50/50 z form-grid-2: textarea | paleta side-by-side) operator ocenil jako "kiepsko". Refactor: textarea na pelna szerokosc, paleta przeniesiona ponizej jako pelnoszerokie chipy w grupach oddzielonych dashed separatorem. Chipy zmienione z prostego `inline-block` na `border-radius: 999px` (pill) z `{{var}}` w `<code>` + opisem `<span class="sms-var-item__desc">` w jednym wierszu. Hover indigo (`#eef2ff`/`#6366f1`). Plik: `resources/scss/modules/_sms-templates.scss`, `resources/views/settings/sms-templates-form.php`.
2. **Akcje w liscie szablonow zawijaja sie**`td.sms-template-actions` z `white-space: nowrap` nie wystarczyl, bo wewnetrzna `<form>` (delete) jest blokowa (klasa `inline-form` nie istnieje globalnie w SCSS). Fix: `.sms-template-actions { display: flex; flex-wrap: nowrap; gap: 6px; }` + `.sms-template-actions > form { display: inline-flex; margin: 0; }`. Plik: `resources/scss/modules/_sms-templates.scss`.
SCSS przebudowany `npx sass --style=compressed` po obu fixach. Operator zaakceptowal final UI ("jest ok").
## Pending Actions (operator)
- `php bin/migrate.php` (XAMPP MySQL online) — utworzy `sms_templates`.
- SCSS rebuild (`npm run scss` lub workflow operatora) dla `_sms-templates.scss`.
- UAT manualny:
1. `/settings/sms-templates/create` — utworz "Numer sledzenia": body `"Czesc {{kupujacy.imie_nazwisko}}, Twoja przesylka {{przesylka.numer}} jest w drodze. Sledzenie: {{przesylka.link_sledzenia}}"`. Zapisz.
2. Toggle status (AJAX) — dezaktywuj, ponownie aktywuj.
3. Edycja — zmien body, zapisz.
4. Duplikuj? — brak akcji (zgodnie z planem; minimalne pola).
5. Usun (potwierdzenie OrderProAlerts).
6. `/orders/{id}?tab=sms` z zamowieniem majacym paczke — dropdown widoczny, wybierz szablon, body wypelnia textarea z rozwinietymi `{{kupujacy.imie_nazwisko}}` i `{{przesylka.numer}}`.
7. Wpisz cos w textarea, wybierz inny szablon — pojawia sie OrderProAlerts confirm.
8. Wyslij SMS — sprawdz w `sms_messages.body` ze stopka SMSPLANET doklejona raz (jezeli skonfigurowana).
9. Regresja Email: wyslij e-mail z zamowienia z istniejacym szablonem — placeholders `{{kupujacy.imie_nazwisko}}` i `{{zamowienie.numer}}` rozwiazane normalnie.
## SonarQube
- CLI niedostepne (per Phase 116/117/121/122 history). Skipped, log w STATE Pending Actions.
## Next Phase Readiness
**Ready:**
- `SmsTemplateRepository::listActive()` + `SmsVariableResolver` gotowe do reuse w przyszlej automatyzacji SMS (akcja `send_sms` analogiczna do `send_email` z Phase 16).
- Wzorzec dropdown + JSON endpoint + OrderProAlerts.confirm = template do innych quick-insert mechanizmow (np. szablony notatek do zamowienia).
- Sidebar Settings ma teraz blok email-templates + sms-templates obok — naturalne miejsce na kolejne typy szablonow.
**Concerns:**
- Brak preview z realnymi danymi w formularzu (paleta tylko inline). Jezeli operator bedzie chcial weryfikowac wynik przed zapisaniem, dodac endpoint `POST /settings/sms-templates/preview` z `order_id` (mirror EmailTemplateController::preview).
- `SmsTemplateRepository::save()` rzuca `RuntimeException` zamiast zwracac strukturalne bledy — controller wlapuje przez `Throwable` i przekazuje message do Flash. Dla wiekszej UX precyzji rozwazyc `IntegrationConfigException` w przyszlosci.
**Blockers:** None.