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:
228
src/Modules/Settings/EmailTemplateController.php
Normal file
228
src/Modules/Settings/EmailTemplateController.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
117
src/Modules/Settings/EmailTemplateRepository.php
Normal file
117
src/Modules/Settings/EmailTemplateRepository.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user