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.

View File

@@ -1,6 +1,6 @@
# Database Schema
**Updated:** 2026-05-12 | **Total tables:** 60 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
**Updated:** 2026-05-12 | **Total tables:** 61 | **Engine:** InnoDB | **Charset:** utf8mb4_unicode_ci
---
@@ -617,6 +617,18 @@ UNIQUE: `(integration_id)` - one global SMSPLANET settings row.
**notifications** - Global notification center (Phase 121): stores type, title, body, target URL, related order/SMS references, `read_at`, and `created_at`. Indexes support unread polling by `(read_at, created_at)` and relation lookups.
**sms_templates** — SMS templates for quick send from order detail (Phase 124)
| Column | Type | Nullable | Notes |
|--------|------|----------|-------|
| `id` | INT UNSIGNED | NO | PK, AUTO_INCREMENT |
| `name` | VARCHAR(200) | NO | Display name in template picker |
| `body` | TEXT | NO | Template body with `{{group.var}}` placeholders (e.g. `{{zamowienie.numer}}`, `{{przesylka.numer}}`). Footer NOT included — appended automatically by `SmsConversationService::buildFinalOutboundBody()` |
| `is_active` | TINYINT(1) | NO | DEFAULT 1 — filters listActive() for picker dropdown |
| `created_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP |
| `updated_at` | DATETIME | NO | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP |
Indexes: `sms_templates_active_name_idx (is_active, name)` — supports active-templates dropdown query.
---
## Accounting / Receipts

View File

@@ -1,5 +1,31 @@
# Technical Changelog
## 2026-05-12 - Phase 124 Plan 01: SMS Templates
**Co zrobiono:**
- Nowa tabela `sms_templates(id, name, body, is_active, created_at, updated_at)` + indeks `(is_active, name)` — migracja `20260512_000112_create_sms_templates.sql` (DDL).
- `Sms\SmsTemplateRepository` z minimalnym CRUD (`listAll/listActive/findById/save/delete/toggleStatus`); walidacja name+body w `save()`.
- `Sms\SmsVariableResolver` (wydzielony z `Email\VariableResolver`) — wspolna logika `buildVariableMap` + `resolve` dla Email i SMS (placeholdery `{{zamowienie.*|kupujacy.*|adres.*|firma.*|przesylka.*}}`).
- `Email\VariableResolver` zrefaktorowany na fasade — konstruktor opcjonalnie przyjmuje SmsVariableResolver; metody publiczne deleguja. `EmailSendingService` bez zmian.
- `Settings\SmsTemplateController` + widoki `settings/sms-templates.php` (lista) + `settings/sms-templates-form.php` (CRUD form z paleta zmiennych po prawej, licznikiem znakow, walidacja maxlength 918).
- 7 nowych rout: `GET/POST /settings/sms-templates`, `/create`, `/edit`, `/save`, `/delete`, `/toggle`, `/variables`.
- Dropdown "Wybierz szablon" w zakladce SMS na `/orders/{id}` (renderowany tylko gdy istnieja aktywne szablony) -> JS module `sms-template-picker.js` fetchuje `GET /orders/{id}/sms/template?template_id=N` i wkleja rozwiniete body do textarea. Przy niepustej textarea pyta przez `OrderProAlerts.confirm({...})` (options-object API).
- `OrdersController::smsTemplate()` — nowy endpoint JSON; rozszerzony konstruktor o `?SmsTemplateRepository`, `?SmsVariableResolver`, `?CompanySettingsRepository` (default null = BC).
- Sidebar Ustawien rozszerzony o link "Szablony SMS" (active state na `currentSettings === 'sms-templates'`).
- Nowy SCSS partial `modules/_sms-templates.scss` (paleta zmiennych, licznik znakow). Import w `app.scss`.
- Tlumaczenia `orders.details.sms.template_picker(_placeholder)` w `resources/lang/pl.php`.
**Dlaczego:**
- Operator wysyla powtarzalne SMS-y (numer sledzenia, przypomnienie o platnosci, prosba o opinie). Szablony eliminuja recznie wpisywanie tekstu i tracking number, redukujac wysylke do dropdown + ewentualnej korekty.
- Wspolny VariableResolver bo dokladnie te same placeholdery sa potrzebne w Email i SMS (DRY); zachowanie kontraktu `Email\VariableResolver` jako fasady = zero ryzyka regresji w EmailSendingService.
- Stopka SMSPLANET pozostaje doklejana wylacznie przez `SmsConversationService::buildFinalOutboundBody()` (Phase 122) — nie duplikujemy jej w szablonach, walidacja 918 znakow obowiazuje na finalnej tresci.
**Migracja:**
- `php bin/migrate.php` po wlaczeniu MySQL — utworzy `sms_templates`.
- Operator po wdrozeniu tworzy szablony manualnie z `/settings/sms-templates`.
**BREAKING:** brak. `OrdersController` ctor: nowe params optional. `Email\VariableResolver` ctor: nowy opcjonalny drugi argument (default null = self-construct SmsVariableResolver).
## 2026-05-12 - Phase 123 Plan 01: Receipts Export VAT Breakdown
**Co zrobiono:**