This commit is contained in:
2026-03-28 21:16:21 +01:00
parent 572643ad82
commit cbc2058b83
15 changed files with 767 additions and 277 deletions

View File

@@ -13,7 +13,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value | | Attribute | Value |
|-----------|-------| |-----------|-------|
| Version | 1.0.0 | | Version | 1.0.0 |
| Status | v2.2 Complete | | Status | v2.3 Complete |
| Last Updated | 2026-03-28 | | Last Updated | 2026-03-28 |
## Requirements ## 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] 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] 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] 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) ### Active (In Progress)
@@ -160,6 +161,6 @@ Quick Reference:
--- ---
*PROJECT.md — Updated when requirements or context change* *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)*

View File

@@ -8,10 +8,23 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod
No active milestone - Ready to define next scope 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 ## 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> <details>
<summary>v2.2 Allegro Shipment Waybill Push - 2026-03-28 (1 phase, 1 plan)</summary> <summary>v2.2 Allegro Shipment Waybill Push - 2026-03-28 (1 phase, 1 plan)</summary>

View File

@@ -5,46 +5,48 @@
See: .paul/PROJECT.md (updated 2026-03-28) 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. **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 ## Current Position
Milestone: v2.2 Allegro Shipment Waybill Push - Complete Milestone: v2.3 Email HTML Layout - Complete
Phase: 1 of 1 (50 - Allegro Shipment Waybill Push) - Complete Phase: 1 of 1 (51 - Email HTML Layout) Complete
Plan: 50-01 complete Plan: 51-01 complete
Status: Loop complete - ready for next PLAN 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: Progress:
- Milestone: [##########] 100% - Milestone: [##########] 100%
- Phase 50: [##########] 100% - Phase 51: [##########] 100%
## Loop Position ## Loop Position
Current loop state: Current loop state:
``` ```
PLAN --> APPLY --> UNIFY PLAN ──▶ APPLY ──▶ UNIFY
done done done [Loop complete - ready for next PLAN] ✓ ✓ [Loop complete - ready for next PLAN]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-03-28 15:33:00 Last session: 2026-03-28
Stopped at: Phase 50 complete, milestone v2.2 complete Stopped at: Phase 51 complete, milestone v2.3 complete
Next action: Uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu Next action: Uruchom /paul:milestone (lub /paul:plan) dla kolejnego celu
Resume file: .paul/phases/50-allegro-shipment-waybill-push/50-01-SUMMARY.md Resume file: .paul/phases/51-email-html-layout/51-01-SUMMARY.md
## Accumulated Context ## Accumulated Context
### Decisions ### Decisions
| Date | Decision | Impact | | 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 | Header/footer HTML na poziomie skrzynki (nie szablonu) | Spojny branding bez duplikacji w kazdym szablonie |
| 2026-03-28 | Blad pushu waybilla do Allegro jest niekrytyczny dla tworzenia paczki | Lokalna przesylka nie ginie przy problemie API Allegro | | 2026-03-28 | Quill.js z ograniczonym toolbar (email-safe) zamiast MJML/dedykowanego buildera | Prostota; brak build pipeline w projekcie |
| 2026-03-28 | Retry pushu po `ALLEGRO_HTTP_401` przez ponowne `resolveToken()` | Wyzsza odpornosc na wygasle tokeny | | 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 ## Git State
Last commit: 176d740 Last commit: 572643a
Branch: main Branch: main
Feature branches merged: none Feature branches merged: none

View 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>

View 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*

View 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;">&nbsp;</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]-->

View File

@@ -7,12 +7,6 @@
"lmtime": 1772652932723, "lmtime": 1772652932723,
"modified": false "modified": false
}, },
"_allegro_check.php": {
"type": "-",
"size": 1954,
"lmtime": 1772803697369,
"modified": false
},
"ARCHITECTURE.md": { "ARCHITECTURE.md": {
"type": "-", "type": "-",
"size": 659, "size": 659,
@@ -113,7 +107,6 @@
"modified": false "modified": false
} }
}, },
".claude": {},
"CLAUDE.md": { "CLAUDE.md": {
"type": "-", "type": "-",
"size": 3460, "size": 3460,
@@ -121,41 +114,17 @@
"modified": true "modified": true
}, },
"clients": {}, "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": { "composer-setup.php": {
"type": "-", "type": "-",
"size": 59524, "size": 59524,
"lmtime": 0, "lmtime": 0,
"modified": false "modified": false
}, },
"composer-temp.phar": {
"type": "-",
"size": 3288946,
"lmtime": 0,
"modified": false
},
"config": { "config": {
"app.php": { "app.php": {
"type": "-", "type": "-",
"size": 972, "size": 1033,
"lmtime": 1771955055783, "lmtime": 1774706127464,
"modified": false "modified": false
}, },
"auth.php": { "auth.php": {
@@ -568,6 +537,12 @@
"size": 319, "size": 319,
"lmtime": 1774611787688, "lmtime": 1774611787688,
"modified": false "modified": false
},
"20260328_000072_create_automation_execution_logs_table.sql": {
"type": "-",
"size": 1653,
"lmtime": 1774702996564,
"modified": false
} }
}, },
"seeders": {}, "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": { "DOCS": {
"ARCHITECTURE.md": { "ARCHITECTURE.md": {
"type": "-", "type": "-",
"size": 35558, "size": 39802,
"lmtime": 1774612062257, "lmtime": 1774707780233,
"modified": false "modified": false
}, },
"DB_SCHEMA.md": { "DB_SCHEMA.md": {
"type": "-", "type": "-",
"size": 29871, "size": 32219,
"lmtime": 1774612041000, "lmtime": 1774706184511,
"modified": false "modified": false
}, },
"ORDERS_SCHEMA_APILO_DRAFT.md": { "ORDERS_SCHEMA_APILO_DRAFT.md": {
@@ -655,51 +594,27 @@
}, },
"TECH_CHANGELOG.md": { "TECH_CHANGELOG.md": {
"type": "-", "type": "-",
"size": 57684, "size": 65574,
"lmtime": 1774612077539, "lmtime": 1774707789181,
"modified": false "modified": false
}, },
"todo.md": { "todo.md": {
"type": "-", "type": "-",
"size": 3512, "size": 39751,
"lmtime": 1774474971584, "lmtime": 1774474971584,
"modified": false "modified": true
} }
}, },
".env": { ".env": {
"type": "-", "type": "-",
"size": 538, "size": 596,
"lmtime": 1774565782052, "lmtime": 1774706394170,
"modified": false "modified": false
}, },
".env.codex.bak": {
"type": "-",
"size": 54,
"lmtime": 1771866989245,
"modified": true
},
".env.example": { ".env.example": {
"type": "-", "type": "-",
"size": 580, "size": 661,
"lmtime": 1772491020678, "lmtime": 1774706130497,
"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,
"modified": false "modified": false
}, },
".gitignore": { ".gitignore": {
@@ -720,12 +635,6 @@
"lmtime": 1771459937874, "lmtime": 1771459937874,
"modified": false "modified": false
}, },
"log.md": {
"type": "-",
"size": 4574,
"lmtime": 1771963733140,
"modified": false
},
".mcp.json": { ".mcp.json": {
"type": "-", "type": "-",
"size": 397, "size": 397,
@@ -1898,28 +1807,19 @@
"lmtime": 1771869056394, "lmtime": 1771869056394,
"modified": false "modified": false
}, },
".paul": {},
"phpunit.xml": { "phpunit.xml": {
"type": "-", "type": "-",
"size": 480, "size": 480,
"lmtime": 1772489488633, "lmtime": 1772489488633,
"modified": false "modified": false
}, },
".playwright-mcp": {
"console-2026-03-26T22-55-27-422Z.log": {
"type": "-",
"size": 138,
"lmtime": 1774565727939,
"modified": false
}
},
"public": { "public": {
"assets": { "assets": {
"css": { "css": {
"app.css": { "app.css": {
"type": "-", "type": "-",
"size": 44903, "size": 45416,
"lmtime": 1774600385594, "lmtime": 1774702916830,
"modified": false "modified": false
}, },
"app.css.map": { "app.css.map": {
@@ -1931,7 +1831,7 @@
"login.css": { "login.css": {
"type": "-", "type": "-",
"size": 5996, "size": 5996,
"lmtime": 1774474932148, "lmtime": 1774702917327,
"modified": false "modified": false
}, },
"login.css.map": { "login.css.map": {
@@ -1954,8 +1854,8 @@
"modules": { "modules": {
"automation-form.js": { "automation-form.js": {
"type": "-", "type": "-",
"size": 7251, "size": 8789,
"lmtime": 1774475530521, "lmtime": 1774704031241,
"modified": false "modified": false
}, },
"inline-status-change.js": { "inline-status-change.js": {
@@ -2027,8 +1927,8 @@
}, },
"app.scss": { "app.scss": {
"type": "-", "type": "-",
"size": 43794, "size": 43784,
"lmtime": 1774600368218, "lmtime": 1774701658193,
"modified": false "modified": false
}, },
"login.css": { "login.css": {
@@ -2052,8 +1952,8 @@
"modules": { "modules": {
"_automation.scss": { "_automation.scss": {
"type": "-", "type": "-",
"size": 1038, "size": 1565,
"lmtime": 1773789611848, "lmtime": 1774702761945,
"modified": false "modified": false
}, },
"_delivery-status-mappings.scss": { "_delivery-status-mappings.scss": {
@@ -2232,9 +2132,9 @@
}, },
"allegro.php": { "allegro.php": {
"type": "-", "type": "-",
"size": 38673, "size": 38653,
"lmtime": 1774565889261, "lmtime": 1774565889261,
"modified": false "modified": true
}, },
"apaczka.php": { "apaczka.php": {
"type": "-", "type": "-",
@@ -2274,8 +2174,8 @@
}, },
"email-templates.php": { "email-templates.php": {
"type": "-", "type": "-",
"size": 14153, "size": 14291,
"lmtime": 1774564964461, "lmtime": 1774701677322,
"modified": false "modified": false
}, },
"gs1.php": { "gs1.php": {
@@ -2346,14 +2246,14 @@
"automation": { "automation": {
"form.php": { "form.php": {
"type": "-", "type": "-",
"size": 12406, "size": 14751,
"lmtime": 1774475517368, "lmtime": 1774704020030,
"modified": false "modified": false
}, },
"index.php": { "index.php": {
"type": "-", "type": "-",
"size": 4426, "size": 15170,
"lmtime": 1774566437972, "lmtime": 1774703149752,
"modified": false "modified": false
} }
} }
@@ -2362,64 +2262,17 @@
"routes": { "routes": {
"web.php": { "web.php": {
"type": "-", "type": "-",
"size": 24995, "size": 27445,
"lmtime": 1774566431014, "lmtime": 1774706514882,
"modified": false "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": { "src": {
"Core": { "Core": {
"Application.php": { "Application.php": {
"type": "-", "type": "-",
"size": 9588, "size": 9697,
"lmtime": 1774474842256, "lmtime": 1774706140100,
"modified": false "modified": false
}, },
"Constants": { "Constants": {
@@ -2621,10 +2474,16 @@
"lmtime": 1772655083686, "lmtime": 1772655083686,
"modified": false "modified": false
}, },
"AutomationHistoryCleanupHandler.php": {
"type": "-",
"size": 694,
"lmtime": 1774702487146,
"modified": false
},
"CronHandlerFactory.php": { "CronHandlerFactory.php": {
"type": "-", "type": "-",
"size": 7970, "size": 8710,
"lmtime": 1774612020782, "lmtime": 1774702549664,
"modified": false "modified": false
}, },
"CronJobProcessor.php": { "CronJobProcessor.php": {
@@ -3173,9 +3032,9 @@
"Shipments": { "Shipments": {
"AllegroShipmentService.php": { "AllegroShipmentService.php": {
"type": "-", "type": "-",
"size": 14932, "size": 18411,
"lmtime": 1773396238404, "lmtime": 1774707709265,
"modified": true "modified": false
}, },
"AllegroTrackingService.php": { "AllegroTrackingService.php": {
"type": "-", "type": "-",
@@ -3185,8 +3044,8 @@
}, },
"ApaczkaShipmentService.php": { "ApaczkaShipmentService.php": {
"type": "-", "type": "-",
"size": 33757, "size": 35982,
"lmtime": 1774303090337, "lmtime": 1774705073868,
"modified": false "modified": false
}, },
"ApaczkaTrackingService.php": { "ApaczkaTrackingService.php": {
@@ -3221,15 +3080,15 @@
}, },
"ShipmentController.php": { "ShipmentController.php": {
"type": "-", "type": "-",
"size": 18873, "size": 20135,
"lmtime": 1774285889068, "lmtime": 1774285889068,
"modified": false "modified": true
}, },
"ShipmentPackageRepository.php": { "ShipmentPackageRepository.php": {
"type": "-", "type": "-",
"size": 8391, "size": 8836,
"lmtime": 1774296780272, "lmtime": 1774296780272,
"modified": false "modified": true
}, },
"ShipmentPresetController.php": { "ShipmentPresetController.php": {
"type": "-", "type": "-",
@@ -3285,20 +3144,26 @@
"Automation": { "Automation": {
"AutomationController.php": { "AutomationController.php": {
"type": "-", "type": "-",
"size": 15747, "size": 19948,
"lmtime": 1774566422090, "lmtime": 1774703990637,
"modified": false
},
"AutomationExecutionLogRepository.php": {
"type": "-",
"size": 6493,
"lmtime": 1774702487160,
"modified": false "modified": false
}, },
"AutomationRepository.php": { "AutomationRepository.php": {
"type": "-", "type": "-",
"size": 9712, "size": 10496,
"lmtime": 1774566398224, "lmtime": 1774703977978,
"modified": false "modified": false
}, },
"AutomationService.php": { "AutomationService.php": {
"type": "-", "type": "-",
"size": 22368, "size": 29708,
"lmtime": 1774475866269, "lmtime": 1774704000450,
"modified": false "modified": false
} }
}, },
@@ -3335,8 +3200,8 @@
"phpunit": { "phpunit": {
"test-results": { "test-results": {
"type": "-", "type": "-",
"size": 503, "size": 3083,
"lmtime": 1772489557946, "lmtime": 1774707817763,
"modified": false "modified": false
} }
} }
@@ -5181,12 +5046,6 @@
"lmtime": 1772490702841, "lmtime": 1772490702841,
"modified": false "modified": false
}, },
"_test_apaczka.php": {
"type": "-",
"size": 2690,
"lmtime": 1774296169244,
"modified": false
},
"tests": { "tests": {
"bootstrap.php": { "bootstrap.php": {
"type": "-", "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": { "tools": {
"apaczka_probe_order.php": { "apaczka_probe_order.php": {
"type": "-", "type": "-",

View File

@@ -29,7 +29,7 @@
- `App\Modules\Accounting` (modul paragonow — wystawianie, podglad, druk, PDF, lista, eksport XLSX) - `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\EmailMailbox*` (skrzynki pocztowe SMTP — CRUD + test polaczenia)
- `App\Modules\Settings\EmailTemplate*` (szablony e-mail — CRUD + Quill.js + zmienne + zalaczniki) - `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) - `App\Modules\Automation` (zadania automatyczne — reguly zdarzenie/warunki/akcje, CRUD)
## Routing ## Routing

View File

@@ -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) - `smtp_password_encrypted` TEXT NOT NULL — szyfrowane IntegrationSecretCipher (AES-256-CBC+HMAC)
- `sender_email` VARCHAR(255) NOT NULL - `sender_email` VARCHAR(255) NOT NULL
- `sender_name` VARCHAR(200) DEFAULT 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_default` TINYINT(1) NOT NULL DEFAULT 0
- `is_active` TINYINT(1) NOT NULL DEFAULT 1 - `is_active` TINYINT(1) NOT NULL DEFAULT 1
- `created_at`, `updated_at` DATETIME - `created_at`, `updated_at` DATETIME

View File

@@ -1,5 +1,12 @@
# Tech Changelog # 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) ## 2026-03-28 (Phase 50 - Allegro Shipment Waybill Push, Plan 01)
- `AllegroShipmentService`: - `AllegroShipmentService`:
- po sukcesie `checkCreationStatus(...)` (gdy jest `tracking_number`) probuje dopiac przesylke do checkout form Allegro, - po sukcesie `checkCreationStatus(...)` (gdy jest `tracking_number`) probuje dopiac przesylke do checkout form Allegro,

View File

@@ -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;

View File

@@ -3,6 +3,14 @@ $mailboxes = is_array($mailboxes ?? null) ? $mailboxes : [];
$em = is_array($editMailbox ?? null) ? $editMailbox : null; $em = is_array($editMailbox ?? null) ? $editMailbox : null;
$isEdit = $em !== 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"> <section class="card">
<h2 class="section-title">Skrzynki pocztowe</h2> <h2 class="section-title">Skrzynki pocztowe</h2>
@@ -150,6 +158,25 @@ $isEdit = $em !== null;
</label> </label>
</div> </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">&lt;/&gt; 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">&lt;/&gt; 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"> <div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj skrzynke' ?></button> <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> <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> <div id="js-test-result" class="mt-12" style="display:none"></div>
</section> </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;">&times;</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> <script>
document.addEventListener('DOMContentLoaded', function() { 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) { document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
var form = this.closest('form'); var form = this.closest('form');

View File

@@ -63,6 +63,7 @@ final class EmailSendingService
$variableMap = $this->variableResolver->buildVariableMap($order, $addresses, $companySettings); $variableMap = $this->variableResolver->buildVariableMap($order, $addresses, $companySettings);
$resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap); $resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap);
$resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap); $resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap);
$resolvedBody = $this->composeBody($resolvedBody, $mailbox, $variableMap);
$attachments = []; $attachments = [];
$attachmentType = (string) ($template['attachment_1'] ?? ''); $attachmentType = (string) ($template['attachment_1'] ?? '');
@@ -142,6 +143,9 @@ final class EmailSendingService
$resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap); $resolvedSubject = $this->variableResolver->resolve((string) ($template['subject'] ?? ''), $variableMap);
$resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap); $resolvedBody = $this->variableResolver->resolve((string) ($template['body_html'] ?? ''), $variableMap);
$mailbox = $this->resolveMailbox(null, $template);
$resolvedBody = $this->composeBody($resolvedBody, $mailbox, $variableMap);
$attachmentNames = []; $attachmentNames = [];
$attachmentType = (string) ($template['attachment_1'] ?? ''); $attachmentType = (string) ($template['attachment_1'] ?? '');
if ($attachmentType !== '') { 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 * @param array<string, mixed>|null $template
* @return array<string, mixed>|null * @return array<string, mixed>|null

View File

@@ -88,6 +88,8 @@ final class EmailMailboxController
'smtp_password' => $password, 'smtp_password' => $password,
'sender_email' => $senderEmail, 'sender_email' => $senderEmail,
'sender_name' => $request->input('sender_name', ''), '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_default' => $request->input('is_default', null),
'is_active' => $request->input('is_active', null), 'is_active' => $request->input('is_active', null),
]); ]);

View File

@@ -84,6 +84,9 @@ final class EmailMailboxRepository
$this->pdo->prepare('UPDATE email_mailboxes SET is_default = 0 WHERE is_default = 1')->execute(); $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 = [ $params = [
'name' => trim((string) ($data['name'] ?? '')), 'name' => trim((string) ($data['name'] ?? '')),
'smtp_host' => trim((string) ($data['smtp_host'] ?? '')), 'smtp_host' => trim((string) ($data['smtp_host'] ?? '')),
@@ -92,6 +95,8 @@ final class EmailMailboxRepository
'smtp_username' => trim((string) ($data['smtp_username'] ?? '')), 'smtp_username' => trim((string) ($data['smtp_username'] ?? '')),
'sender_email' => trim((string) ($data['sender_email'] ?? '')), 'sender_email' => trim((string) ($data['sender_email'] ?? '')),
'sender_name' => trim((string) ($data['sender_name'] ?? '')) ?: null, 'sender_name' => trim((string) ($data['sender_name'] ?? '')) ?: null,
'header_html' => $headerHtml,
'footer_html' => $footerHtml,
'is_default' => $isDefault, 'is_default' => $isDefault,
'is_active' => isset($data['is_active']) ? 1 : 0, 'is_active' => isset($data['is_active']) ? 1 : 0,
]; ];
@@ -110,6 +115,8 @@ final class EmailMailboxRepository
'smtp_username = :smtp_username', 'smtp_username = :smtp_username',
'sender_email = :sender_email', 'sender_email = :sender_email',
'sender_name = :sender_name', 'sender_name = :sender_name',
'header_html = :header_html',
'footer_html = :footer_html',
'is_default = :is_default', 'is_default = :is_default',
'is_active = :is_active', 'is_active = :is_active',
]; ];
@@ -128,8 +135,8 @@ final class EmailMailboxRepository
} }
$statement = $this->pdo->prepare( $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) '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, :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)'
); );
} }