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

@@ -22,8 +22,11 @@ use App\Modules\Automation\AutomationService;
use App\Modules\Settings\ShopproApiClient;
use App\Modules\Settings\ShopproIntegrationsRepository;
use App\Modules\Shipments\ShipmentPackageRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Sms\SmsConversationService;
use App\Modules\Sms\SmsMessageRepository;
use App\Modules\Sms\SmsTemplateRepository;
use App\Modules\Sms\SmsVariableResolver;
use Throwable;
final class OrdersController
@@ -46,7 +49,10 @@ final class OrdersController
private readonly ?InvoiceRepository $invoiceRepo = null,
private readonly ?InvoiceConfigRepository $invoiceConfigRepo = null,
private readonly ?SmsMessageRepository $smsMessages = null,
private readonly ?SmsConversationService $smsConversation = null
private readonly ?SmsConversationService $smsConversation = null,
private readonly ?SmsTemplateRepository $smsTemplates = null,
private readonly ?SmsVariableResolver $smsVariableResolver = null,
private readonly ?CompanySettingsRepository $companySettingsRepo = null
) {
}
@@ -255,6 +261,7 @@ final class OrdersController
$smsMessages = $this->smsMessages !== null ? $this->smsMessages->findByOrderId($orderId) : [];
$smsPhone = $this->resolveSmsPhone($order, $addresses);
$smsDefaultFooterConfigured = $this->smsConversation !== null && $this->smsConversation->hasDefaultFooter();
$smsTemplates = $this->smsTemplates !== null ? $this->smsTemplates->listActive() : [];
$html = $this->template->render('orders/show', [
'title' => $this->translator->get('orders.details.title') . ' #' . $orderId,
@@ -290,6 +297,7 @@ final class OrdersController
'smsMessages' => $smsMessages,
'smsPhone' => $smsPhone,
'smsDefaultFooterConfigured' => $smsDefaultFooterConfigured,
'smsTemplates' => $smsTemplates,
], 'layouts/app');
return Response::html($html);
@@ -331,6 +339,44 @@ final class OrdersController
return Response::redirect($redirectTo);
}
public function smsTemplate(Request $request): Response
{
$orderId = max(0, (int) $request->input('id', 0));
$templateId = max(0, (int) $request->input('template_id', 0));
if ($orderId <= 0 || $templateId <= 0) {
return Response::json(['ok' => false, 'error' => 'Nieprawidlowe parametry.'], 400);
}
if ($this->smsTemplates === null || $this->smsVariableResolver === null) {
return Response::json(['ok' => false, 'error' => 'Modul szablonow SMS nie jest dostepny.'], 500);
}
$template = $this->smsTemplates->findById($templateId);
if ($template === null || (int) ($template['is_active'] ?? 0) !== 1) {
return Response::json(['ok' => false, 'error' => 'Szablon nie istnieje albo jest nieaktywny.'], 404);
}
$details = $this->orders->findDetails($orderId);
if ($details === null) {
return Response::json(['ok' => false, 'error' => 'Zamowienie nie znalezione.'], 404);
}
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
$companySettings = $this->companySettingsRepo !== null
? $this->companySettingsRepo->getSettings()
: [];
$variableMap = $this->smsVariableResolver->buildVariableMap($order, $addresses, $companySettings);
$resolvedBody = $this->smsVariableResolver->resolve((string) ($template['body'] ?? ''), $variableMap);
return Response::json([
'ok' => true,
'body' => $resolvedBody,
'name' => (string) ($template['name'] ?? ''),
]);
}
/**
* Sklada informacje o historii zwrotow klienta biezacego zamowienia.
*