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:
91
.paul/HANDOFF-2026-03-16.md
Normal file
91
.paul/HANDOFF-2026-03-16.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# PAUL Handoff
|
||||
|
||||
**Date:** 2026-03-16
|
||||
**Status:** paused — checkpoint human-verify in progress
|
||||
|
||||
---
|
||||
|
||||
## READ THIS FIRST
|
||||
|
||||
You have no prior context. This document tells you everything.
|
||||
|
||||
**Project:** orderPRO — aplikacja do zarządzania zamówieniami z wielu platform
|
||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**Milestone:** v0.4 Moduł E-mail
|
||||
**Phase:** 14 of 3 (in milestone) — Szablony wiadomości e-mail
|
||||
**Plan:** 14-01 — APPLY in progress (checkpoint human-verify)
|
||||
|
||||
**Loop Position:**
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ◐ ○ [APPLY in progress — Task 1+2 done, Task 3 checkpoint awaiting approval]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
- Task 1: Created `EmailTemplateRepository` + `EmailTemplateController` + routes (6 endpoints)
|
||||
- Task 2: Created view `email-templates.php` with Quill.js CDN editor, variable panel, preview modal, AJAX toggle
|
||||
- Added sidebar link "Szablony e-mail" in `layouts/app.php`
|
||||
- Compiled SCSS (modal-overlay, email-tpl-editor styles)
|
||||
- Fixed 2 bugs discovered during deploy:
|
||||
- `AuthService` namespace: `App\Core\Auth\AuthService` → `App\Modules\Auth\AuthService`
|
||||
- `Flash` namespace: `App\Core\Session\Flash` → `App\Core\Support\Flash`
|
||||
|
||||
---
|
||||
|
||||
## What's In Progress
|
||||
|
||||
- **Task 3 checkpoint:human-verify** — user was testing the deployed page when session paused
|
||||
- User reported 2 namespace errors which were fixed, needs to re-test
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
**Immediate:** User re-tests `/settings/email-templates` after namespace fixes. If approved → APPLY complete.
|
||||
|
||||
**After that:** Run `sonar-scanner` (required skill), then `/paul:unify .paul/phases/14-email-templates/14-01-PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.paul/STATE.md` | Live project state |
|
||||
| `.paul/ROADMAP.md` | Phase overview |
|
||||
| `.paul/phases/14-email-templates/14-01-PLAN.md` | Current plan |
|
||||
| `src/Modules/Settings/EmailTemplateController.php` | Controller (CRUD + preview + variables) |
|
||||
| `src/Modules/Settings/EmailTemplateRepository.php` | Repository (DB operations) |
|
||||
| `resources/views/settings/email-templates.php` | View (list + form + Quill.js + variable panel) |
|
||||
| `routes/web.php` | Routes (6 new endpoints) |
|
||||
| `resources/views/layouts/app.php` | Sidebar link added |
|
||||
| `resources/scss/app.scss` | Styles (modal-overlay, email-tpl-editor) |
|
||||
|
||||
---
|
||||
|
||||
## Namespace Fixes Applied
|
||||
|
||||
These were wrong in the initial controller and have been corrected:
|
||||
- `use App\Core\Auth\AuthService` → `use App\Modules\Auth\AuthService`
|
||||
- `use App\Core\Session\Flash` → `use App\Core\Support\Flash`
|
||||
|
||||
---
|
||||
|
||||
## Resume Instructions
|
||||
|
||||
1. Read `.paul/STATE.md` for latest position
|
||||
2. Check loop position — APPLY in progress, Task 3 checkpoint
|
||||
3. Run `/paul:resume` or ask user to re-test `/settings/email-templates`
|
||||
4. On approval → finalize APPLY → sonar-scanner → `/paul:unify`
|
||||
|
||||
---
|
||||
|
||||
*Handoff created: 2026-03-16*
|
||||
@@ -13,7 +13,7 @@ Skrzynki pocztowe SMTP, szablony wiadomości z systemem zmiennych (Quill.js), wy
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 13 | DB + Skrzynki pocztowe | 1/1 | Complete ✓ |
|
||||
| 14 | Szablony wiadomości | TBD | Not started |
|
||||
| 14 | Szablony wiadomości | 0/1 | Planning |
|
||||
| 15 | Wysyłka e-mail z zamówień | TBD | Not started |
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
@@ -10,10 +10,10 @@ See: .paul/PROJECT.md (updated 2026-03-12)
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.4 Moduł E-mail
|
||||
Phase: [1] of [3] (DB + Skrzynki pocztowe) — Complete
|
||||
Plan: 13-01 complete
|
||||
Status: Phase 13 complete, ready for Phase 14
|
||||
Last activity: 2026-03-15 — UNIFY 13-01 complete, phase transition done
|
||||
Phase: [2] of [3] (Szablony wiadomości) — Planning
|
||||
Plan: 14-01 created, awaiting approval
|
||||
Status: PLAN created, ready for APPLY
|
||||
Last activity: 2026-03-16 — Created .paul/phases/14-email-templates/14-01-PLAN.md
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [██████████] 100% ✓
|
||||
@@ -21,7 +21,7 @@ Progress:
|
||||
- v0.3 Moduł Paragonów: [██████████] 100% ✓
|
||||
- v0.4 Moduł E-mail: [███░░░░░░░] 33%
|
||||
- Phase 13: [██████████] 100% ✓
|
||||
- Phase 14: [░░░░░░░░░░] 0%
|
||||
- Phase 14: [░░░░░░░░░░] 0% ← planning
|
||||
- Phase 15: [░░░░░░░░░░] 0%
|
||||
|
||||
## Loop Position
|
||||
@@ -29,7 +29,7 @@ Progress:
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete — Phase 13 done, ready for Phase 14]
|
||||
✓ ◐ ○ [APPLY in progress — Task 3 checkpoint awaiting approval]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
@@ -147,7 +147,7 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
|
||||
|
||||
### Git State
|
||||
Last commit: 22fc330 (feat(11-12-accounting): phases 11-12 complete — milestone v0.3 done)
|
||||
Last commit: 3223aac (feat(13-email-mailboxes): phase 13 complete — email DB foundation + SMTP mailbox CRUD)
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
@@ -156,16 +156,17 @@ Brak.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-15
|
||||
Stopped at: Phase 13 complete
|
||||
Next action: /paul:plan for Phase 14 (Szablony wiadomości)
|
||||
Resume file: .paul/phases/13-email-mailboxes/13-01-SUMMARY.md
|
||||
Last session: 2026-03-16
|
||||
Stopped at: APPLY 14-01 in progress — Task 3 checkpoint human-verify (2 namespace bugs fixed, awaiting re-test)
|
||||
Next action: User re-tests /settings/email-templates → if approved → finalize APPLY → sonar-scanner → /paul:unify
|
||||
Resume file: .paul/HANDOFF-2026-03-16.md
|
||||
Resume context:
|
||||
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
||||
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
||||
- v0.3: COMPLETE ✓ (5 phases, 5 plans) — Moduł Paragonów
|
||||
- v0.4: IN PROGRESS — Phase 13 complete, Phase 14 next
|
||||
- v0.4: IN PROGRESS — Phase 13 complete, Phase 14 APPLY checkpoint
|
||||
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
||||
- 2 namespace fixes applied: AuthService, Flash
|
||||
|
||||
---
|
||||
*STATE.md — Updated after every significant action*
|
||||
|
||||
319
.paul/phases/14-email-templates/14-01-PLAN.md
Normal file
319
.paul/phases/14-email-templates/14-01-PLAN.md
Normal file
@@ -0,0 +1,319 @@
|
||||
---
|
||||
phase: 14-email-templates
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["13-01"]
|
||||
files_modified:
|
||||
- src/Modules/Settings/EmailTemplateController.php
|
||||
- src/Modules/Settings/EmailTemplateRepository.php
|
||||
- resources/views/settings/email-templates.php
|
||||
- resources/views/settings/email-template-form.php
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
- resources/scss/app.scss
|
||||
- public/assets/css/app.css
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
CRUD szablonów wiadomości e-mail z edytorem rich-text (Quill.js) i systemem zmiennych do personalizacji treści (np. {{order.number}}, {{buyer.name}}).
|
||||
|
||||
## Purpose
|
||||
Użytkownik musi mieć możliwość tworzenia i zarządzania szablonami e-mail, które w fazie 15 będą używane do wysyłki wiadomości z poziomu zamówień. Szablony muszą obsługiwać zmienne dynamiczne rozwiązywane z danych zamówienia.
|
||||
|
||||
## Output
|
||||
- EmailTemplateController + EmailTemplateRepository (CRUD)
|
||||
- Widok listy szablonów + formularz edycji z Quill.js
|
||||
- System zmiennych: definicja dostępnych zmiennych, wstawianie z UI, podgląd
|
||||
- Link w sidebarze Settings
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/13-email-mailboxes/13-01-SUMMARY.md
|
||||
- email_templates table already created (Phase 13)
|
||||
- EmailMailboxRepository::listActive() ready for mailbox select
|
||||
- Pattern: EmailMailboxController CRUD (same structure to follow)
|
||||
|
||||
## Source Files
|
||||
@database/migrations/20260315_000055_create_email_templates_table.sql
|
||||
@src/Modules/Settings/EmailMailboxController.php
|
||||
@src/Modules/Settings/EmailMailboxRepository.php
|
||||
@resources/views/settings/email-mailboxes.php
|
||||
@routes/web.php
|
||||
@resources/views/layouts/app.php
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner uruchomiony po APPLY
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Lista szablonów
|
||||
```gherkin
|
||||
Given użytkownik jest zalogowany i przechodzi do Ustawienia > Szablony e-mail
|
||||
When strona się ładuje
|
||||
Then widzi tabelę z kolumnami: Nazwa, Temat, Skrzynka, Status, Akcje
|
||||
And każdy szablon ma przyciski: Edytuj, Włącz/Wyłącz, Usuń
|
||||
```
|
||||
|
||||
## AC-2: Tworzenie i edycja szablonu
|
||||
```gherkin
|
||||
Given użytkownik jest na stronie szablonów e-mail
|
||||
When wypełnia formularz (nazwa, temat, skrzynka, treść HTML) i zapisuje
|
||||
Then szablon jest zapisany w bazie z poprawnym body_html z Quill.js
|
||||
And po edycji istniejącego szablonu treść Quill jest załadowana poprawnie
|
||||
```
|
||||
|
||||
## AC-3: Edytor Quill.js z toolbarem
|
||||
```gherkin
|
||||
Given użytkownik edytuje treść szablonu
|
||||
When korzysta z edytora
|
||||
Then dostępne są: bold, italic, underline, listy, linki, nagłówki, kolory, wyrównanie
|
||||
And treść jest zapisywana jako HTML w polu body_html
|
||||
```
|
||||
|
||||
## AC-4: System zmiennych — wstawianie
|
||||
```gherkin
|
||||
Given użytkownik edytuje szablon
|
||||
When klika przycisk "Wstaw zmienną" i wybiera zmienną z listy (np. {{zamowienie.numer}})
|
||||
Then zmienna jest wstawiana do treści edytora w miejscu kursora
|
||||
And zmienne są wizualnie wyróżnione w edytorze (np. badge/tag)
|
||||
```
|
||||
|
||||
## AC-5: System zmiennych — definicja
|
||||
```gherkin
|
||||
Given system zmiennych jest zaimplementowany
|
||||
When użytkownik otwiera listę zmiennych
|
||||
Then widzi pogrupowane zmienne:
|
||||
- Zamówienie: numer, źródło, kwota, waluta, data
|
||||
- Kupujący: imię i nazwisko, email, telefon, login
|
||||
- Adres dostawy: ulica, miasto, kod pocztowy, kraj
|
||||
- Firma: nazwa, NIP (jeśli faktura)
|
||||
And każda zmienna ma opis i placeholder (np. {{zamowienie.numer}})
|
||||
```
|
||||
|
||||
## AC-6: Toggle statusu i usuwanie
|
||||
```gherkin
|
||||
Given szablon istnieje na liście
|
||||
When użytkownik klika Włącz/Wyłącz
|
||||
Then status is_active się zmienia (toggle AJAX)
|
||||
When użytkownik klika Usuń i potwierdzi (OrderProAlerts.confirm)
|
||||
Then szablon jest usunięty z bazy
|
||||
```
|
||||
|
||||
## AC-7: Podgląd szablonu z przykładowymi danymi
|
||||
```gherkin
|
||||
Given użytkownik edytuje szablon z zmiennymi
|
||||
When klika przycisk "Podgląd"
|
||||
Then widzi modal z treścią szablonu gdzie zmienne są zastąpione przykładowymi danymi
|
||||
And temat wiadomości jest również rozwiązany z przykładowymi danymi
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: EmailTemplateRepository + EmailTemplateController + routes</name>
|
||||
<files>
|
||||
src/Modules/Settings/EmailTemplateRepository.php,
|
||||
src/Modules/Settings/EmailTemplateController.php,
|
||||
routes/web.php
|
||||
</files>
|
||||
<action>
|
||||
**EmailTemplateRepository** (wzorowany na EmailMailboxRepository):
|
||||
- listAll(): array — pobiera wszystkie szablony JOIN email_mailboxes (nazwa skrzynki), ORDER BY name
|
||||
- findById(int $id): ?array — jeden szablon po ID
|
||||
- save(array $data): void — INSERT lub UPDATE (id > 0 = update)
|
||||
- delete(int $id): void — DELETE po ID
|
||||
- toggleStatus(int $id): void — toggle is_active (UPDATE SET is_active = NOT is_active)
|
||||
- listActive(): array — szablony z is_active=1 (do użycia w fazie 15)
|
||||
- Medoo + prepared statements, bez surowego SQL
|
||||
|
||||
**EmailTemplateController** (wzorowany na EmailMailboxController):
|
||||
- __construct(TemplateEngine, Translator, Auth, EmailTemplateRepository, EmailMailboxRepository)
|
||||
- index(Request): Response — lista szablonów + formularz (edit mode jeśli ?edit=ID)
|
||||
- save(Request): Response — walidacja (name, subject, body_html required) + CSRF + zapis + redirect z Flash
|
||||
- delete(Request): Response — CSRF + usunięcie + redirect z Flash
|
||||
- toggleStatus(Request): Response — AJAX POST, JSON response {success: true}
|
||||
- preview(Request): Response — AJAX POST, przyjmuje subject + body_html, zwraca JSON z resolved variables (przykładowe dane)
|
||||
- getVariables(Request): Response — AJAX GET, zwraca JSON z definicją dostępnych zmiennych pogrupowanych
|
||||
|
||||
**Definicja zmiennych** (statyczna metoda lub stała w kontrolerze):
|
||||
```
|
||||
Zamówienie:
|
||||
{{zamowienie.numer}} — Numer wewnętrzny (OP...)
|
||||
{{zamowienie.numer_zewnetrzny}} — Numer z platformy
|
||||
{{zamowienie.zrodlo}} — Źródło (Allegro/shopPRO/...)
|
||||
{{zamowienie.kwota}} — Kwota brutto
|
||||
{{zamowienie.waluta}} — Waluta (PLN/EUR/...)
|
||||
{{zamowienie.data}} — Data zamówienia
|
||||
Kupujący:
|
||||
{{kupujacy.imie_nazwisko}} — Imię i nazwisko
|
||||
{{kupujacy.email}} — Adres e-mail
|
||||
{{kupujacy.telefon}} — Telefon
|
||||
{{kupujacy.login}} — Login platformy
|
||||
Adres dostawy:
|
||||
{{adres.ulica}} — Ulica z numerem
|
||||
{{adres.miasto}} — Miasto
|
||||
{{adres.kod_pocztowy}} — Kod pocztowy
|
||||
{{adres.kraj}} — Kraj
|
||||
Firma:
|
||||
{{firma.nazwa}} — Nazwa firmy
|
||||
{{firma.nip}} — NIP
|
||||
```
|
||||
|
||||
**Metoda resolveVariables(string $text, array $sampleData): string**
|
||||
- Zamienia {{klucz.pole}} na wartości z $sampleData
|
||||
- Dla podglądu: używa przykładowych (hardcoded) danych
|
||||
- Dla wysyłki (faza 15): otrzyma prawdziwe dane zamówienia
|
||||
|
||||
**Routes** (dodać w web.php obok email-mailboxes):
|
||||
- GET /settings/email-templates → index
|
||||
- POST /settings/email-templates/save → save
|
||||
- POST /settings/email-templates/delete → delete
|
||||
- POST /settings/email-templates/toggle → toggleStatus
|
||||
- POST /settings/email-templates/preview → preview (AJAX)
|
||||
- GET /settings/email-templates/variables → getVariables (AJAX)
|
||||
</action>
|
||||
<verify>
|
||||
- Klasy się ładują bez Fatal Error (PHP syntax check)
|
||||
- Routes zarejestrowane poprawnie
|
||||
- Strona /settings/email-templates renderuje się bez błędów
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-5, AC-6, AC-7 backend satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Widoki — lista szablonów + formularz z Quill.js + system zmiennych UI</name>
|
||||
<files>
|
||||
resources/views/settings/email-templates.php,
|
||||
resources/views/settings/email-template-form.php,
|
||||
resources/views/layouts/app.php,
|
||||
resources/scss/app.scss,
|
||||
public/assets/css/app.css
|
||||
</files>
|
||||
<action>
|
||||
**Widok listy** (resources/views/settings/email-templates.php):
|
||||
- Tabela: Nazwa, Temat, Skrzynka (nazwa lub "—"), Status (badge), Akcje
|
||||
- Przycisk "Edytuj" → ?edit=ID
|
||||
- Toggle status: AJAX POST do /settings/email-templates/toggle (jak w mailboxes)
|
||||
- Usuwanie: window.OrderProAlerts.confirm() → POST /settings/email-templates/delete
|
||||
- Flash messages (success/error)
|
||||
- Przycisk "Nowy szablon" → przejście do formularza
|
||||
|
||||
**Formularz edycji** (osobny plik lub sekcja w tym samym widoku):
|
||||
- Pola: nazwa (input), temat (input — z możliwością wpisania zmiennych ręcznie), skrzynka (select z listActive), status (checkbox)
|
||||
- Edytor Quill.js:
|
||||
- Załadować z CDN: quill@2 (https://cdn.quilljs.com/2.0.3/quill.snow.css + quill.js)
|
||||
- Toolbar: bold, italic, underline, strike, headers (h1-h3), lists (ordered/bullet), link, color, background, align
|
||||
- Hidden input #body_html synchronizowany z Quill content on form submit
|
||||
- Panel zmiennych (sidebar lub dropdown):
|
||||
- Przycisk "Wstaw zmienną" otwiera listę pogrupowaną (pobrana AJAX z /variables)
|
||||
- Klik na zmienną → wstawia {{zmienna.pole}} w miejsce kursora w Quill
|
||||
- Przycisk "Podgląd":
|
||||
- AJAX POST do /settings/email-templates/preview z subject + body_html
|
||||
- Wynik w modalu (resolved subject + resolved body_html)
|
||||
- Przycisk "Zapisz" → submit formularza
|
||||
|
||||
**Sidebar** (resources/views/layouts/app.php):
|
||||
- Dodać link "Szablony e-mail" pod "Skrzynki pocztowe" w sekcji Settings
|
||||
- activeSettings === 'email-templates'
|
||||
|
||||
**Style** (resources/scss/app.scss → public/assets/css/app.css):
|
||||
- Styl dla kontenera Quill (.ql-editor min-height: 200px)
|
||||
- Styl dla panelu zmiennych (.email-var-list, .email-var-group, .email-var-item)
|
||||
- Styl dla modala podglądu (reuse istniejący .modal jeśli jest, lub prosty modal)
|
||||
- Kompaktowy layout zgodnie z zasadami projektu
|
||||
|
||||
**Nie używać** natywnych alert()/confirm() — tylko OrderProAlerts.
|
||||
**Quill CDN** — nie dodawać jako zależność npm/composer, wystarczy CDN w widoku.
|
||||
</action>
|
||||
<verify>
|
||||
- Quill.js ładuje się i renderuje edytor
|
||||
- Formularz zapisuje poprawnie (body_html zawiera HTML z Quill)
|
||||
- Zmienne wstawiane w edytor poprawnie
|
||||
- Podgląd wyświetla resolved template
|
||||
- SCSS skompilowane do CSS
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3, AC-4, AC-5, AC-6, AC-7 UI satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>CRUD szablonów e-mail z Quill.js i systemem zmiennych</what-built>
|
||||
<how-to-verify>
|
||||
1. Otwórz: /settings/email-templates
|
||||
2. Sprawdź: link "Szablony e-mail" widoczny w sidebarze
|
||||
3. Kliknij "Nowy szablon":
|
||||
a. Wypełnij nazwę, temat (np. "Potwierdzenie zamówienia {{zamowienie.numer}}")
|
||||
b. Wybierz skrzynkę z listy
|
||||
c. W Quill wpisz treść, użyj formatowania (bold, listy)
|
||||
d. Kliknij "Wstaw zmienną" → wybierz np. {{kupujacy.imie_nazwisko}}
|
||||
e. Kliknij "Podgląd" → sprawdź czy zmienne zamienione na przykładowe dane
|
||||
f. Zapisz szablon
|
||||
4. Na liście: sprawdź czy szablon się pojawił z poprawną nazwą/tematem/skrzynką
|
||||
5. Edytuj szablon — sprawdź czy Quill załadował treść HTML poprawnie
|
||||
6. Toggle status (włącz/wyłącz) — badge powinien się zmienić
|
||||
7. Usuń szablon — potwierdzenie OrderProAlerts, usunięcie z listy
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- database/migrations/* (tabela email_templates już istnieje z fazy 13)
|
||||
- src/Modules/Settings/EmailMailboxController.php (tylko import/reuse)
|
||||
- src/Modules/Settings/EmailMailboxRepository.php (tylko wywołanie listActive)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Nie implementować wysyłki e-mail (faza 15)
|
||||
- Nie dodawać PHPMailer/SwiftMailer (faza 15)
|
||||
- Nie modyfikować tabeli email_templates (schemat zamrożony)
|
||||
- Quill.js z CDN — bez npm/bundlera
|
||||
- Zmienne rozwiązywane tylko z przykładowymi danymi (prawdziwe dane zamówienia w fazie 15)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] /settings/email-templates renderuje się bez błędów PHP
|
||||
- [ ] CRUD: create, read, update, delete szablonów działa
|
||||
- [ ] Quill.js edytor ładuje się i zapisuje HTML
|
||||
- [ ] System zmiennych: lista, wstawianie, podgląd z przykładowymi danymi
|
||||
- [ ] Toggle statusu via AJAX działa
|
||||
- [ ] Usuwanie z potwierdzeniem OrderProAlerts działa
|
||||
- [ ] Link w sidebarze Settings aktywny
|
||||
- [ ] SCSS skompilowane, brak inline styles w widokach
|
||||
- [ ] Brak natywnych alert()/confirm()
|
||||
- [ ] CSRF walidowany we wszystkich POST
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie AC-1 do AC-7 spełnione
|
||||
- Wszystkie taski zakończone
|
||||
- Checkpoint human-verify approved
|
||||
- Brak błędów PHP, brak broken UI
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/14-email-templates/14-01-SUMMARY.md`
|
||||
</output>
|
||||
File diff suppressed because one or more lines are too long
@@ -105,8 +105,12 @@ a {
|
||||
}
|
||||
|
||||
.sidebar__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
white-space: nowrap;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 9px 10px;
|
||||
text-decoration: none;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
@@ -2303,5 +2307,158 @@ h4.section-title {
|
||||
.modal--image-preview {
|
||||
width: min(92vw, 100%);
|
||||
}
|
||||
|
||||
.email-tpl-editor-wrap {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.email-tpl-var-panel {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
width: 95vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
// Email template editor
|
||||
.email-tpl-editor-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.email-tpl-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--c-bg-subtle, #f8f9fa);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.email-tpl-var-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.email-tpl-var-panel {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
min-width: 260px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
background: var(--c-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
padding: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.email-var-group {
|
||||
&:not(:first-child) {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-text-muted);
|
||||
padding: 2px 4px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
}
|
||||
|
||||
.email-var-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 3px 6px;
|
||||
margin: 1px 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 12px;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
color: var(--c-text);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--c-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
#js-quill-editor {
|
||||
min-height: 200px;
|
||||
|
||||
.ql-editor {
|
||||
min-height: 200px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal overlay (reusable)
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.modal-box {
|
||||
width: min(680px, 90vw);
|
||||
max-height: 80vh;
|
||||
background: var(--c-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: var(--c-text-muted);
|
||||
padding: 0 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--c-text);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'email-mailboxes' ? ' is-active' : '' ?>" href="/settings/email-mailboxes">
|
||||
Skrzynki pocztowe
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'email-templates' ? ' is-active' : '' ?>" href="/settings/email-templates">
|
||||
Szablony e-mail
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
311
resources/views/settings/email-templates.php
Normal file
311
resources/views/settings/email-templates.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
$templates = is_array($templates ?? null) ? $templates : [];
|
||||
$mailboxes = is_array($mailboxes ?? null) ? $mailboxes : [];
|
||||
$et = is_array($editTemplate ?? null) ? $editTemplate : null;
|
||||
$isEdit = $et !== null;
|
||||
$variableGroups = is_array($variableGroups ?? null) ? $variableGroups : [];
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title">Szablony e-mail</h2>
|
||||
<p class="muted mt-12">Szablony wiadomosci e-mail z edytorem i systemem zmiennych.</p>
|
||||
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($successMessage)): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Lista szablonow</h3>
|
||||
|
||||
<?php if (count($templates) === 0): ?>
|
||||
<p class="muted mt-12">Brak szablonow. Dodaj pierwszy szablon ponizej.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>Temat</th>
|
||||
<th>Skrzynka</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<tr data-id="<?= (int) ($tpl['id'] ?? 0) ?>">
|
||||
<td><?= $e((string) ($tpl['name'] ?? '')) ?></td>
|
||||
<td><?= $e((string) ($tpl['subject'] ?? '')) ?></td>
|
||||
<td><?= $e((string) ($tpl['mailbox_name'] ?? '—')) ?></td>
|
||||
<td>
|
||||
<?php if (((int) ($tpl['is_active'] ?? 0)) === 1): ?>
|
||||
<span class="badge badge--success js-status-badge">Aktywny</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted js-status-badge">Nieaktywny</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/email-templates?edit=<?= (int) ($tpl['id'] ?? 0) ?>" class="btn btn--sm btn--secondary">Edytuj</a>
|
||||
<button type="button" class="btn btn--sm btn--secondary js-toggle-btn"
|
||||
data-id="<?= (int) ($tpl['id'] ?? 0) ?>"
|
||||
data-active="<?= (int) ($tpl['is_active'] ?? 0) ?>">
|
||||
<?= ((int) ($tpl['is_active'] ?? 0)) === 1 ? 'Dezaktywuj' : 'Aktywuj' ?>
|
||||
</button>
|
||||
<form action="/settings/email-templates/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($tpl['id'] ?? 0) ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $isEdit ? 'Edytuj szablon' : 'Dodaj szablon' ?></h3>
|
||||
|
||||
<form action="/settings/email-templates/save" method="post" novalidate class="mt-12" id="js-template-form">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="body_html" id="js-body-html" value="<?= $e((string) ($et['body_html'] ?? '')) ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int) ($et['id'] ?? 0) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Nazwa szablonu *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="200" required value="<?= $e((string) ($et['name'] ?? '')) ?>" placeholder="np. Potwierdzenie zamowienia">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Skrzynka nadawcza</span>
|
||||
<select class="form-control" name="mailbox_id">
|
||||
<option value="">— domyslna —</option>
|
||||
<?php foreach ($mailboxes as $mb): ?>
|
||||
<option value="<?= (int) ($mb['id'] ?? 0) ?>"<?= ((int) ($et['mailbox_id'] ?? 0)) === (int) ($mb['id'] ?? 0) ? ' selected' : '' ?>>
|
||||
<?= $e((string) ($mb['name'] ?? '')) ?> (<?= $e((string) ($mb['sender_email'] ?? '')) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Temat wiadomosci *</span>
|
||||
<input class="form-control" type="text" name="subject" maxlength="500" required value="<?= $e((string) ($et['subject'] ?? '')) ?>" placeholder="np. Potwierdzenie zamowienia {{zamowienie.numer}}">
|
||||
</label>
|
||||
<div class="form-field" style="display:flex;align-items:flex-end;gap:8px">
|
||||
<label style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isEdit ? (((int) ($et['is_active'] ?? 0)) === 1 ? ' checked' : '') : ' checked' ?>>
|
||||
<span class="field-label" style="margin:0">Aktywny</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12">
|
||||
<span class="field-label">Tresc wiadomosci *</span>
|
||||
<div class="email-tpl-editor-wrap mt-4">
|
||||
<div class="email-tpl-toolbar">
|
||||
<div class="email-tpl-var-dropdown">
|
||||
<button type="button" class="btn btn--sm btn--secondary" id="js-var-toggle">Wstaw zmienna ▾</button>
|
||||
<div class="email-tpl-var-panel" id="js-var-panel" style="display:none">
|
||||
<?php foreach ($variableGroups as $groupKey => $group): ?>
|
||||
<div class="email-var-group">
|
||||
<div class="email-var-group__label"><?= $e((string) ($group['label'] ?? '')) ?></div>
|
||||
<?php foreach (($group['vars'] ?? []) as $varKey => $varDesc): ?>
|
||||
<button type="button" class="email-var-item" data-var="{{<?= $e($groupKey . '.' . $varKey) ?>}}" title="<?= $e((string) $varDesc) ?>">
|
||||
{{<?= $e($groupKey . '.' . $varKey) ?>}}
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn--sm btn--secondary" id="js-preview-btn">Podglad</button>
|
||||
</div>
|
||||
<div id="js-quill-editor"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj szablon' ?></button>
|
||||
<?php if ($isEdit): ?>
|
||||
<a href="/settings/email-templates" class="btn btn--secondary">Anuluj</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="modal-overlay" id="js-preview-overlay" style="display:none">
|
||||
<div class="modal-box">
|
||||
<div class="modal-box__header">
|
||||
<h3 class="modal-box__title">Podglad szablonu</h3>
|
||||
<button type="button" class="modal-box__close" id="js-preview-close">×</button>
|
||||
</div>
|
||||
<div class="modal-box__body">
|
||||
<div class="mt-4"><strong>Temat:</strong> <span id="js-preview-subject"></span></div>
|
||||
<hr class="mt-8 mb-8">
|
||||
<div id="js-preview-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href="https://cdn.quilljs.com/2.0.3/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.quilljs.com/2.0.3/quill.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var csrfToken = <?= json_encode($csrfToken ?? '', JSON_HEX_TAG) ?>;
|
||||
|
||||
// Quill editor
|
||||
var quill = new Quill('#js-quill-editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link'],
|
||||
['clean']
|
||||
]
|
||||
},
|
||||
placeholder: 'Wpisz tresc wiadomosci...'
|
||||
});
|
||||
|
||||
// Load existing content
|
||||
var existingHtml = document.getElementById('js-body-html').value;
|
||||
if (existingHtml) {
|
||||
quill.root.innerHTML = existingHtml;
|
||||
}
|
||||
|
||||
// Sync on submit
|
||||
var form = document.getElementById('js-template-form');
|
||||
form.addEventListener('submit', function() {
|
||||
document.getElementById('js-body-html').value = quill.root.innerHTML;
|
||||
});
|
||||
|
||||
// Variable panel toggle
|
||||
var varToggle = document.getElementById('js-var-toggle');
|
||||
var varPanel = document.getElementById('js-var-panel');
|
||||
varToggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
varPanel.style.display = varPanel.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!varPanel.contains(e.target) && e.target !== varToggle) {
|
||||
varPanel.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Insert variable into Quill
|
||||
varPanel.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.email-var-item');
|
||||
if (!btn) return;
|
||||
var varText = btn.getAttribute('data-var');
|
||||
var range = quill.getSelection(true);
|
||||
quill.insertText(range.index, varText);
|
||||
quill.setSelection(range.index + varText.length);
|
||||
varPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Preview
|
||||
var previewBtn = document.getElementById('js-preview-btn');
|
||||
var previewOverlay = document.getElementById('js-preview-overlay');
|
||||
var previewClose = document.getElementById('js-preview-close');
|
||||
var previewSubject = document.getElementById('js-preview-subject');
|
||||
var previewBody = document.getElementById('js-preview-body');
|
||||
|
||||
previewBtn.addEventListener('click', function() {
|
||||
var subjectVal = form.querySelector('[name="subject"]').value;
|
||||
var bodyVal = quill.root.innerHTML;
|
||||
|
||||
previewBtn.disabled = true;
|
||||
previewBtn.textContent = 'Ladowanie...';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('_token', csrfToken);
|
||||
fd.append('subject', subjectVal);
|
||||
fd.append('body_html', bodyVal);
|
||||
|
||||
fetch('/settings/email-templates/preview', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
previewSubject.textContent = data.subject;
|
||||
previewBody.innerHTML = data.body_html;
|
||||
previewOverlay.style.display = 'flex';
|
||||
}
|
||||
})
|
||||
.catch(function() {})
|
||||
.finally(function() {
|
||||
previewBtn.disabled = false;
|
||||
previewBtn.textContent = 'Podglad';
|
||||
});
|
||||
});
|
||||
|
||||
previewClose.addEventListener('click', function() {
|
||||
previewOverlay.style.display = 'none';
|
||||
});
|
||||
previewOverlay.addEventListener('click', function(e) {
|
||||
if (e.target === previewOverlay) previewOverlay.style.display = 'none';
|
||||
});
|
||||
|
||||
// Toggle status (AJAX)
|
||||
document.querySelectorAll('.js-toggle-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var id = this.getAttribute('data-id');
|
||||
var isActive = this.getAttribute('data-active') === '1';
|
||||
var toggleBtn = this;
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('_token', csrfToken);
|
||||
fd.append('id', id);
|
||||
|
||||
toggleBtn.disabled = true;
|
||||
fetch('/settings/email-templates/toggle', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
var newActive = !isActive;
|
||||
toggleBtn.setAttribute('data-active', newActive ? '1' : '0');
|
||||
toggleBtn.textContent = newActive ? 'Dezaktywuj' : 'Aktywuj';
|
||||
var badge = toggleBtn.closest('tr').querySelector('.js-status-badge');
|
||||
if (badge) {
|
||||
badge.textContent = newActive ? 'Aktywny' : 'Nieaktywny';
|
||||
badge.className = 'badge js-status-badge ' + (newActive ? 'badge--success' : 'badge--muted');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {})
|
||||
.finally(function() { toggleBtn.disabled = false; });
|
||||
});
|
||||
});
|
||||
|
||||
// Delete with OrderProAlerts
|
||||
document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var delForm = this.closest('form');
|
||||
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||
window.OrderProAlerts.confirm(
|
||||
'Usuwanie szablonu',
|
||||
'Czy na pewno chcesz usunac ten szablon e-mail?',
|
||||
function() { delForm.submit(); }
|
||||
);
|
||||
} else {
|
||||
if (confirm('Czy na pewno chcesz usunac ten szablon e-mail?')) {
|
||||
delForm.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -38,6 +38,8 @@ use App\Modules\Settings\ReceiptConfigController;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use App\Modules\Settings\EmailMailboxController;
|
||||
use App\Modules\Settings\EmailMailboxRepository;
|
||||
use App\Modules\Settings\EmailTemplateController;
|
||||
use App\Modules\Settings\EmailTemplateRepository;
|
||||
use App\Modules\Settings\IntegrationSecretCipher;
|
||||
use App\Modules\Accounting\AccountingController;
|
||||
use App\Modules\Accounting\ReceiptController;
|
||||
@@ -197,6 +199,14 @@ return static function (Application $app): void {
|
||||
$auth,
|
||||
$emailMailboxRepository
|
||||
);
|
||||
$emailTemplateRepository = new EmailTemplateRepository($app->db());
|
||||
$emailTemplateController = new EmailTemplateController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$emailTemplateRepository,
|
||||
$emailMailboxRepository
|
||||
);
|
||||
$receiptController = new ReceiptController(
|
||||
$template,
|
||||
$translator,
|
||||
@@ -325,6 +335,12 @@ return static function (Application $app): void {
|
||||
$router->post('/settings/email-mailboxes/delete', [$emailMailboxController, 'delete'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/toggle', [$emailMailboxController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/test', [$emailMailboxController, 'testConnection'], [$authMiddleware]);
|
||||
$router->get('/settings/email-templates', [$emailTemplateController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/email-templates/save', [$emailTemplateController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/email-templates/delete', [$emailTemplateController, 'delete'], [$authMiddleware]);
|
||||
$router->post('/settings/email-templates/toggle', [$emailTemplateController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/email-templates/preview', [$emailTemplateController, 'preview'], [$authMiddleware]);
|
||||
$router->get('/settings/email-templates/variables', [$emailTemplateController, 'getVariables'], [$authMiddleware]);
|
||||
$router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]);
|
||||
$router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]);
|
||||
$router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]);
|
||||
|
||||
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