wip(14-email-templates): CRUD szablonów e-mail z Quill.js + system zmiennych

APPLY in progress — checkpoint human-verify awaiting re-test po namespace fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 00:21:01 +01:00
parent 3223aac4d9
commit 4d091b2441
11 changed files with 1258 additions and 15 deletions

View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Modules\Auth\AuthService;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Core\I18n\Translator;
use Throwable;
final class EmailTemplateController
{
private const VARIABLE_GROUPS = [
'zamowienie' => [
'label' => 'Zamowienie',
'vars' => [
'numer' => 'Numer wewnetrzny (OP...)',
'numer_zewnetrzny' => 'Numer z platformy',
'zrodlo' => 'Zrodlo (Allegro/shopPRO/...)',
'kwota' => 'Kwota brutto',
'waluta' => 'Waluta (PLN/EUR/...)',
'data' => 'Data zamowienia',
],
],
'kupujacy' => [
'label' => 'Kupujacy',
'vars' => [
'imie_nazwisko' => 'Imie i nazwisko',
'email' => 'Adres e-mail',
'telefon' => 'Telefon',
'login' => 'Login platformy',
],
],
'adres' => [
'label' => 'Adres dostawy',
'vars' => [
'ulica' => 'Ulica z numerem',
'miasto' => 'Miasto',
'kod_pocztowy' => 'Kod pocztowy',
'kraj' => 'Kraj',
],
],
'firma' => [
'label' => 'Firma',
'vars' => [
'nazwa' => 'Nazwa firmy',
'nip' => 'NIP',
],
],
];
private const SAMPLE_DATA = [
'zamowienie.numer' => 'OP000001234',
'zamowienie.numer_zewnetrzny' => 'ALG-98765432',
'zamowienie.zrodlo' => 'Allegro',
'zamowienie.kwota' => '149,99',
'zamowienie.waluta' => 'PLN',
'zamowienie.data' => '2026-03-16',
'kupujacy.imie_nazwisko' => 'Jan Kowalski',
'kupujacy.email' => 'jan.kowalski@example.com',
'kupujacy.telefon' => '+48 600 123 456',
'kupujacy.login' => 'jankowalski82',
'adres.ulica' => 'ul. Dluga 15/3',
'adres.miasto' => 'Warszawa',
'adres.kod_pocztowy' => '00-238',
'adres.kraj' => 'PL',
'firma.nazwa' => 'Przykladowa Firma Sp. z o.o.',
'firma.nip' => '5271234567',
];
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly EmailTemplateRepository $repository,
private readonly EmailMailboxRepository $mailboxRepository
) {
}
public function index(Request $request): Response
{
$t = $this->translator;
$templates = $this->repository->listAll();
$mailboxes = $this->mailboxRepository->listActive();
$editTemplate = null;
$editId = (int) $request->input('edit', '0');
if ($editId > 0) {
$editTemplate = $this->repository->findById($editId);
}
$html = $this->template->render('settings/email-templates', [
'title' => 'Szablony e-mail',
'activeMenu' => 'settings',
'activeSettings' => 'email-templates',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'templates' => $templates,
'mailboxes' => $mailboxes,
'editTemplate' => $editTemplate,
'variableGroups' => self::VARIABLE_GROUPS,
'successMessage' => Flash::get('settings.email_templates.success', ''),
'errorMessage' => Flash::get('settings.email_templates.error', ''),
], 'layouts/app');
return Response::html($html);
}
public function save(Request $request): Response
{
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings.email_templates.error', 'Nieprawidlowy token CSRF');
return Response::redirect('/settings/email-templates');
}
$name = trim((string) $request->input('name', ''));
$subject = trim((string) $request->input('subject', ''));
$bodyHtml = (string) $request->input('body_html', '');
if ($name === '' || $subject === '' || $bodyHtml === '') {
Flash::set('settings.email_templates.error', 'Nazwa, temat i tresc sa wymagane');
return Response::redirect('/settings/email-templates');
}
try {
$this->repository->save([
'id' => $request->input('id', ''),
'name' => $name,
'subject' => $subject,
'body_html' => $bodyHtml,
'mailbox_id' => $request->input('mailbox_id', ''),
'is_active' => $request->input('is_active', null),
]);
Flash::set('settings.email_templates.success', 'Szablon zostal zapisany');
} catch (Throwable) {
Flash::set('settings.email_templates.error', 'Blad zapisu szablonu');
}
return Response::redirect('/settings/email-templates');
}
public function delete(Request $request): Response
{
if (!Csrf::validate((string) $request->input('_token', ''))) {
Flash::set('settings.email_templates.error', 'Nieprawidlowy token CSRF');
return Response::redirect('/settings/email-templates');
}
$id = (int) $request->input('id', '0');
if ($id <= 0) {
Flash::set('settings.email_templates.error', 'Nieprawidlowy identyfikator szablonu');
return Response::redirect('/settings/email-templates');
}
try {
$this->repository->delete($id);
Flash::set('settings.email_templates.success', 'Szablon zostal usuniety');
} catch (Throwable) {
Flash::set('settings.email_templates.error', 'Blad usuwania szablonu');
}
return Response::redirect('/settings/email-templates');
}
public function toggleStatus(Request $request): Response
{
if (!Csrf::validate((string) $request->input('_token', ''))) {
return Response::json(['success' => false, 'message' => 'Nieprawidlowy token CSRF'], 403);
}
$id = (int) $request->input('id', '0');
if ($id <= 0) {
return Response::json(['success' => false, 'message' => 'Nieprawidlowy identyfikator'], 400);
}
try {
$this->repository->toggleStatus($id);
return Response::json(['success' => true]);
} catch (Throwable) {
return Response::json(['success' => false, 'message' => 'Blad zmiany statusu'], 500);
}
}
public function preview(Request $request): Response
{
if (!Csrf::validate((string) $request->input('_token', ''))) {
return Response::json(['success' => false, 'message' => 'Nieprawidlowy token CSRF'], 403);
}
$subject = (string) $request->input('subject', '');
$bodyHtml = (string) $request->input('body_html', '');
return Response::json([
'success' => true,
'subject' => self::resolveVariables($subject, self::SAMPLE_DATA),
'body_html' => self::resolveVariables($bodyHtml, self::SAMPLE_DATA),
]);
}
public function getVariables(Request $request): Response
{
return Response::json([
'success' => true,
'groups' => self::VARIABLE_GROUPS,
]);
}
/**
* @param array<string, string> $data
*/
public static function resolveVariables(string $text, array $data): string
{
return (string) preg_replace_callback(
'/\{\{([a-z_]+)\.([a-z_]+)\}\}/',
static function (array $matches) use ($data): string {
$key = $matches[1] . '.' . $matches[2];
return $data[$key] ?? $matches[0];
},
$text
);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use PDO;
final class EmailTemplateRepository
{
public function __construct(
private readonly PDO $pdo
) {
}
/**
* @return list<array<string, mixed>>
*/
public function listAll(): array
{
$statement = $this->pdo->prepare(
'SELECT t.id, t.name, t.subject, t.mailbox_id, t.is_active, t.created_at, t.updated_at,
m.name AS mailbox_name
FROM email_templates t
LEFT JOIN email_mailboxes m ON m.id = t.mailbox_id
ORDER BY t.name ASC'
);
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}
/**
* @return array<string, mixed>|null
*/
public function findById(int $id): ?array
{
$statement = $this->pdo->prepare(
'SELECT id, name, subject, body_html, mailbox_id, is_active, created_at, updated_at
FROM email_templates
WHERE id = :id'
);
$statement->execute(['id' => $id]);
$row = $statement->fetch(PDO::FETCH_ASSOC);
return is_array($row) ? $row : null;
}
/**
* @return list<array<string, mixed>>
*/
public function listActive(): array
{
$statement = $this->pdo->prepare(
'SELECT id, name, subject, mailbox_id
FROM email_templates
WHERE is_active = 1
ORDER BY name ASC'
);
$statement->execute();
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
return is_array($rows) ? $rows : [];
}
/**
* @param array<string, mixed> $data
*/
public function save(array $data): void
{
$id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null;
$mailboxId = isset($data['mailbox_id']) && $data['mailbox_id'] !== '' && $data['mailbox_id'] !== '0'
? (int) $data['mailbox_id']
: null;
$params = [
'name' => trim((string) ($data['name'] ?? '')),
'subject' => trim((string) ($data['subject'] ?? '')),
'body_html' => (string) ($data['body_html'] ?? ''),
'mailbox_id' => $mailboxId,
'is_active' => isset($data['is_active']) ? 1 : 0,
];
if ($id !== null) {
$params['id'] = $id;
$statement = $this->pdo->prepare(
'UPDATE email_templates
SET name = :name, subject = :subject, body_html = :body_html,
mailbox_id = :mailbox_id, is_active = :is_active
WHERE id = :id'
);
} else {
$statement = $this->pdo->prepare(
'INSERT INTO email_templates (name, subject, body_html, mailbox_id, is_active)
VALUES (:name, :subject, :body_html, :mailbox_id, :is_active)'
);
}
$statement->execute($params);
}
public function delete(int $id): void
{
$statement = $this->pdo->prepare('DELETE FROM email_templates WHERE id = :id');
$statement->execute(['id' => $id]);
}
public function toggleStatus(int $id): void
{
$statement = $this->pdo->prepare(
'UPDATE email_templates SET is_active = NOT is_active WHERE id = :id'
);
$statement->execute(['id' => $id]);
}
}