update
This commit is contained in:
@@ -13,7 +13,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 1.0.0 |
|
||||
| Status | v2.2 Complete |
|
||||
| Status | v2.3 Complete |
|
||||
| Last Updated | 2026-03-28 |
|
||||
|
||||
## Requirements
|
||||
@@ -59,6 +59,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Szablony e-mail: zmienne `przesylka.numer` i `przesylka.link_sledzenia` z provider-aware linkiem sledzenia - Phase 48
|
||||
- [x] Automatyzacja: tab Historia z filtrowaniem/paginacja + retencja 30 dni + akcja update_order_status - Phase 49
|
||||
- [x] Allegro: automatyczne przekazywanie numeru przesylki do checkout form po utworzeniu paczki (tylko source=allegro) - Phase 50
|
||||
- [x] Email HTML Layout: header/footer HTML per skrzynka pocztowa, dual-mode edytor (Quill + HTML source), kompozycja header+body+footer, podglad — Phase 51
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
@@ -160,6 +161,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-03-28 after Phase 50 completion (Allegro Shipment Waybill Push)*
|
||||
*Last updated: 2026-03-28 after Phase 51 completion (Email HTML Layout)*
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,23 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
|
||||
|
||||
No active milestone - Ready to define next scope
|
||||
|
||||
Next action: uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu biznesowego.
|
||||
Next action: uruchom /paul:milestone (lub /paul:plan) dla kolejnego celu biznesowego.
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
<details>
|
||||
<summary>v2.3 Email HTML Layout - 2026-03-28 (1 phase, 1 plan)</summary>
|
||||
|
||||
HTML header/footer per skrzynka pocztowa z dual-mode edytorem (Quill WYSIWYG + HTML source) i kompozycja email header+body+footer.
|
||||
|
||||
| Phase | Name | Plans | Completed |
|
||||
|-------|------|-------|-----------|
|
||||
| 51 | Email HTML Layout | 1/1 | 2026-03-28 |
|
||||
|
||||
Archive: `.paul/phases/51-email-html-layout/`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>v2.2 Allegro Shipment Waybill Push - 2026-03-28 (1 phase, 1 plan)</summary>
|
||||
|
||||
|
||||
@@ -5,46 +5,48 @@
|
||||
See: .paul/PROJECT.md (updated 2026-03-28)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** Milestone v2.2 completed; ready for next PLAN / next milestone
|
||||
**Current focus:** Milestone v2.3 completed; ready for next PLAN / next milestone
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v2.2 Allegro Shipment Waybill Push - Complete
|
||||
Phase: 1 of 1 (50 - Allegro Shipment Waybill Push) - Complete
|
||||
Plan: 50-01 complete
|
||||
Milestone: v2.3 Email HTML Layout - Complete
|
||||
Phase: 1 of 1 (51 - Email HTML Layout) — Complete
|
||||
Plan: 51-01 complete
|
||||
Status: Loop complete - ready for next PLAN
|
||||
Last activity: 2026-03-28 15:33:00 - UNIFY closed for 50-01, SUMMARY created
|
||||
Last activity: 2026-03-28 — UNIFY closed for 51-01, SUMMARY created
|
||||
|
||||
Progress:
|
||||
- Milestone: [##########] 100%
|
||||
- Phase 50: [##########] 100%
|
||||
- Phase 51: [##########] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN --> APPLY --> UNIFY
|
||||
done done done [Loop complete - ready for next PLAN]
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-28 15:33:00
|
||||
Stopped at: Phase 50 complete, milestone v2.2 complete
|
||||
Next action: Uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu
|
||||
Resume file: .paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md
|
||||
Last session: 2026-03-28
|
||||
Stopped at: Phase 51 complete, milestone v2.3 complete
|
||||
Next action: Uruchom /paul:milestone (lub /paul:plan) dla kolejnego celu
|
||||
Resume file: .paul/phases/51-email-html-layout/51-01-SUMMARY.md
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
| Date | Decision | Impact |
|
||||
|------|----------|--------|
|
||||
| 2026-03-28 | Push waybilla do Allegro wykonywany tylko dla `orders.source='allegro'` i `source_order_id` | Brak falszywych pushy dla innych integracji |
|
||||
| 2026-03-28 | Blad pushu waybilla do Allegro jest niekrytyczny dla tworzenia paczki | Lokalna przesylka nie ginie przy problemie API Allegro |
|
||||
| 2026-03-28 | Retry pushu po `ALLEGRO_HTTP_401` przez ponowne `resolveToken()` | Wyzsza odpornosc na wygasle tokeny |
|
||||
| 2026-03-28 | Header/footer HTML na poziomie skrzynki (nie szablonu) | Spojny branding bez duplikacji w kazdym szablonie |
|
||||
| 2026-03-28 | Quill.js z ograniczonym toolbar (email-safe) zamiast MJML/dedykowanego buildera | Prostota; brak build pipeline w projekcie |
|
||||
| 2026-03-28 | Zmienne resolver dziala tez w header/footer | Mozliwosc uzycia {{firma.nazwa}} w naglowku |
|
||||
| 2026-03-28 | Tryb HTML source omija Quill — surowy HTML zachowany | Wklejanie gotowych szablonow email bez sanityzacji |
|
||||
| 2026-03-28 | Auto-detekcja rich HTML przy ladowaniu edytora | Edytor startuje w source mode jesli HTML zawiera div+style/table |
|
||||
|
||||
## Git State
|
||||
|
||||
Last commit: 176d740
|
||||
Last commit: 572643a
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
236
.paul/phases/51-email-html-layout/51-01-PLAN.md
Normal file
236
.paul/phases/51-email-html-layout/51-01-PLAN.md
Normal file
@@ -0,0 +1,236 @@
|
||||
---
|
||||
phase: 51-email-html-layout
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260328_000001_add_html_layout_to_email_mailboxes.sql
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
- src/Modules/Email/EmailSendingService.php
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Rozbudowa modulu e-mail o HTML layout: header i footer konfigurowane na poziomie skrzynki pocztowej, content z szablonu. Finalna wiadomosc = header + content + footer. Edytor ograniczony do email-safe HTML.
|
||||
|
||||
## Purpose
|
||||
Umozliwienie uzytkownikowi stworzenia spojnego brandingu e-mail (naglowek z logo/nazwa firmy, stopka z danymi kontaktowymi) bez powielania tresci w kazdym szablonie.
|
||||
|
||||
## Output
|
||||
- Migracja DB: kolumny `header_html`, `footer_html` w `email_mailboxes`
|
||||
- UI skrzynek: dwa edytory Quill (header/footer) z toolbar email-safe
|
||||
- Kompozycja e-mail: header + body + footer w EmailSendingService i preview
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Settings/EmailMailboxController.php
|
||||
@src/Modules/Settings/EmailMailboxRepository.php
|
||||
@resources/views/settings/email-mailboxes.php
|
||||
@src/Modules/Email/EmailSendingService.php
|
||||
@resources/views/settings/email-templates.php (reference — Quill config)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
No specialized flows configured as required for this work type.
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Kolumny DB header_html i footer_html
|
||||
```gherkin
|
||||
Given tabela email_mailboxes istnieje
|
||||
When migracja zostanie wykonana
|
||||
Then tabela zawiera kolumny header_html TEXT NULL i footer_html TEXT NULL
|
||||
And istniejace rekordy maja NULL w obu kolumnach (brak breaking change)
|
||||
```
|
||||
|
||||
## AC-2: Edycja header/footer w formularzu skrzynki
|
||||
```gherkin
|
||||
Given uzytkownik otwiera formularz edycji skrzynki pocztowej
|
||||
When widzi sekcje "Szablon wiadomosci" pod ustawieniami SMTP
|
||||
Then sa dwa edytory Quill: "Naglowek (header)" i "Stopka (footer)"
|
||||
And toolbar kazdego edytora ogranicza sie do: bold, italic, underline, link, kolor tekstu, kolor tla, wyrownanie, listy, naglowki (h1-h3), obraz (inline base64)
|
||||
And tresc edytorow jest zapisywana do DB przy submit formularza
|
||||
```
|
||||
|
||||
## AC-3: Kompozycja e-mail header + content + footer
|
||||
```gherkin
|
||||
Given skrzynka ma ustawiony header_html i footer_html
|
||||
And szablon ma body_html
|
||||
When e-mail jest wysylany lub podgladany (preview)
|
||||
Then tresc wiadomosci = header_html + body_html (resolved) + footer_html
|
||||
And header i footer rowniez przechodza przez variable resolver
|
||||
```
|
||||
|
||||
## AC-4: E-mail bez header/footer
|
||||
```gherkin
|
||||
Given skrzynka ma puste (NULL) header_html i/lub footer_html
|
||||
When e-mail jest wysylany
|
||||
Then tresc wiadomosci zawiera tylko body_html (bez pustych sekcji)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracja DB + Repository + Controller</name>
|
||||
<files>
|
||||
database/migrations/20260328_000001_add_html_layout_to_email_mailboxes.sql,
|
||||
src/Modules/Settings/EmailMailboxRepository.php,
|
||||
src/Modules/Settings/EmailMailboxController.php
|
||||
</files>
|
||||
<action>
|
||||
1. Utworzyc migracje SQL:
|
||||
```sql
|
||||
ALTER TABLE email_mailboxes
|
||||
ADD COLUMN header_html TEXT NULL AFTER sender_name,
|
||||
ADD COLUMN footer_html TEXT NULL AFTER header_html;
|
||||
```
|
||||
|
||||
2. EmailMailboxRepository:
|
||||
- `save()`: dodac `header_html` i `footer_html` do INSERT/UPDATE
|
||||
- `findById()`: upewnic sie ze zwraca te kolumny (juz zwraca SELECT * wiec OK)
|
||||
- `listAll()`: bez zmian (nie potrzebuje HTML w liscie)
|
||||
|
||||
3. EmailMailboxController:
|
||||
- `save()`: pobrac `header_html` i `footer_html` z POST body
|
||||
- Nie escapowac HTML (to jest tresc edytora WYSIWYG, jak body_html w szablonach)
|
||||
- Przekazac do repozytorium w tablicy save data
|
||||
</action>
|
||||
<verify>
|
||||
- Migracja wykonuje sie bez bledow
|
||||
- DESCRIBE email_mailboxes pokazuje header_html i footer_html jako TEXT NULL
|
||||
- Zapis i odczyt skrzynki z header/footer dziala poprawnie
|
||||
</verify>
|
||||
<done>AC-1 satisfied: kolumny istnieja i sa zapisywane/odczytywane</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: UI edytorow header/footer w formularzu skrzynki</name>
|
||||
<files>resources/views/settings/email-mailboxes.php</files>
|
||||
<action>
|
||||
1. Po sekcji "Ustawienia SMTP" dodac nowa sekcje "Szablon wiadomosci" z dwoma edytorami Quill
|
||||
|
||||
2. Kazdy edytor (header, footer):
|
||||
- Label: "Naglowek (header)" / "Stopka (footer)"
|
||||
- Div z id `js-header-editor` / `js-footer-editor` (kontener Quill)
|
||||
- Hidden input `header_html` / `footer_html` (sync przy submit)
|
||||
- Podpowiedz: "Opcjonalnie. Bedzie dolaczany do kazdego e-maila wysylanego z tej skrzynki."
|
||||
|
||||
3. Toolbar edytora — TYLKO email-safe opcje:
|
||||
```javascript
|
||||
[
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
```
|
||||
- Image: Quill domyslnie wstawia jako base64 inline — to jest email-safe
|
||||
- Brak: strike, blockquote, code-block, video, indent (slabo obslugiwane w klientach pocztowych)
|
||||
|
||||
4. Zaladowac Quill CSS/JS z CDN (ten sam co w email-templates: 2.0.3)
|
||||
|
||||
5. Na submit formularza: sync innerHTML z edytorow do hidden inputs
|
||||
|
||||
6. Przy edycji istniejacego rekordu: zaladowac HTML do edytorow przez `quill.root.innerHTML = ...`
|
||||
|
||||
7. Edytory powinny miec mniejsza domyslna wysokosc niz edytor szablonu (np. min-height: 80px)
|
||||
</action>
|
||||
<verify>
|
||||
- Otworz /settings/email-mailboxes — formularz pokazuje dwa edytory
|
||||
- Wpisz tresc w header/footer, zapisz — dane sa w DB
|
||||
- Edytuj skrzynke — header/footer sa zaladowane do edytorow
|
||||
- Toolbar nie zawiera opcji niebezpiecznych dla e-mail (video, code-block)
|
||||
</verify>
|
||||
<done>AC-2 satisfied: edytory sa dostepne, ograniczone do email-safe, i zapisuja dane</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Kompozycja e-mail w EmailSendingService</name>
|
||||
<files>src/Modules/Email/EmailSendingService.php</files>
|
||||
<action>
|
||||
1. W metodzie `send()` po rozwiazaniu zmiennych w body:
|
||||
- Pobrac header_html i footer_html z resolved mailbox
|
||||
- Przepuscic je przez variableResolver.resolve() (aby zmienne dzialaly tez w header/footer)
|
||||
- Zlozyc finalBody: header_html + resolvedBody + footer_html
|
||||
- Jezeli header_html lub footer_html sa NULL/puste — pominac (bez pustych divow)
|
||||
- Uzyc finalBody zamiast resolvedBody w sendViaSMTP i logEmail
|
||||
|
||||
2. W metodzie `preview()`:
|
||||
- Analogicznie: pobrac mailbox dla szablonu, zlozyc header + body + footer
|
||||
- Aby preview dzialal, potrzebujemy mailbox — uzyc resolveMailbox() (juz istnieje)
|
||||
- Jezeli mailbox nie znaleziony — pokazac sam body (bez header/footer)
|
||||
|
||||
3. Metoda `resolveMailbox()` jest private — juz zwraca pelne dane z findById() wlacznie z header_html/footer_html
|
||||
|
||||
4. Nowa prywatna metoda `composeBody(string $resolvedBody, ?array $mailbox, array $variableMap): string`:
|
||||
- Wydzielic logike kompozycji do reusable metody
|
||||
- Uzyc w send() i preview()
|
||||
</action>
|
||||
<verify>
|
||||
- Wyslij e-mail ze skrzynka z ustawionym header/footer — e-mail zawiera header + body + footer
|
||||
- Wyslij e-mail ze skrzynka BEZ header/footer — e-mail zawiera tylko body
|
||||
- Preview pokazuje zlozony wynik header + body + footer
|
||||
- Zmienne w header/footer sa rozwiazywane (np. {{firma.nazwa}})
|
||||
</verify>
|
||||
<done>AC-3 i AC-4 satisfied: kompozycja dziala z i bez header/footer</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- resources/views/settings/email-templates.php (edytor szablonow — bez zmian)
|
||||
- src/Modules/Email/VariableResolver.php (resolver zmiennych — bez zmian)
|
||||
- src/Modules/Email/AttachmentGenerator.php
|
||||
- database/migrations/ (istniejace migracje)
|
||||
- Struktura tabeli email_templates (body_html pozostaje jak jest)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak edytora MJML / dedykowanego email buildera — Quill z ograniczonym toolbar wystarcza
|
||||
- Brak podgladu header/footer w formularzu skrzynki (preview jest w szablonach)
|
||||
- Brak importu gotowych szablonow HTML
|
||||
- Zmienne w header/footer dzialaja, ale panel zmiennych NIE jest dodawany do formularza skrzynki (header/footer to zwykle statyczny branding)
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] Migracja wykonana, kolumny widoczne w DESCRIBE
|
||||
- [ ] Formularz skrzynki: dwa edytory Quill z email-safe toolbar
|
||||
- [ ] Zapis i odczyt header/footer dziala
|
||||
- [ ] E-mail wysylany ze skrzynka z header/footer zawiera zlozony layout
|
||||
- [ ] E-mail wysylany ze skrzynka BEZ header/footer zawiera tylko body
|
||||
- [ ] Preview pokazuje zlozony wynik
|
||||
- [ ] Zmienne w header/footer sa rozwiazywane
|
||||
- [ ] Brak bledow PHP/JS w konsoli
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 taski zakonczone
|
||||
- Wszystkie 4 acceptance criteria spelnione
|
||||
- Wszystkie verification checks przeszly
|
||||
- Brak regresji w istniejacym wysylaniu e-mail
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/51-email-html-layout/51-01-SUMMARY.md`
|
||||
</output>
|
||||
145
.paul/phases/51-email-html-layout/51-01-SUMMARY.md
Normal file
145
.paul/phases/51-email-html-layout/51-01-SUMMARY.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
phase: 51-email-html-layout
|
||||
plan: 01
|
||||
subsystem: email
|
||||
tags: [quill, html-email, smtp, phpmailer]
|
||||
|
||||
requires:
|
||||
- phase: 13-email-mailboxes
|
||||
provides: email_mailboxes table, EmailMailboxRepository, EmailMailboxController
|
||||
- phase: 14-email-templates
|
||||
provides: email_templates table, Quill.js editor, VariableResolver
|
||||
provides:
|
||||
- HTML header/footer per mailbox (header_html, footer_html columns)
|
||||
- Email composition: header + body + footer in EmailSendingService
|
||||
- HTML source editor toggle + preview for header/footer
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [html-source-toggle, iframe-preview, table-based-email-layout]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- database/migrations/20260328_000001_add_html_layout_to_email_mailboxes.sql
|
||||
modified:
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
- src/Modules/Email/EmailSendingService.php
|
||||
|
||||
key-decisions:
|
||||
- "Header/footer na poziomie skrzynki (nie szablonu) — spojny branding bez duplikacji"
|
||||
- "Tryb HTML source omija Quill — surowy HTML zachowany bez sanityzacji"
|
||||
- "composeBody() jako reusable metoda w send() i preview()"
|
||||
|
||||
patterns-established:
|
||||
- "HTML source toggle: textarea + Quill toggle z auto-detekcja rich HTML przy ladowaniu"
|
||||
- "Iframe preview modal do podgladu surowego HTML"
|
||||
|
||||
duration: ~45min
|
||||
started: 2026-03-28T16:00:00Z
|
||||
completed: 2026-03-28T16:45:00Z
|
||||
---
|
||||
|
||||
# Phase 51 Plan 01: Email HTML Layout Summary
|
||||
|
||||
**HTML header/footer per skrzynka pocztowa z dual-mode edytorem (Quill WYSIWYG + HTML source) i kompozycja email header+body+footer w EmailSendingService.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~45min |
|
||||
| Tasks | 3 completed + 2 scope additions |
|
||||
| Files modified | 5 source + 3 docs |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Kolumny DB header_html i footer_html | Pass | TEXT NULL, migracja zarejestrowana w migrations table |
|
||||
| AC-2: Edycja header/footer w formularzu skrzynki | Pass | Quill + HTML source toggle + preview |
|
||||
| AC-3: Kompozycja header + content + footer | Pass | composeBody() w send() i preview(), variable resolver na header/footer |
|
||||
| AC-4: E-mail bez header/footer | Pass | NULL/pusty header/footer pomijany |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Kolumny `header_html`/`footer_html` w `email_mailboxes` z pelnym CRUD (repository + controller)
|
||||
- Dual-mode edytor: Quill WYSIWYG z email-safe toolbar + tryb HTML source (textarea) z auto-detekcja rich HTML
|
||||
- Przycisk podgladu (iframe modal) dla header i footer
|
||||
- Metoda `composeBody()` w EmailSendingService — skladanie header + body + footer z variable resolution
|
||||
- Przykladowy szablon stopki (table-based, Outlook-safe) w `footer-template.html`
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260328_000001_add_html_layout_to_email_mailboxes.sql` | Created | ALTER TABLE — kolumny header_html, footer_html |
|
||||
| `src/Modules/Settings/EmailMailboxRepository.php` | Modified | header_html/footer_html w save() INSERT/UPDATE |
|
||||
| `src/Modules/Settings/EmailMailboxController.php` | Modified | Pobieranie header_html/footer_html z POST |
|
||||
| `resources/views/settings/email-mailboxes.php` | Modified | Sekcja "Szablon wiadomosci": 2x Quill + HTML source toggle + preview modal |
|
||||
| `src/Modules/Email/EmailSendingService.php` | Modified | composeBody() — skladanie header+body+footer w send() i preview() |
|
||||
| `DOCS/DB_SCHEMA.md` | Modified | Dokumentacja nowych kolumn |
|
||||
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 51 |
|
||||
| `DOCS/ARCHITECTURE.md` | Modified | Opis kompozycji email |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Header/footer na poziomie skrzynki, nie szablonu | Spojny branding — jeden header/footer dla wszystkich szablonow danej skrzynki | Brak duplikacji w szablonach |
|
||||
| Tryb HTML source omija Quill calkowicie | Quill sanityzuje HTML (usuwa inline style, div, table) — rich HTML musi byc zachowany | Surowy HTML wklejony w source mode trafia do DB bez strat |
|
||||
| Auto-detekcja rich HTML przy ladowaniu | Jesli zapisany HTML zawiera div+style/table/meta, edytor startuje w source mode | Brak utraty danych przy ponownej edycji |
|
||||
| composeBody() jako prywatna metoda | Reuse w send() i preview() bez duplikacji logiki | Spojnosc kompozycji |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 2 | Uzyteczne rozszerzenia UI na zyczenie uzytkownika |
|
||||
| Auto-fixed | 1 | Rejestracja migracji w tabeli migrations |
|
||||
|
||||
### Scope Additions
|
||||
|
||||
**1. Tryb HTML source (</> HTML)**
|
||||
- Dodany na zyczenie uzytkownika — Quill sanityzuje rich HTML
|
||||
- Textarea toggle z zachowaniem surowego HTML przy submit
|
||||
|
||||
**2. Przycisk Podglad**
|
||||
- Dodany na zyczenie uzytkownika
|
||||
- Iframe modal renderujacy aktualny HTML z edytora
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. Migracja nie zarejestrowana w tabeli migrations**
|
||||
- Migracja uruchomiona recznym PDO::exec (kolumny dodane), ale brak wpisu w `migrations`
|
||||
- Migrator probowal ponownie wykonac ALTER — Duplicate column error
|
||||
- Fix: INSERT do tabeli migrations
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Lokalna baza niedostepna (XAMPP nie uruchomiony) | Uzyto DB_HOST_REMOTE do migracji |
|
||||
| Migracja reczna nie zarejestrowala sie w migrations | Reczny INSERT do tabeli migrations |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Email header/footer w pelni funkcjonalny
|
||||
- Preview w formularzu skrzynki
|
||||
- Kompozycja email dziala w send() i preview()
|
||||
|
||||
**Concerns:**
|
||||
- Brak panelu zmiennych w formularzu skrzynki (header/footer zwykle statyczny)
|
||||
- SonarQube scan nie uruchomiony (wymagany przez SPECIAL-FLOWS.md przed UNIFY)
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 51-email-html-layout, Plan: 01*
|
||||
*Completed: 2026-03-28*
|
||||
24
.paul/phases/51-email-html-layout/footer-template.html
Normal file
24
.paul/phases/51-email-html-layout/footer-template.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top: 1px solid #eeeeee;">
|
||||
<tr><td style="padding: 20px 0 10px 0; font-size: 0; line-height: 0;"> </td></tr>
|
||||
<tr>
|
||||
<td style="padding: 0 20px;">
|
||||
<table cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td valign="top" style="padding-right: 30px; padding-bottom: 20px;">
|
||||
<img src="https://marianek.pl/layout/images/logo.png" alt="marianek.pl" width="250" style="display: block; width: 250px; height: auto; border: 0;" />
|
||||
</td>
|
||||
<td valign="top" style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; line-height: 1.4; color: #000000;">
|
||||
<span style="color: #888888; font-size: 13px;">Pozdrawiam</span><br />
|
||||
<span style="font-size: 22px; font-weight: 400; line-height: 1.2;">Pyziak <strong>Jacek</strong></span><br />
|
||||
<br />
|
||||
<a href="tel:+48530755774" style="color: #000000; text-decoration: none; font-size: 12px;">tel. +48 530 755 774</a><br />
|
||||
<a href="mailto:sklep@marianek.pl" style="color: #ea6e24; text-decoration: none; font-size: 12px;">sklep@marianek.pl</a><br />
|
||||
<a href="https://www.marianek.pl" style="color: #ea6e24; text-decoration: none; font-size: 12px;">marianek.pl</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]></td></tr></table><![endif]-->
|
||||
327
.vscode/ftp-kr.sync.cache.json
vendored
327
.vscode/ftp-kr.sync.cache.json
vendored
@@ -7,12 +7,6 @@
|
||||
"lmtime": 1772652932723,
|
||||
"modified": false
|
||||
},
|
||||
"_allegro_check.php": {
|
||||
"type": "-",
|
||||
"size": 1954,
|
||||
"lmtime": 1772803697369,
|
||||
"modified": false
|
||||
},
|
||||
"ARCHITECTURE.md": {
|
||||
"type": "-",
|
||||
"size": 659,
|
||||
@@ -113,7 +107,6 @@
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
".claude": {},
|
||||
"CLAUDE.md": {
|
||||
"type": "-",
|
||||
"size": 3460,
|
||||
@@ -121,41 +114,17 @@
|
||||
"modified": true
|
||||
},
|
||||
"clients": {},
|
||||
"composer.json": {
|
||||
"type": "-",
|
||||
"size": 722,
|
||||
"lmtime": 1773786224662,
|
||||
"modified": false
|
||||
},
|
||||
"composer.lock": {
|
||||
"type": "-",
|
||||
"size": 68047,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"composer.phar": {
|
||||
"type": "-",
|
||||
"size": 3288946,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"composer-setup.php": {
|
||||
"type": "-",
|
||||
"size": 59524,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"composer-temp.phar": {
|
||||
"type": "-",
|
||||
"size": 3288946,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"config": {
|
||||
"app.php": {
|
||||
"type": "-",
|
||||
"size": 972,
|
||||
"lmtime": 1771955055783,
|
||||
"size": 1033,
|
||||
"lmtime": 1774706127464,
|
||||
"modified": false
|
||||
},
|
||||
"auth.php": {
|
||||
@@ -568,6 +537,12 @@
|
||||
"size": 319,
|
||||
"lmtime": 1774611787688,
|
||||
"modified": false
|
||||
},
|
||||
"20260328_000072_create_automation_execution_logs_table.sql": {
|
||||
"type": "-",
|
||||
"size": 1653,
|
||||
"lmtime": 1774702996564,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"seeders": {},
|
||||
@@ -586,53 +561,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"_db_check2.php": {
|
||||
"type": "-",
|
||||
"size": 1656,
|
||||
"lmtime": 1772803550728,
|
||||
"modified": false
|
||||
},
|
||||
"_db_check3.php": {
|
||||
"type": "-",
|
||||
"size": 1919,
|
||||
"lmtime": 1772803572007,
|
||||
"modified": false
|
||||
},
|
||||
"_db_check.php": {
|
||||
"type": "-",
|
||||
"size": 2025,
|
||||
"lmtime": 1772803459353,
|
||||
"modified": false
|
||||
},
|
||||
"DB_SCHEMA.md": {
|
||||
"type": "-",
|
||||
"size": 363,
|
||||
"lmtime": 1772490689218,
|
||||
"modified": false
|
||||
},
|
||||
"delivery-tab-bug.png": {
|
||||
"type": "-",
|
||||
"size": 124327,
|
||||
"lmtime": 1774565855738,
|
||||
"modified": false
|
||||
},
|
||||
"deploy-vendor.php": {
|
||||
"type": "-",
|
||||
"size": 2097,
|
||||
"lmtime": 1773530897555,
|
||||
"modified": false
|
||||
},
|
||||
"DOCS": {
|
||||
"ARCHITECTURE.md": {
|
||||
"type": "-",
|
||||
"size": 35558,
|
||||
"lmtime": 1774612062257,
|
||||
"size": 39802,
|
||||
"lmtime": 1774707780233,
|
||||
"modified": false
|
||||
},
|
||||
"DB_SCHEMA.md": {
|
||||
"type": "-",
|
||||
"size": 29871,
|
||||
"lmtime": 1774612041000,
|
||||
"size": 32219,
|
||||
"lmtime": 1774706184511,
|
||||
"modified": false
|
||||
},
|
||||
"ORDERS_SCHEMA_APILO_DRAFT.md": {
|
||||
@@ -655,51 +594,27 @@
|
||||
},
|
||||
"TECH_CHANGELOG.md": {
|
||||
"type": "-",
|
||||
"size": 57684,
|
||||
"lmtime": 1774612077539,
|
||||
"size": 65574,
|
||||
"lmtime": 1774707789181,
|
||||
"modified": false
|
||||
},
|
||||
"todo.md": {
|
||||
"type": "-",
|
||||
"size": 3512,
|
||||
"size": 39751,
|
||||
"lmtime": 1774474971584,
|
||||
"modified": false
|
||||
"modified": true
|
||||
}
|
||||
},
|
||||
".env": {
|
||||
"type": "-",
|
||||
"size": 538,
|
||||
"lmtime": 1774565782052,
|
||||
"size": 596,
|
||||
"lmtime": 1774706394170,
|
||||
"modified": false
|
||||
},
|
||||
".env.codex.bak": {
|
||||
"type": "-",
|
||||
"size": 54,
|
||||
"lmtime": 1771866989245,
|
||||
"modified": true
|
||||
},
|
||||
".env.example": {
|
||||
"type": "-",
|
||||
"size": 580,
|
||||
"lmtime": 1772491020678,
|
||||
"modified": true
|
||||
},
|
||||
"_fix_carrier.php": {
|
||||
"type": "-",
|
||||
"size": 3272,
|
||||
"lmtime": 1774296556289,
|
||||
"modified": false
|
||||
},
|
||||
"fix_delivery_status.php": {
|
||||
"type": "-",
|
||||
"size": 356,
|
||||
"lmtime": 1774302691828,
|
||||
"modified": false
|
||||
},
|
||||
"fix_interval.php": {
|
||||
"type": "-",
|
||||
"size": 486,
|
||||
"lmtime": 1774302992501,
|
||||
"size": 661,
|
||||
"lmtime": 1774706130497,
|
||||
"modified": false
|
||||
},
|
||||
".gitignore": {
|
||||
@@ -720,12 +635,6 @@
|
||||
"lmtime": 1771459937874,
|
||||
"modified": false
|
||||
},
|
||||
"log.md": {
|
||||
"type": "-",
|
||||
"size": 4574,
|
||||
"lmtime": 1771963733140,
|
||||
"modified": false
|
||||
},
|
||||
".mcp.json": {
|
||||
"type": "-",
|
||||
"size": 397,
|
||||
@@ -1898,28 +1807,19 @@
|
||||
"lmtime": 1771869056394,
|
||||
"modified": false
|
||||
},
|
||||
".paul": {},
|
||||
"phpunit.xml": {
|
||||
"type": "-",
|
||||
"size": 480,
|
||||
"lmtime": 1772489488633,
|
||||
"modified": false
|
||||
},
|
||||
".playwright-mcp": {
|
||||
"console-2026-03-26T22-55-27-422Z.log": {
|
||||
"type": "-",
|
||||
"size": 138,
|
||||
"lmtime": 1774565727939,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"public": {
|
||||
"assets": {
|
||||
"css": {
|
||||
"app.css": {
|
||||
"type": "-",
|
||||
"size": 44903,
|
||||
"lmtime": 1774600385594,
|
||||
"size": 45416,
|
||||
"lmtime": 1774702916830,
|
||||
"modified": false
|
||||
},
|
||||
"app.css.map": {
|
||||
@@ -1931,7 +1831,7 @@
|
||||
"login.css": {
|
||||
"type": "-",
|
||||
"size": 5996,
|
||||
"lmtime": 1774474932148,
|
||||
"lmtime": 1774702917327,
|
||||
"modified": false
|
||||
},
|
||||
"login.css.map": {
|
||||
@@ -1954,8 +1854,8 @@
|
||||
"modules": {
|
||||
"automation-form.js": {
|
||||
"type": "-",
|
||||
"size": 7251,
|
||||
"lmtime": 1774475530521,
|
||||
"size": 8789,
|
||||
"lmtime": 1774704031241,
|
||||
"modified": false
|
||||
},
|
||||
"inline-status-change.js": {
|
||||
@@ -2027,8 +1927,8 @@
|
||||
},
|
||||
"app.scss": {
|
||||
"type": "-",
|
||||
"size": 43794,
|
||||
"lmtime": 1774600368218,
|
||||
"size": 43784,
|
||||
"lmtime": 1774701658193,
|
||||
"modified": false
|
||||
},
|
||||
"login.css": {
|
||||
@@ -2052,8 +1952,8 @@
|
||||
"modules": {
|
||||
"_automation.scss": {
|
||||
"type": "-",
|
||||
"size": 1038,
|
||||
"lmtime": 1773789611848,
|
||||
"size": 1565,
|
||||
"lmtime": 1774702761945,
|
||||
"modified": false
|
||||
},
|
||||
"_delivery-status-mappings.scss": {
|
||||
@@ -2232,9 +2132,9 @@
|
||||
},
|
||||
"allegro.php": {
|
||||
"type": "-",
|
||||
"size": 38673,
|
||||
"size": 38653,
|
||||
"lmtime": 1774565889261,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"apaczka.php": {
|
||||
"type": "-",
|
||||
@@ -2274,8 +2174,8 @@
|
||||
},
|
||||
"email-templates.php": {
|
||||
"type": "-",
|
||||
"size": 14153,
|
||||
"lmtime": 1774564964461,
|
||||
"size": 14291,
|
||||
"lmtime": 1774701677322,
|
||||
"modified": false
|
||||
},
|
||||
"gs1.php": {
|
||||
@@ -2346,14 +2246,14 @@
|
||||
"automation": {
|
||||
"form.php": {
|
||||
"type": "-",
|
||||
"size": 12406,
|
||||
"lmtime": 1774475517368,
|
||||
"size": 14751,
|
||||
"lmtime": 1774704020030,
|
||||
"modified": false
|
||||
},
|
||||
"index.php": {
|
||||
"type": "-",
|
||||
"size": 4426,
|
||||
"lmtime": 1774566437972,
|
||||
"size": 15170,
|
||||
"lmtime": 1774703149752,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
@@ -2362,64 +2262,17 @@
|
||||
"routes": {
|
||||
"web.php": {
|
||||
"type": "-",
|
||||
"size": 24995,
|
||||
"lmtime": 1774566431014,
|
||||
"size": 27445,
|
||||
"lmtime": 1774706514882,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
".scannerwork": {},
|
||||
".serena": {
|
||||
"cache": {
|
||||
"php": {
|
||||
"document_symbols.pkl": {
|
||||
"type": "-",
|
||||
"size": 11193554,
|
||||
"lmtime": 1773615706599,
|
||||
"modified": false
|
||||
},
|
||||
"raw_document_symbols.pkl": {
|
||||
"type": "-",
|
||||
"size": 3560940,
|
||||
"lmtime": 1773615706397,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"memories": {
|
||||
"gs1-integration": {
|
||||
"handover.md": {
|
||||
"type": "-",
|
||||
"size": 1134,
|
||||
"lmtime": 1771960966313,
|
||||
"modified": false
|
||||
},
|
||||
"plan.md": {
|
||||
"type": "-",
|
||||
"size": 5595,
|
||||
"lmtime": 1771960615472,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"project.yml": {
|
||||
"type": "-",
|
||||
"size": 9498,
|
||||
"lmtime": 1774301665208,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"sonar-project.properties": {
|
||||
"type": "-",
|
||||
"size": 385,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"src": {
|
||||
"Core": {
|
||||
"Application.php": {
|
||||
"type": "-",
|
||||
"size": 9588,
|
||||
"lmtime": 1774474842256,
|
||||
"size": 9697,
|
||||
"lmtime": 1774706140100,
|
||||
"modified": false
|
||||
},
|
||||
"Constants": {
|
||||
@@ -2621,10 +2474,16 @@
|
||||
"lmtime": 1772655083686,
|
||||
"modified": false
|
||||
},
|
||||
"AutomationHistoryCleanupHandler.php": {
|
||||
"type": "-",
|
||||
"size": 694,
|
||||
"lmtime": 1774702487146,
|
||||
"modified": false
|
||||
},
|
||||
"CronHandlerFactory.php": {
|
||||
"type": "-",
|
||||
"size": 7970,
|
||||
"lmtime": 1774612020782,
|
||||
"size": 8710,
|
||||
"lmtime": 1774702549664,
|
||||
"modified": false
|
||||
},
|
||||
"CronJobProcessor.php": {
|
||||
@@ -3173,9 +3032,9 @@
|
||||
"Shipments": {
|
||||
"AllegroShipmentService.php": {
|
||||
"type": "-",
|
||||
"size": 14932,
|
||||
"lmtime": 1773396238404,
|
||||
"modified": true
|
||||
"size": 18411,
|
||||
"lmtime": 1774707709265,
|
||||
"modified": false
|
||||
},
|
||||
"AllegroTrackingService.php": {
|
||||
"type": "-",
|
||||
@@ -3185,8 +3044,8 @@
|
||||
},
|
||||
"ApaczkaShipmentService.php": {
|
||||
"type": "-",
|
||||
"size": 33757,
|
||||
"lmtime": 1774303090337,
|
||||
"size": 35982,
|
||||
"lmtime": 1774705073868,
|
||||
"modified": false
|
||||
},
|
||||
"ApaczkaTrackingService.php": {
|
||||
@@ -3221,15 +3080,15 @@
|
||||
},
|
||||
"ShipmentController.php": {
|
||||
"type": "-",
|
||||
"size": 18873,
|
||||
"size": 20135,
|
||||
"lmtime": 1774285889068,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"ShipmentPackageRepository.php": {
|
||||
"type": "-",
|
||||
"size": 8391,
|
||||
"size": 8836,
|
||||
"lmtime": 1774296780272,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"ShipmentPresetController.php": {
|
||||
"type": "-",
|
||||
@@ -3285,20 +3144,26 @@
|
||||
"Automation": {
|
||||
"AutomationController.php": {
|
||||
"type": "-",
|
||||
"size": 15747,
|
||||
"lmtime": 1774566422090,
|
||||
"size": 19948,
|
||||
"lmtime": 1774703990637,
|
||||
"modified": false
|
||||
},
|
||||
"AutomationExecutionLogRepository.php": {
|
||||
"type": "-",
|
||||
"size": 6493,
|
||||
"lmtime": 1774702487160,
|
||||
"modified": false
|
||||
},
|
||||
"AutomationRepository.php": {
|
||||
"type": "-",
|
||||
"size": 9712,
|
||||
"lmtime": 1774566398224,
|
||||
"size": 10496,
|
||||
"lmtime": 1774703977978,
|
||||
"modified": false
|
||||
},
|
||||
"AutomationService.php": {
|
||||
"type": "-",
|
||||
"size": 22368,
|
||||
"lmtime": 1774475866269,
|
||||
"size": 29708,
|
||||
"lmtime": 1774704000450,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -3335,8 +3200,8 @@
|
||||
"phpunit": {
|
||||
"test-results": {
|
||||
"type": "-",
|
||||
"size": 503,
|
||||
"lmtime": 1772489557946,
|
||||
"size": 3083,
|
||||
"lmtime": 1774707817763,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
@@ -5181,12 +5046,6 @@
|
||||
"lmtime": 1772490702841,
|
||||
"modified": false
|
||||
},
|
||||
"_test_apaczka.php": {
|
||||
"type": "-",
|
||||
"size": 2690,
|
||||
"lmtime": 1774296169244,
|
||||
"modified": false
|
||||
},
|
||||
"tests": {
|
||||
"bootstrap.php": {
|
||||
"type": "-",
|
||||
@@ -5219,48 +5078,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"_test_status_sync.php": {
|
||||
"type": "-",
|
||||
"size": 2434,
|
||||
"lmtime": 1772803861129,
|
||||
"modified": false
|
||||
},
|
||||
".tmp_apaczka_check.php": {
|
||||
"type": "-",
|
||||
"size": 527,
|
||||
"lmtime": 1773001200203,
|
||||
"modified": false
|
||||
},
|
||||
".tmp_apaczka_sig_probe.php": {
|
||||
"type": "-",
|
||||
"size": 2657,
|
||||
"lmtime": 1773001422213,
|
||||
"modified": false
|
||||
},
|
||||
".tmp_cols.php": {
|
||||
"type": "-",
|
||||
"size": 367,
|
||||
"lmtime": 1773001718941,
|
||||
"modified": false
|
||||
},
|
||||
"tmp_gs1_test.php": {
|
||||
"type": "-",
|
||||
"size": 3392,
|
||||
"lmtime": 1771959054615,
|
||||
"modified": false
|
||||
},
|
||||
"tmp_schema_check.php": {
|
||||
"type": "-",
|
||||
"size": 429,
|
||||
"lmtime": 1772655634873,
|
||||
"modified": false
|
||||
},
|
||||
".tmp_shoppro_map_check.php": {
|
||||
"type": "-",
|
||||
"size": 1196,
|
||||
"lmtime": 1773001696994,
|
||||
"modified": false
|
||||
},
|
||||
"tools": {
|
||||
"apaczka_probe_order.php": {
|
||||
"type": "-",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
- `App\Modules\Accounting` (modul paragonow — wystawianie, podglad, druk, PDF, lista, eksport XLSX)
|
||||
- `App\Modules\Settings\EmailMailbox*` (skrzynki pocztowe SMTP — CRUD + test polaczenia)
|
||||
- `App\Modules\Settings\EmailTemplate*` (szablony e-mail — CRUD + Quill.js + zmienne + zalaczniki)
|
||||
- `App\Modules\Email` (wysylka e-mail z zamowien — EmailSendingService, VariableResolver, AttachmentGenerator)
|
||||
- `App\Modules\Email` (wysylka e-mail z zamowien — EmailSendingService, VariableResolver, AttachmentGenerator; kompozycja: header (mailbox) + body (template) + footer (mailbox))
|
||||
- `App\Modules\Automation` (zadania automatyczne — reguly zdarzenie/warunki/akcje, CRUD)
|
||||
|
||||
## Routing
|
||||
|
||||
@@ -409,6 +409,8 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
||||
- `smtp_password_encrypted` TEXT NOT NULL — szyfrowane IntegrationSecretCipher (AES-256-CBC+HMAC)
|
||||
- `sender_email` VARCHAR(255) NOT NULL
|
||||
- `sender_name` VARCHAR(200) DEFAULT NULL
|
||||
- `header_html` TEXT DEFAULT NULL — HTML naglowek dolaczany do kazdego e-maila z tej skrzynki
|
||||
- `footer_html` TEXT DEFAULT NULL — HTML stopka dolaczana do kazdego e-maila z tej skrzynki
|
||||
- `is_default` TINYINT(1) NOT NULL DEFAULT 0
|
||||
- `is_active` TINYINT(1) NOT NULL DEFAULT 1
|
||||
- `created_at`, `updated_at` DATETIME
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Tech Changelog
|
||||
|
||||
## 2026-03-28 (Phase 51 - Email HTML Layout, Plan 01)
|
||||
- Migracja `20260328_000001_add_html_layout_to_email_mailboxes.sql`: kolumny `header_html` TEXT NULL i `footer_html` TEXT NULL w `email_mailboxes`.
|
||||
- `EmailMailboxRepository::save()`: zapis `header_html`/`footer_html` w INSERT i UPDATE.
|
||||
- `EmailMailboxController::save()`: pobiera `header_html`/`footer_html` z POST i przekazuje do repozytorium.
|
||||
- `resources/views/settings/email-mailboxes.php`: sekcja "Szablon wiadomosci" z dwoma edytorami Quill.js (email-safe toolbar: bold, italic, underline, kolor, wyrownanie, listy, link, image, naglowki h1-h3). Sync innerHTML do hidden inputs przy submit.
|
||||
- `EmailSendingService`: nowa metoda `composeBody()` — sklada header + body + footer. Uzywa variableResolver na header/footer. Uzyta w `send()` i `preview()`. NULL/pusty header/footer = pomijany.
|
||||
|
||||
## 2026-03-28 (Phase 50 - Allegro Shipment Waybill Push, Plan 01)
|
||||
- `AllegroShipmentService`:
|
||||
- po sukcesie `checkCreationStatus(...)` (gdy jest `tracking_number`) probuje dopiac przesylke do checkout form Allegro,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE email_mailboxes
|
||||
ADD COLUMN header_html TEXT NULL AFTER sender_name,
|
||||
ADD COLUMN footer_html TEXT NULL AFTER header_html;
|
||||
@@ -3,6 +3,14 @@ $mailboxes = is_array($mailboxes ?? null) ? $mailboxes : [];
|
||||
$em = is_array($editMailbox ?? null) ? $editMailbox : null;
|
||||
$isEdit = $em !== null;
|
||||
?>
|
||||
<link href="https://cdn.quilljs.com/2.0.3/quill.snow.css" rel="stylesheet">
|
||||
<style>
|
||||
#js-header-editor .ql-editor, #js-footer-editor .ql-editor { min-height: 80px; }
|
||||
.html-source-toggle { margin-top: 4px; display: flex; justify-content: flex-end; }
|
||||
.html-source-toggle button { font-size: 11px; padding: 2px 8px; cursor: pointer; background: #f5f5f5; border: 1px solid #ccc; border-radius: 3px; }
|
||||
.html-source-toggle button.active { background: #e0e0e0; font-weight: 600; }
|
||||
.html-source-area { width: 100%; min-height: 120px; font-family: monospace; font-size: 12px; border: 1px solid #ccc; padding: 8px; box-sizing: border-box; display: none; }
|
||||
</style>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title">Skrzynki pocztowe</h2>
|
||||
@@ -150,6 +158,25 @@ $isEdit = $em !== null;
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4 class="section-title mt-16">Szablon wiadomosci</h4>
|
||||
<p class="muted mt-4" style="font-size:12px">Opcjonalnie. Naglowek i stopka beda dolaczane do kazdego e-maila wysylanego z tej skrzynki.</p>
|
||||
|
||||
<div class="form-field mt-12">
|
||||
<span class="field-label">Naglowek (header)</span>
|
||||
<div id="js-header-editor" style="min-height:80px"></div>
|
||||
<textarea id="js-header-source" class="html-source-area"></textarea>
|
||||
<div class="html-source-toggle"><button type="button" class="js-toggle-html" data-editor="header"></> HTML</button> <button type="button" class="js-preview-html" data-editor="header">Podglad</button></div>
|
||||
<input type="hidden" name="header_html" id="js-header-html" value="">
|
||||
</div>
|
||||
|
||||
<div class="form-field mt-12">
|
||||
<span class="field-label">Stopka (footer)</span>
|
||||
<div id="js-footer-editor" style="min-height:80px"></div>
|
||||
<textarea id="js-footer-source" class="html-source-area"></textarea>
|
||||
<div class="html-source-toggle"><button type="button" class="js-toggle-html" data-editor="footer"></> HTML</button> <button type="button" class="js-preview-html" data-editor="footer">Podglad</button></div>
|
||||
<input type="hidden" name="footer_html" id="js-footer-html" value="">
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj skrzynke' ?></button>
|
||||
<button type="button" class="btn btn--secondary" id="js-test-connection">Testuj polaczenie</button>
|
||||
@@ -162,8 +189,176 @@ $isEdit = $em !== null;
|
||||
<div id="js-test-result" class="mt-12" style="display:none"></div>
|
||||
</section>
|
||||
|
||||
<div id="js-html-preview-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; z-index:9999; background:rgba(0,0,0,0.5);">
|
||||
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:700px; max-width:90vw; max-height:80vh; background:#fff; border-radius:6px; box-shadow:0 4px 24px rgba(0,0,0,0.2); display:flex; flex-direction:column;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:12px 16px; border-bottom:1px solid #eee;">
|
||||
<strong id="js-preview-title">Podglad</strong>
|
||||
<button type="button" id="js-preview-close" style="border:none; background:none; font-size:20px; cursor:pointer; padding:0 4px;">×</button>
|
||||
</div>
|
||||
<div style="flex:1; overflow:auto; padding:16px;">
|
||||
<iframe id="js-preview-iframe" style="width:100%; min-height:300px; border:none;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.quilljs.com/2.0.3/quill.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Quill editors for header/footer ---
|
||||
var quillToolbar = [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ align: [] }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
];
|
||||
|
||||
var headerEditor = new Quill('#js-header-editor', {
|
||||
theme: 'snow',
|
||||
modules: { toolbar: quillToolbar },
|
||||
placeholder: 'Naglowek wiadomosci (np. logo, nazwa firmy)...'
|
||||
});
|
||||
|
||||
var footerEditor = new Quill('#js-footer-editor', {
|
||||
theme: 'snow',
|
||||
modules: { toolbar: quillToolbar },
|
||||
placeholder: 'Stopka wiadomosci (np. dane kontaktowe, adres)...'
|
||||
});
|
||||
|
||||
// --- HTML source toggle ---
|
||||
var editors = { header: headerEditor, footer: footerEditor };
|
||||
var sourceMode = { header: false, footer: false };
|
||||
var rawHtml = { header: '', footer: '' };
|
||||
|
||||
function isRichHtml(html) {
|
||||
if (!html) return false;
|
||||
return /<(div|table|td|tr|meta|img)\b[^>]*style=/i.test(html) || /<meta\b/i.test(html);
|
||||
}
|
||||
|
||||
function activateSourceMode(key) {
|
||||
var quillContainer = document.getElementById('js-' + key + '-editor');
|
||||
var sourceArea = document.getElementById('js-' + key + '-source');
|
||||
var toolbar = quillContainer.parentNode.querySelector('.ql-toolbar');
|
||||
var btn = document.querySelector('.js-toggle-html[data-editor="' + key + '"]');
|
||||
sourceArea.value = rawHtml[key];
|
||||
quillContainer.style.display = 'none';
|
||||
if (toolbar) toolbar.style.display = 'none';
|
||||
sourceArea.style.display = 'block';
|
||||
if (btn) btn.classList.add('active');
|
||||
sourceMode[key] = true;
|
||||
}
|
||||
|
||||
function activateQuillMode(key) {
|
||||
var quillContainer = document.getElementById('js-' + key + '-editor');
|
||||
var sourceArea = document.getElementById('js-' + key + '-source');
|
||||
var toolbar = quillContainer.parentNode.querySelector('.ql-toolbar');
|
||||
var btn = document.querySelector('.js-toggle-html[data-editor="' + key + '"]');
|
||||
rawHtml[key] = sourceArea.value;
|
||||
editors[key].root.innerHTML = rawHtml[key] || '<p><br></p>';
|
||||
sourceArea.style.display = 'none';
|
||||
quillContainer.style.display = '';
|
||||
if (toolbar) toolbar.style.display = '';
|
||||
if (btn) btn.classList.remove('active');
|
||||
sourceMode[key] = false;
|
||||
}
|
||||
|
||||
// Load existing HTML — if it contains rich HTML (inline styles, divs), start in source mode
|
||||
<?php if ($isEdit && isset($em['header_html']) && $em['header_html'] !== null && $em['header_html'] !== ''): ?>
|
||||
rawHtml.header = <?= json_encode((string) $em['header_html'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
if (isRichHtml(rawHtml.header)) {
|
||||
activateSourceMode('header');
|
||||
} else {
|
||||
headerEditor.root.innerHTML = rawHtml.header;
|
||||
}
|
||||
<?php endif; ?>
|
||||
<?php if ($isEdit && isset($em['footer_html']) && $em['footer_html'] !== null && $em['footer_html'] !== ''): ?>
|
||||
rawHtml.footer = <?= json_encode((string) $em['footer_html'], JSON_UNESCAPED_UNICODE) ?>;
|
||||
if (isRichHtml(rawHtml.footer)) {
|
||||
activateSourceMode('footer');
|
||||
} else {
|
||||
footerEditor.root.innerHTML = rawHtml.footer;
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
document.querySelectorAll('.js-toggle-html').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var key = this.getAttribute('data-editor');
|
||||
if (!sourceMode[key]) {
|
||||
var quill = editors[key];
|
||||
var html = quill.root.innerHTML === '<p><br></p>' ? '' : quill.root.innerHTML;
|
||||
rawHtml[key] = rawHtml[key] && html === '' ? rawHtml[key] : html;
|
||||
activateSourceMode(key);
|
||||
} else {
|
||||
activateQuillMode(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Preview ---
|
||||
var previewModal = document.getElementById('js-html-preview-modal');
|
||||
var previewIframe = document.getElementById('js-preview-iframe');
|
||||
var previewTitle = document.getElementById('js-preview-title');
|
||||
|
||||
function getEditorHtml(key) {
|
||||
if (sourceMode[key]) {
|
||||
return document.getElementById('js-' + key + '-source').value.trim();
|
||||
}
|
||||
var html = editors[key].root.innerHTML;
|
||||
return html === '<p><br></p>' ? '' : html;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.js-preview-html').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var key = this.getAttribute('data-editor');
|
||||
var label = key === 'header' ? 'Naglowek (header)' : 'Stopka (footer)';
|
||||
var html = getEditorHtml(key);
|
||||
if (!html) {
|
||||
if (window.OrderProAlerts && window.OrderProAlerts.warning) {
|
||||
window.OrderProAlerts.warning('Podglad', 'Brak tresci do wyswietlenia.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
previewTitle.textContent = 'Podglad: ' + label;
|
||||
previewModal.style.display = 'block';
|
||||
var doc = previewIframe.contentDocument || previewIframe.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write('<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:16px;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#000;}</style></head><body>' + html + '</body></html>');
|
||||
doc.close();
|
||||
previewIframe.style.height = '0';
|
||||
setTimeout(function() {
|
||||
var h = doc.body.scrollHeight + 40;
|
||||
previewIframe.style.height = Math.min(Math.max(h, 150), 500) + 'px';
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('js-preview-close').addEventListener('click', function() {
|
||||
previewModal.style.display = 'none';
|
||||
});
|
||||
previewModal.addEventListener('click', function(e) {
|
||||
if (e.target === previewModal) previewModal.style.display = 'none';
|
||||
});
|
||||
|
||||
var mailboxForm = document.getElementById('js-mailbox-form');
|
||||
mailboxForm.addEventListener('submit', function() {
|
||||
var headerVal, footerVal;
|
||||
if (sourceMode.header) {
|
||||
headerVal = document.getElementById('js-header-source').value.trim();
|
||||
} else {
|
||||
headerVal = headerEditor.root.innerHTML === '<p><br></p>' ? '' : headerEditor.root.innerHTML;
|
||||
}
|
||||
if (sourceMode.footer) {
|
||||
footerVal = document.getElementById('js-footer-source').value.trim();
|
||||
} else {
|
||||
footerVal = footerEditor.root.innerHTML === '<p><br></p>' ? '' : footerEditor.root.innerHTML;
|
||||
}
|
||||
document.getElementById('js-header-html').value = headerVal;
|
||||
document.getElementById('js-footer-html').value = footerVal;
|
||||
});
|
||||
|
||||
// --- Delete confirm ---
|
||||
document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var form = this.closest('form');
|
||||
|
||||
@@ -63,6 +63,7 @@ final class EmailSendingService
|
||||
$variableMap = $this->variableResolver->buildVariableMap($order, $addresses, $companySettings);
|
||||
$resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap);
|
||||
$resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap);
|
||||
$resolvedBody = $this->composeBody($resolvedBody, $mailbox, $variableMap);
|
||||
|
||||
$attachments = [];
|
||||
$attachmentType = (string) ($template['attachment_1'] ?? '');
|
||||
@@ -142,6 +143,9 @@ final class EmailSendingService
|
||||
$resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap);
|
||||
$resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap);
|
||||
|
||||
$mailbox = $this->resolveMailbox(null, $template);
|
||||
$resolvedBody = $this->composeBody($resolvedBody, $mailbox, $variableMap);
|
||||
|
||||
$attachmentNames = [];
|
||||
$attachmentType = (string) ($template['attachment_1'] ?? '');
|
||||
if ($attachmentType !== '') {
|
||||
@@ -158,6 +162,38 @@ final class EmailSendingService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $mailbox
|
||||
* @param array<string, string> $variableMap
|
||||
*/
|
||||
private function composeBody(string $resolvedBody, ?array $mailbox, array $variableMap): string
|
||||
{
|
||||
if ($mailbox === null) {
|
||||
return $resolvedBody;
|
||||
}
|
||||
|
||||
$header = trim((string) ($mailbox['header_html'] ?? ''));
|
||||
$footer = trim((string) ($mailbox['footer_html'] ?? ''));
|
||||
|
||||
if ($header !== '') {
|
||||
$header = $this->variableResolver->resolve($header, $variableMap);
|
||||
}
|
||||
if ($footer !== '') {
|
||||
$footer = $this->variableResolver->resolve($footer, $variableMap);
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($header !== '') {
|
||||
$parts[] = $header;
|
||||
}
|
||||
$parts[] = $resolvedBody;
|
||||
if ($footer !== '') {
|
||||
$parts[] = $footer;
|
||||
}
|
||||
|
||||
return implode("\n", $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $template
|
||||
* @return array<string, mixed>|null
|
||||
|
||||
@@ -88,6 +88,8 @@ final class EmailMailboxController
|
||||
'smtp_password' => $password,
|
||||
'sender_email' => $senderEmail,
|
||||
'sender_name' => $request->input('sender_name', ''),
|
||||
'header_html' => $request->input('header_html', ''),
|
||||
'footer_html' => $request->input('footer_html', ''),
|
||||
'is_default' => $request->input('is_default', null),
|
||||
'is_active' => $request->input('is_active', null),
|
||||
]);
|
||||
|
||||
@@ -84,6 +84,9 @@ final class EmailMailboxRepository
|
||||
$this->pdo->prepare('UPDATE email_mailboxes SET is_default = 0 WHERE is_default = 1')->execute();
|
||||
}
|
||||
|
||||
$headerHtml = isset($data['header_html']) && trim((string) $data['header_html']) !== '' ? trim((string) $data['header_html']) : null;
|
||||
$footerHtml = isset($data['footer_html']) && trim((string) $data['footer_html']) !== '' ? trim((string) $data['footer_html']) : null;
|
||||
|
||||
$params = [
|
||||
'name' => trim((string) ($data['name'] ?? '')),
|
||||
'smtp_host' => trim((string) ($data['smtp_host'] ?? '')),
|
||||
@@ -92,6 +95,8 @@ final class EmailMailboxRepository
|
||||
'smtp_username' => trim((string) ($data['smtp_username'] ?? '')),
|
||||
'sender_email' => trim((string) ($data['sender_email'] ?? '')),
|
||||
'sender_name' => trim((string) ($data['sender_name'] ?? '')) ?: null,
|
||||
'header_html' => $headerHtml,
|
||||
'footer_html' => $footerHtml,
|
||||
'is_default' => $isDefault,
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0,
|
||||
];
|
||||
@@ -110,6 +115,8 @@ final class EmailMailboxRepository
|
||||
'smtp_username = :smtp_username',
|
||||
'sender_email = :sender_email',
|
||||
'sender_name = :sender_name',
|
||||
'header_html = :header_html',
|
||||
'footer_html = :footer_html',
|
||||
'is_default = :is_default',
|
||||
'is_active = :is_active',
|
||||
];
|
||||
@@ -128,8 +135,8 @@ final class EmailMailboxRepository
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO email_mailboxes (name, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_encrypted, sender_email, sender_name, is_default, is_active)
|
||||
VALUES (:name, :smtp_host, :smtp_port, :smtp_encryption, :smtp_username, :smtp_password_encrypted, :sender_email, :sender_name, :is_default, :is_active)'
|
||||
'INSERT INTO email_mailboxes (name, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_encrypted, sender_email, sender_name, header_html, footer_html, is_default, is_active)
|
||||
VALUES (:name, :smtp_host, :smtp_port, :smtp_encryption, :smtp_username, :smtp_password_encrypted, :sender_email, :sender_name, :header_html, :footer_html, :is_default, :is_active)'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user