feat(11-receipt-print): phase 11 complete — receipt preview, print & PDF
Add receipt show/print/pdf endpoints with dompdf integration. Active preview and PDF links in order Documents tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ Moduł księgowości z obsługą paragonów: wielokonfiguracyjne szablony, wysta
|
|||||||
| 8 | DB Foundation + Company Settings | 1/1 | Complete ✓ |
|
| 8 | DB Foundation + Company Settings | 1/1 | Complete ✓ |
|
||||||
| 9 | Konfiguracja paragonów (Ustawienia) | 1/1 | Complete ✓ |
|
| 9 | Konfiguracja paragonów (Ustawienia) | 1/1 | Complete ✓ |
|
||||||
| 10 | Wystawianie paragonów z zamówienia | 1/1 | Complete ✓ |
|
| 10 | Wystawianie paragonów z zamówienia | 1/1 | Complete ✓ |
|
||||||
| 11 | Podgląd i wydruk paragonu (HTML+PDF) | 1 | Not started |
|
| 11 | Podgląd i wydruk paragonu (HTML+PDF) | 1/1 | Complete ✓ |
|
||||||
| 12 | Sekcja Księgowość — lista + eksport XLSX | 1 | Not started |
|
| 12 | Sekcja Księgowość — lista + eksport XLSX | 1 | Not started |
|
||||||
|
|
||||||
## Completed Milestones
|
## Completed Milestones
|
||||||
@@ -56,4 +56,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Roadmap created: 2026-03-12*
|
*Roadmap created: 2026-03-12*
|
||||||
*Last updated: 2026-03-15 — milestone v0.2 complete*
|
*Last updated: 2026-03-15 — phase 11 complete*
|
||||||
|
|||||||
@@ -5,31 +5,31 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||||
|
|
||||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||||
**Current focus:** Milestone v0.3 Moduł Paragonów — Faza 11 Podgląd i wydruk paragonu.
|
**Current focus:** Milestone v0.3 Moduł Paragonów — Faza 11 COMPLETE, transition do fazy 12.
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v0.3 Moduł Paragonów
|
Milestone: v0.3 Moduł Paragonów
|
||||||
Phase: 10 of 12 (10-receipt-issue) — Complete ✓
|
Phase: 11 of 12 (11-receipt-print) — Complete
|
||||||
Plan: 10-01 ✓
|
Plan: 11-01 complete
|
||||||
Status: Phase 10 complete — ready for Phase 11
|
Status: Loop closed, phase transition required
|
||||||
Last activity: 2026-03-15 — Phase 10 loop closed
|
Last activity: 2026-03-15 — Phase 11 complete (podgląd, druk, PDF paragonu)
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- v0.1 Initial Release: [██████████] 100% ✓
|
- v0.1 Initial Release: [██████████] 100% ✓
|
||||||
- v0.2 Pre-Expansion Fixes: [██████████] 100% ✓
|
- v0.2 Pre-Expansion Fixes: [██████████] 100% ✓
|
||||||
- v0.3 Moduł Paragonów: [██████░░░░] 60%
|
- v0.3 Moduł Paragonów: [████████░░] 80%
|
||||||
- Phase 8: [██████████] 100% ✓
|
- Phase 8: [██████████] 100% ✓
|
||||||
- Phase 9: [██████████] 100% ✓
|
- Phase 9: [██████████] 100% ✓
|
||||||
- Phase 10: [██████████] 100% ✓
|
- Phase 10: [██████████] 100% ✓
|
||||||
- Phase 11: [░░░░░░░░░░] 0%
|
- Phase 11: [██████████] 100% ✓
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Phase 10 complete — ready for next phase]
|
✓ ✓ ✓ [Loop complete — phase transition required]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
@@ -50,6 +50,13 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
|||||||
| 2026-03-15 | Snapshot pattern: seller/buyer/items jako JSON | Faza 10 | Dane zamrożone w momencie wystawienia paragonu |
|
| 2026-03-15 | Snapshot pattern: seller/buyer/items jako JSON | Faza 10 | Dane zamrożone w momencie wystawienia paragonu |
|
||||||
| 2026-03-15 | Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE | Faza 10 | Bezpieczne kolejne numery paragonów |
|
| 2026-03-15 | Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE | Faza 10 | Bezpieczne kolejne numery paragonów |
|
||||||
| 2026-03-15 | Moduł Accounting w App\Modules\Accounting | Faza 10 | Separacja od Settings |
|
| 2026-03-15 | Moduł Accounting w App\Modules\Accounting | Faza 10 | Separacja od Settings |
|
||||||
|
| 2026-03-15 | dompdf v3.1 server-side PDF generation | Faza 11 | Nowa zależność composer; wymaga vendor/ na serwerze |
|
||||||
|
| 2026-03-15 | ftp-kr vendor/ nie ignorowany (zmiana na /vendor/bin) | Faza 11 | Automatyczny upload vendor/ przy zmianach; rewizja decyzji z fazy 07 |
|
||||||
|
|
||||||
|
### Skill Audit (Faza 11, Plan 01)
|
||||||
|
| Oczekiwany | Wywołany | Uwagi |
|
||||||
|
|------------|---------|-------|
|
||||||
|
| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY |
|
||||||
|
|
||||||
### Skill Audit (Faza 10, Plan 01)
|
### Skill Audit (Faza 10, Plan 01)
|
||||||
| Oczekiwany | Wywołany | Uwagi |
|
| Oczekiwany | Wywołany | Uwagi |
|
||||||
@@ -125,7 +132,7 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
|||||||
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
|
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
|
||||||
|
|
||||||
### Git State
|
### Git State
|
||||||
Last commit: pending (phase 10 commit)
|
Last commit: ed057fc (feat(08-10-receipt-module): phases 08-10 complete)
|
||||||
Branch: main
|
Branch: main
|
||||||
Feature branches merged: none
|
Feature branches merged: none
|
||||||
|
|
||||||
@@ -135,13 +142,13 @@ Brak.
|
|||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-15
|
Last session: 2026-03-15
|
||||||
Stopped at: Phase 10 complete
|
Stopped at: Phase 11 complete, transition required
|
||||||
Next action: /paul:plan for Phase 11 (Podglad i wydruk paragonu HTML+PDF)
|
Next action: Phase transition — git commit, ROADMAP update, then /paul:plan for phase 12
|
||||||
Resume file: .paul/phases/10-receipt-issue/10-01-SUMMARY.md
|
Resume file: .paul/phases/11-receipt-print/11-01-SUMMARY.md
|
||||||
Resume context:
|
Resume context:
|
||||||
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
||||||
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
||||||
- v0.3: IN PROGRESS — Phase 08-10 done, Phase 11 next
|
- v0.3: IN PROGRESS — Phase 08-11 done, Phase 12 next
|
||||||
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
282
.paul/phases/11-receipt-print/11-01-PLAN.md
Normal file
282
.paul/phases/11-receipt-print/11-01-PLAN.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
---
|
||||||
|
phase: 11-receipt-print
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: ["10-01"]
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Accounting/ReceiptController.php
|
||||||
|
- resources/views/receipts/show.php
|
||||||
|
- resources/views/receipts/print.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/shared/_ui-components.scss
|
||||||
|
- resources/scss/app.scss
|
||||||
|
- public/assets/css/app.css
|
||||||
|
- composer.json
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
autonomous: false
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Podglad paragonu w formie HTML oraz generowanie PDF — uzytkownik moze otworzyc paragon, zobaczyc jego tresc i pobrac/wydrukować jako PDF.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Bez podgladu i wydruku wystawione paragony sa tylko rekordami w bazie — nie mozna ich przekazac klientowi ani zweryfikowac wizualnie. Ta faza zamyka cykl: wystawienie → podglad → wydruk/PDF.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Widok podgladu paragonu (HTML w layoucie aplikacji)
|
||||||
|
- Widok druku paragonu (standalone HTML, bez nawigacji, gotowy do window.print())
|
||||||
|
- Generowanie PDF przez dompdf
|
||||||
|
- Aktywny link "Podglad" w zakladce Dokumenty
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/10-receipt-issue/10-01-SUMMARY.md — ReceiptRepository, snapshoty JSON, przycisk w zamowieniu
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Accounting/ReceiptRepository.php
|
||||||
|
@src/Modules/Accounting/ReceiptController.php
|
||||||
|
@resources/views/orders/show.php
|
||||||
|
@routes/web.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
## Required Skills (from SPECIAL-FLOWS.md)
|
||||||
|
|
||||||
|
| Skill | Priority | When to Invoke | Loaded? |
|
||||||
|
|-------|----------|----------------|---------|
|
||||||
|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||||
|
|
||||||
|
## Skill Invocation Checklist
|
||||||
|
- [ ] sonar-scanner uruchomiony po APPLY
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Podglad paragonu w layoucie aplikacji
|
||||||
|
```gherkin
|
||||||
|
Given paragon o id={receiptId} istnieje dla zamowienia {orderId}
|
||||||
|
When uzytkownik otwiera /orders/{orderId}/receipt/{receiptId}
|
||||||
|
Then wyswietlany jest podglad paragonu z danymi:
|
||||||
|
- numer paragonu, data wystawienia, data sprzedazy
|
||||||
|
- dane sprzedawcy (z seller_data_json)
|
||||||
|
- dane nabywcy (z buyer_data_json, jesli is_named)
|
||||||
|
- tabela pozycji (z items_json): nazwa, ilosc, cena, wartosc
|
||||||
|
- suma brutto
|
||||||
|
- numer referencyjny zamowienia (jesli skonfigurowany)
|
||||||
|
And widoczne sa przyciski: "Drukuj", "Pobierz PDF", "Powrot"
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Widok druku (standalone HTML)
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik jest na podgladzie paragonu
|
||||||
|
When klika przycisk "Drukuj"
|
||||||
|
Then otwiera sie nowe okno/zakladka z czystym widokiem paragonu (bez nawigacji aplikacji)
|
||||||
|
And automatycznie uruchamia sie window.print()
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Generowanie PDF
|
||||||
|
```gherkin
|
||||||
|
Given paragon o id={receiptId} istnieje
|
||||||
|
When uzytkownik klika "Pobierz PDF" lub otwiera /orders/{orderId}/receipt/{receiptId}/pdf
|
||||||
|
Then przeglądarka pobiera plik PDF z trescia paragonu
|
||||||
|
And plik ma nazwe: {receipt_number}.pdf (znaki specjalne zamienione na _)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Aktywny link "Podglad" w zakladce Dokumenty
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie ma wystawione paragony
|
||||||
|
When uzytkownik otwiera zakladke "Dokumenty"
|
||||||
|
Then przycisk "Podglad" przy kazdym paragonie jest aktywny (nie disabled)
|
||||||
|
And prowadzi do /orders/{orderId}/receipt/{receiptId}
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Obsluga bledu — nieistniejacy paragon
|
||||||
|
```gherkin
|
||||||
|
Given paragon o danym id nie istnieje lub nie nalezy do zamowienia
|
||||||
|
When uzytkownik otwiera /orders/{orderId}/receipt/{receiptId}
|
||||||
|
Then zwracany jest 404 Not Found
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Instalacja dompdf + metody kontrolera (show, print, pdf)</name>
|
||||||
|
<files>
|
||||||
|
composer.json,
|
||||||
|
src/Modules/Accounting/ReceiptController.php,
|
||||||
|
routes/web.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. **Instalacja dompdf:**
|
||||||
|
- `composer require dompdf/dompdf` (w katalogu projektu)
|
||||||
|
- UWAGA: vendor/ jest w ftp-kr ignore — po deploy trzeba recznie zainstalowac na serwerze
|
||||||
|
|
||||||
|
2. **ReceiptController — nowe metody:**
|
||||||
|
- **show(Request): Response** — GET /orders/{id}/receipt/{receiptId}
|
||||||
|
- Pobierz paragon: ReceiptRepository::findById($receiptId)
|
||||||
|
- Waliduj: paragon istnieje i paragon.order_id == $orderId, inaczej 404
|
||||||
|
- Dekoduj JSON: seller_data_json, buyer_data_json, items_json
|
||||||
|
- Pobierz config name: ReceiptConfigRepository::findById(config_id)
|
||||||
|
- Renderuj widok `receipts/show` w layoucie `layouts/app`
|
||||||
|
- **printView(Request): Response** — GET /orders/{id}/receipt/{receiptId}/print
|
||||||
|
- Te same dane co show()
|
||||||
|
- Renderuj widok `receipts/print` BEZ layoutu (standalone HTML)
|
||||||
|
- **pdf(Request): Response** — GET /orders/{id}/receipt/{receiptId}/pdf
|
||||||
|
- Te same dane co show()
|
||||||
|
- Renderuj widok `receipts/print` do stringa HTML
|
||||||
|
- Uzyj Dompdf do konwersji HTML → PDF
|
||||||
|
- Zwroc Response z Content-Type: application/pdf i Content-Disposition: attachment
|
||||||
|
- Nazwa pliku: receipt_number z / zamienione na _ + .pdf
|
||||||
|
|
||||||
|
3. **routes/web.php — 3 nowe trasy:**
|
||||||
|
- `GET /orders/{id}/receipt/{receiptId}` → `[$receiptController, 'show']`
|
||||||
|
- `GET /orders/{id}/receipt/{receiptId}/print` → `[$receiptController, 'printView']`
|
||||||
|
- `GET /orders/{id}/receipt/{receiptId}/pdf` → `[$receiptController, 'pdf']`
|
||||||
|
- Dodaj PRZED trasami shipment (analogicznie do create/store)
|
||||||
|
|
||||||
|
Wzorzec: Uzyj Template::render() z layoutem dla show, bez layoutu dla print.
|
||||||
|
Dla PDF: `$dompdf = new \Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4'); $dompdf->render();`
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `php -l src/Modules/Accounting/ReceiptController.php`
|
||||||
|
- `php -l routes/web.php`
|
||||||
|
- `composer show dompdf/dompdf` — zainstalowany
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 backend, AC-2 backend, AC-3 backend, AC-5 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Widoki receipts/show.php i receipts/print.php + SCSS</name>
|
||||||
|
<files>
|
||||||
|
resources/views/receipts/show.php,
|
||||||
|
resources/views/receipts/print.php,
|
||||||
|
resources/lang/pl.php,
|
||||||
|
resources/scss/shared/_ui-components.scss,
|
||||||
|
resources/scss/app.scss,
|
||||||
|
public/assets/css/app.css
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
1. **receipts/show.php** (w layoucie aplikacji):
|
||||||
|
- Naglowek: "Paragon {receipt_number}" + link powrotny do zamowienia
|
||||||
|
- Przyciski: "Drukuj" (target=_blank do /print), "Pobierz PDF" (link do /pdf), "Powrot"
|
||||||
|
- Sekcja danych sprzedawcy (z seller_data_json): firma, NIP, adres, tel, email
|
||||||
|
- Sekcja danych nabywcy (z buyer_data_json — jesli istnieje): nazwa, adres, NIP
|
||||||
|
- Tabela pozycji (z items_json): Lp, Nazwa, Ilosc, Cena, Wartosc
|
||||||
|
- Podsumowanie: suma brutto
|
||||||
|
- Metadane: data wystawienia, data sprzedazy, konfiguracja, nr referencyjny
|
||||||
|
- Uzyj klas .card, .section-title, .order-kv, .table — istniejace style
|
||||||
|
|
||||||
|
2. **receipts/print.php** (standalone, BEZ layoutu):
|
||||||
|
- Pelny dokument HTML z inline CSS (lub <link> do app.css)
|
||||||
|
- Uklad paragonowy: kompaktowy, A4-friendly
|
||||||
|
- Naglowek: dane sprzedawcy (lewo) + "PARAGON" + numer (prawo)
|
||||||
|
- Dane nabywcy (jesli is_named w konfiguracji)
|
||||||
|
- Tabela pozycji z podsumowaniem
|
||||||
|
- Stopka: data wystawienia, data sprzedazy, nr referencyjny
|
||||||
|
- Na koncu: `<script>window.print();</script>` — auto-print po zaladowaniu
|
||||||
|
- CSS @media print: ukryj elementy kontrolne, ustaw marginesy
|
||||||
|
|
||||||
|
3. **resources/lang/pl.php:**
|
||||||
|
- Dodaj klucze: `receipts.show.title`, `receipts.show.print`, `receipts.show.pdf`, `receipts.show.back`
|
||||||
|
- `receipts.show.seller`, `receipts.show.buyer`, `receipts.show.items`, `receipts.show.total`
|
||||||
|
- `receipts.show.issue_date`, `receipts.show.sale_date`, `receipts.show.reference`, `receipts.show.config`
|
||||||
|
|
||||||
|
4. **SCSS:**
|
||||||
|
- Dodaj klase `.receipt-print` dla widoku druku (kompaktowy, bez marginesow bocznych)
|
||||||
|
- Dodaj `.receipt-header` (flexbox: dane sprzedawcy | tytul+numer)
|
||||||
|
- Build SCSS do CSS
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `php -l resources/views/receipts/show.php`
|
||||||
|
- `php -l resources/views/receipts/print.php`
|
||||||
|
- Otworz /orders/{id}/receipt/{receiptId} — podglad wyswietla sie poprawnie
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-2 spelnione: widok podgladu i druku</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Aktywacja linku "Podglad" w zakladce Dokumenty</name>
|
||||||
|
<files>
|
||||||
|
resources/views/orders/show.php
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
W zakladce Dokumenty (data-order-tab-panel="documents"):
|
||||||
|
- Zamien `<span class="btn btn--sm btn--secondary btn--disabled">Podglad</span>` na:
|
||||||
|
`<a href="/orders/{orderId}/receipt/{receiptId}" class="btn btn--sm btn--secondary">Podglad</a>`
|
||||||
|
- Dodaj tez link "PDF": `<a href="/orders/{orderId}/receipt/{receiptId}/pdf" class="btn btn--sm btn--secondary">PDF</a>`
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Otworz zamowienie z paragonem → zakladka Dokumenty → link "Podglad" jest aktywny i prowadzi do podgladu
|
||||||
|
</verify>
|
||||||
|
<done>AC-4 spelnione: aktywny link Podglad + PDF w Dokumentach</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Podglad paragonu (HTML), widok druku (standalone + auto-print), generowanie PDF, aktywne linki w Dokumentach</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. Otworz zamowienie z paragonem → zakladka Dokumenty
|
||||||
|
2. Kliknij "Podglad" — otwiera sie strona z pelnym podgladem paragonu
|
||||||
|
3. Sprawdz: dane sprzedawcy, pozycje, suma, daty sa poprawne
|
||||||
|
4. Kliknij "Drukuj" — otwiera sie nowe okno z czystym widokiem + okno drukowania
|
||||||
|
5. Kliknij "Pobierz PDF" — przeglądarka pobiera plik PDF
|
||||||
|
6. Otworz PDF — tresc paragonu poprawna
|
||||||
|
7. Kliknij "PDF" bezposrednio z zakladki Dokumenty — pobiera PDF
|
||||||
|
8. Sprobuj otworzyc /orders/{id}/receipt/99999 — powinien byc 404
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- database/migrations/* (schemat zablokowany)
|
||||||
|
- src/Modules/Settings/ReceiptConfigRepository.php (gotowe z fazy 09)
|
||||||
|
- src/Modules/Accounting/ReceiptRepository.php (gotowe z fazy 10 — tylko odczyt)
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Brak edycji/anulowania paragonu — poza zakresem v0.3
|
||||||
|
- Brak logo firmy w paragonie — upload logo bedzie osobna funkcjonalnoscia
|
||||||
|
- Format paragonu jest staly (nie konfigurowalny per-config) — uproszczenie v0.3
|
||||||
|
- PDF generowany server-side przez dompdf (nie client-side)
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `composer show dompdf/dompdf` — zainstalowany
|
||||||
|
- [ ] `php -l src/Modules/Accounting/ReceiptController.php` — brak bledow
|
||||||
|
- [ ] `php -l resources/views/receipts/show.php` — brak bledow
|
||||||
|
- [ ] `php -l resources/views/receipts/print.php` — brak bledow
|
||||||
|
- [ ] Podglad /orders/{id}/receipt/{receiptId} wyswietla dane paragonu
|
||||||
|
- [ ] Druk otwiera czyste okno z window.print()
|
||||||
|
- [ ] PDF pobiera sie poprawnie
|
||||||
|
- [ ] Link "Podglad" w Dokumentach jest aktywny
|
||||||
|
- [ ] 404 dla nieistniejacego paragonu
|
||||||
|
- [ ] All acceptance criteria met
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Wszystkie 3 taski auto + 1 checkpoint ukonczone
|
||||||
|
- Wszystkie AC-1 do AC-5 spelnione
|
||||||
|
- dompdf zainstalowany i dzialajacy
|
||||||
|
- Brak bledow PHP
|
||||||
|
- Weryfikacja manualna przez uzytkownika (checkpoint)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/11-receipt-print/11-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
141
.paul/phases/11-receipt-print/11-01-SUMMARY.md
Normal file
141
.paul/phases/11-receipt-print/11-01-SUMMARY.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
phase: 11-receipt-print
|
||||||
|
plan: 01
|
||||||
|
subsystem: accounting
|
||||||
|
tags: [dompdf, pdf, receipt, print, view]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 10-receipt-issue
|
||||||
|
provides: ReceiptRepository, receipts table, snapshots JSON
|
||||||
|
provides:
|
||||||
|
- Receipt preview (HTML in app layout)
|
||||||
|
- Receipt print view (standalone HTML + auto window.print())
|
||||||
|
- Receipt PDF generation via dompdf
|
||||||
|
- Active preview/PDF links in Documents tab
|
||||||
|
affects: [12-receipt-corrections]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: [dompdf/dompdf ^3.1]
|
||||||
|
patterns: [buildReceiptViewData shared helper, standalone print view pattern]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- resources/views/receipts/show.php
|
||||||
|
- resources/views/receipts/print.php
|
||||||
|
modified:
|
||||||
|
- src/Modules/Accounting/ReceiptController.php
|
||||||
|
- routes/web.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- resources/scss/shared/_ui-components.scss
|
||||||
|
- public/assets/css/app.css
|
||||||
|
- composer.json
|
||||||
|
- .vscode/ftp-kr.json
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "dompdf v3.1 server-side PDF generation (not client-side)"
|
||||||
|
- "DejaVu Sans font for PDF Polish character support"
|
||||||
|
- "ftp-kr vendor/ no longer fully ignored — changed to /vendor/bin only"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "buildReceiptViewData() shared helper for show/print/pdf reuse"
|
||||||
|
- "Standalone print view pattern: full HTML without layout + window.print()"
|
||||||
|
|
||||||
|
duration: ~30min
|
||||||
|
completed: 2026-03-15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 11 Plan 01: Receipt Preview & Print Summary
|
||||||
|
|
||||||
|
**Podglad paragonu HTML, widok druku standalone i generowanie PDF przez dompdf — zamyka cykl wystawienie-podglad-wydruk.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~30min |
|
||||||
|
| Completed | 2026-03-15 |
|
||||||
|
| Tasks | 4 completed (3 auto + 1 checkpoint) |
|
||||||
|
| Files modified | 10 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Podglad paragonu w layoucie aplikacji | Pass | /orders/{id}/receipt/{receiptId} — dane sprzedawcy, nabywcy, pozycje, suma, daty |
|
||||||
|
| AC-2: Widok druku (standalone HTML) | Pass | /print endpoint, window.print() auto-trigger |
|
||||||
|
| AC-3: Generowanie PDF | Pass | /pdf endpoint, dompdf v3.1, Content-Disposition attachment |
|
||||||
|
| AC-4: Aktywny link Podglad w Dokumentach | Pass | span.btn--disabled zamieniony na a.btn + dodany link PDF |
|
||||||
|
| AC-5: Obsluga bledu — nieistniejacy paragon | Pass | 404 gdy paragon nie istnieje lub order_id mismatch |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- 3 nowe metody kontrolera: show(), printView(), pdf() + helper buildReceiptViewData()
|
||||||
|
- 2 nowe widoki: receipts/show.php (w layoucie) i receipts/print.php (standalone)
|
||||||
|
- dompdf zainstalowany i dzialajacy na serwerze
|
||||||
|
- Aktywne linki Podglad + PDF w zakladce Dokumenty zamowienia
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Accounting/ReceiptController.php` | Modified | +show(), +printView(), +pdf(), +buildReceiptViewData() |
|
||||||
|
| `routes/web.php` | Modified | +3 trasy GET: show, print, pdf |
|
||||||
|
| `resources/views/receipts/show.php` | Created | Podglad paragonu w layoucie aplikacji |
|
||||||
|
| `resources/views/receipts/print.php` | Created | Standalone widok druku + auto window.print() |
|
||||||
|
| `resources/views/orders/show.php` | Modified | Aktywne linki Podglad + PDF w Dokumentach |
|
||||||
|
| `resources/lang/pl.php` | Modified | +receipts.show.* (13 kluczy) |
|
||||||
|
| `resources/scss/shared/_ui-components.scss` | Modified | +.receipt-header, +.receipt-print |
|
||||||
|
| `public/assets/css/app.css` | Modified | Przebudowany z nowym SCSS |
|
||||||
|
| `composer.json` | Modified | +dompdf/dompdf ^3.1 |
|
||||||
|
| `.vscode/ftp-kr.json` | Modified | /vendor → /vendor/bin w ignore |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| dompdf server-side | Plan wymaga server-side PDF, dompdf standard PHP | Wymaga vendor/ na serwerze |
|
||||||
|
| ftp-kr vendor/ nie ignorowany | dompdf musi byc na serwerze, reczny deploy uciazliwy | Automatyczny upload vendor/ przy zmianach |
|
||||||
|
| DejaVu Sans w print CSS | Obsluga polskich znakow w PDF | Poprawne znaki w generowanym PDF |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | ftp-kr config change for vendor deploy |
|
||||||
|
| Deferred | 0 | — |
|
||||||
|
|
||||||
|
**Total impact:** Minimalne — jedyna zmiana to konfiguracja deploy.
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Deploy] vendor/ nie uploadowany na serwer**
|
||||||
|
- **Found during:** Task 1 (instalacja dompdf)
|
||||||
|
- **Issue:** ftp-kr ignorowal caly /vendor, dompdf nie trafialo na serwer
|
||||||
|
- **Fix:** Zmiana /vendor na /vendor/bin w ftp-kr.json ignore
|
||||||
|
- **Files:** .vscode/ftp-kr.json
|
||||||
|
- **Verification:** Upload vendor/ → dompdf dziala na serwerze
|
||||||
|
|
||||||
|
### Skill Audit
|
||||||
|
|
||||||
|
| Expected | Invoked | Notes |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Pelny cykl paragonow: konfiguracja → wystawienie → podglad → druk → PDF
|
||||||
|
- Faza 11 zamknieta, gotowe do fazy 12
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- sonar-scanner nie uruchomiony (gap z wielu faz)
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- Brak
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 11-receipt-print, Plan: 01*
|
||||||
|
*Completed: 2026-03-15*
|
||||||
2
.vscode/ftp-kr.json
vendored
2
.vscode/ftp-kr.json
vendored
@@ -17,7 +17,7 @@
|
|||||||
".gitignore",
|
".gitignore",
|
||||||
"/.scannerwork",
|
"/.scannerwork",
|
||||||
"/.paul",
|
"/.paul",
|
||||||
"/vendor",
|
"/vendor/bin",
|
||||||
"/node_modules",
|
"/node_modules",
|
||||||
"/composer.phar",
|
"/composer.phar",
|
||||||
"/composer.lock",
|
"/composer.lock",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"type": "project",
|
"type": "project",
|
||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4"
|
"php": "^8.4",
|
||||||
|
"dompdf/dompdf": "^3.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^11.5",
|
"phpunit/phpunit": "^11.5",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1234,6 +1234,20 @@ return [
|
|||||||
'no_config_selected' => 'Wybierz konfiguracje paragonu',
|
'no_config_selected' => 'Wybierz konfiguracje paragonu',
|
||||||
'invalid_config' => 'Nieprawidlowa konfiguracja paragonu',
|
'invalid_config' => 'Nieprawidlowa konfiguracja paragonu',
|
||||||
],
|
],
|
||||||
|
'show' => [
|
||||||
|
'title' => 'Paragon',
|
||||||
|
'print' => 'Drukuj',
|
||||||
|
'pdf' => 'Pobierz PDF',
|
||||||
|
'back' => 'Powrot do zamowienia',
|
||||||
|
'seller' => 'Dane sprzedawcy',
|
||||||
|
'buyer' => 'Dane nabywcy',
|
||||||
|
'items' => 'Pozycje',
|
||||||
|
'total' => 'Razem brutto',
|
||||||
|
'issue_date' => 'Data wystawienia',
|
||||||
|
'sale_date' => 'Data sprzedazy',
|
||||||
|
'config' => 'Konfiguracja',
|
||||||
|
'reference' => 'Nr referencyjny zamowienia',
|
||||||
|
],
|
||||||
'documents' => [
|
'documents' => [
|
||||||
'number' => 'Numer',
|
'number' => 'Numer',
|
||||||
'issue_date' => 'Data wystawienia',
|
'issue_date' => 'Data wystawienia',
|
||||||
|
|||||||
@@ -244,3 +244,41 @@
|
|||||||
color: var(--c-primary);
|
color: var(--c-primary);
|
||||||
background: #edf2ff;
|
background: #edf2ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receipt-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid var(--c-text-strong);
|
||||||
|
|
||||||
|
&__seller {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 14px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-print {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -545,7 +545,8 @@ foreach ($addressesList as $address) {
|
|||||||
<td class="text-nowrap"><?= $e($receipt['total_gross'] !== null ? number_format((float) $receipt['total_gross'], 2, '.', ' ') : '-') ?></td>
|
<td class="text-nowrap"><?= $e($receipt['total_gross'] !== null ? number_format((float) $receipt['total_gross'], 2, '.', ' ') : '-') ?></td>
|
||||||
<td><?= $e((string) ($receipt['config_name'] ?? '-')) ?></td>
|
<td><?= $e((string) ($receipt['config_name'] ?? '-')) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="btn btn--sm btn--secondary btn--disabled"><?= $e($t('receipts.documents.preview')) ?></span>
|
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/<?= $e((string) ($receipt['id'] ?? '')) ?>" class="btn btn--sm btn--secondary"><?= $e($t('receipts.documents.preview')) ?></a>
|
||||||
|
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/receipt/<?= $e((string) ($receipt['id'] ?? '')) ?>/pdf" class="btn btn--sm btn--secondary">PDF</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
115
resources/views/receipts/print.php
Normal file
115
resources/views/receipts/print.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Paragon <?= $e((string) ($receipt['receipt_number'] ?? '')) ?></title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: "DejaVu Sans", "Segoe UI", Arial, sans-serif; font-size: 12px; color: #1a1a1a; padding: 20mm 15mm; }
|
||||||
|
.receipt-print { max-width: 700px; margin: 0 auto; }
|
||||||
|
.receipt-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 2px solid #333; }
|
||||||
|
.receipt-header__seller { flex: 1; }
|
||||||
|
.receipt-header__seller strong { font-size: 14px; display: block; margin-bottom: 4px; }
|
||||||
|
.receipt-header__title { text-align: right; }
|
||||||
|
.receipt-header__title h1 { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
|
||||||
|
.receipt-header__title .receipt-number { font-size: 14px; font-weight: 600; }
|
||||||
|
.receipt-buyer { margin-bottom: 16px; padding: 8px 12px; border: 1px solid #ccc; }
|
||||||
|
.receipt-buyer strong { display: block; margin-bottom: 4px; font-size: 11px; text-transform: uppercase; color: #666; }
|
||||||
|
.receipt-items { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
||||||
|
.receipt-items th { background: #f0f0f0; text-align: left; padding: 6px 8px; font-size: 11px; border-bottom: 1px solid #999; }
|
||||||
|
.receipt-items td { padding: 5px 8px; border-bottom: 1px solid #ddd; }
|
||||||
|
.receipt-items .text-right { text-align: right; }
|
||||||
|
.receipt-items .text-nowrap { white-space: nowrap; }
|
||||||
|
.receipt-items tfoot td { border-top: 2px solid #333; font-weight: 700; padding-top: 8px; }
|
||||||
|
.receipt-meta { margin-top: 16px; font-size: 11px; color: #555; }
|
||||||
|
.receipt-meta dt { display: inline; font-weight: 600; }
|
||||||
|
.receipt-meta dd { display: inline; margin: 0 16px 0 4px; }
|
||||||
|
@media print {
|
||||||
|
body { padding: 10mm; }
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php
|
||||||
|
$receiptData = is_array($receipt ?? null) ? $receipt : [];
|
||||||
|
$sellerData = is_array($seller ?? null) ? $seller : [];
|
||||||
|
$buyerData = is_array($buyer ?? null) ? $buyer : null;
|
||||||
|
$itemsList = is_array($items ?? null) ? $items : [];
|
||||||
|
$totalGross = (float) ($receiptData['total_gross'] ?? 0);
|
||||||
|
?>
|
||||||
|
<div class="receipt-print">
|
||||||
|
<div class="receipt-header">
|
||||||
|
<div class="receipt-header__seller">
|
||||||
|
<strong><?= $e((string) ($sellerData['company_name'] ?? '')) ?></strong>
|
||||||
|
<?php if (($sellerData['tax_number'] ?? '') !== ''): ?>
|
||||||
|
<div>NIP: <?= $e((string) $sellerData['tax_number']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div><?= $e((string) ($sellerData['street'] ?? '')) ?></div>
|
||||||
|
<div><?= $e((string) ($sellerData['postal_code'] ?? '')) ?> <?= $e((string) ($sellerData['city'] ?? '')) ?></div>
|
||||||
|
<?php if (($sellerData['phone'] ?? '') !== ''): ?>
|
||||||
|
<div>Tel: <?= $e((string) $sellerData['phone']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="receipt-header__title">
|
||||||
|
<h1>PARAGON</h1>
|
||||||
|
<div class="receipt-number"><?= $e((string) ($receiptData['receipt_number'] ?? '')) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($buyerData !== null): ?>
|
||||||
|
<div class="receipt-buyer">
|
||||||
|
<strong>Nabywca</strong>
|
||||||
|
<?php if (($buyerData['company_name'] ?? '') !== ''): ?>
|
||||||
|
<div><?= $e((string) $buyerData['company_name']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (($buyerData['name'] ?? '') !== ''): ?>
|
||||||
|
<div><?= $e((string) $buyerData['name']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (($buyerData['tax_number'] ?? '') !== ''): ?>
|
||||||
|
<div>NIP: <?= $e((string) $buyerData['tax_number']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div><?= $e((string) ($buyerData['street'] ?? '')) ?>, <?= $e((string) ($buyerData['postal_code'] ?? '')) ?> <?= $e((string) ($buyerData['city'] ?? '')) ?></div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<table class="receipt-items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lp.</th>
|
||||||
|
<th>Nazwa</th>
|
||||||
|
<th class="text-right">Ilosc</th>
|
||||||
|
<th class="text-right">Cena</th>
|
||||||
|
<th class="text-right">Wartosc</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($itemsList as $idx => $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e((string) ($idx + 1)) ?></td>
|
||||||
|
<td><?= $e((string) ($item['name'] ?? '')) ?></td>
|
||||||
|
<td class="text-right text-nowrap"><?= $e((string) ($item['quantity'] ?? 0)) ?></td>
|
||||||
|
<td class="text-right text-nowrap"><?= $e(number_format((float) ($item['price'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
<td class="text-right text-nowrap"><?= $e(number_format((float) ($item['total'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-right">Razem brutto</td>
|
||||||
|
<td class="text-right text-nowrap"><?= $e(number_format($totalGross, 2, '.', ' ')) ?> PLN</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<dl class="receipt-meta">
|
||||||
|
<dt>Data wystawienia:</dt><dd><?= $e((string) ($receiptData['issue_date'] ?? '-')) ?></dd>
|
||||||
|
<dt>Data sprzedazy:</dt><dd><?= $e((string) ($receiptData['sale_date'] ?? '-')) ?></dd>
|
||||||
|
<?php if (($receiptData['order_reference_value'] ?? null) !== null): ?>
|
||||||
|
<dt>Nr referencyjny:</dt><dd><?= $e((string) $receiptData['order_reference_value']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<script>window.print();</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
103
resources/views/receipts/show.php
Normal file
103
resources/views/receipts/show.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
$receiptData = is_array($receipt ?? null) ? $receipt : [];
|
||||||
|
$sellerData = is_array($seller ?? null) ? $seller : [];
|
||||||
|
$buyerData = is_array($buyer ?? null) ? $buyer : null;
|
||||||
|
$itemsList = is_array($items ?? null) ? $items : [];
|
||||||
|
$orderIdVal = (int) ($orderId ?? 0);
|
||||||
|
$configNameVal = (string) ($configName ?? '');
|
||||||
|
$receiptNumber = (string) ($receiptData['receipt_number'] ?? '');
|
||||||
|
$totalGross = (float) ($receiptData['total_gross'] ?? 0);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="order-details-head">
|
||||||
|
<div>
|
||||||
|
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="order-back-link">← <?= $e($t('receipts.show.back')) ?></a>
|
||||||
|
<h2 class="section-title mt-12"><?= $e($t('receipts.show.title')) ?> <?= $e($receiptNumber) ?></h2>
|
||||||
|
</div>
|
||||||
|
<div class="order-details-head__actions">
|
||||||
|
<a href="/orders/<?= $e((string) $orderIdVal) ?>/receipt/<?= $e((string) ($receiptData['id'] ?? '')) ?>/print" target="_blank" class="btn btn--secondary"><?= $e($t('receipts.show.print')) ?></a>
|
||||||
|
<a href="/orders/<?= $e((string) $orderIdVal) ?>/receipt/<?= $e((string) ($receiptData['id'] ?? '')) ?>/pdf" class="btn btn--primary"><?= $e($t('receipts.show.pdf')) ?></a>
|
||||||
|
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary"><?= $e($t('receipts.show.back')) ?></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid-2 mt-16">
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title"><?= $e($t('receipts.show.seller')) ?></h3>
|
||||||
|
<dl class="order-kv mt-8">
|
||||||
|
<dt>Firma</dt><dd><?= $e((string) ($sellerData['company_name'] ?? '-')) ?></dd>
|
||||||
|
<dt>NIP</dt><dd><?= $e((string) ($sellerData['tax_number'] ?? '-')) ?></dd>
|
||||||
|
<dt>Adres</dt><dd><?= $e((string) ($sellerData['street'] ?? '')) ?>, <?= $e((string) ($sellerData['postal_code'] ?? '')) ?> <?= $e((string) ($sellerData['city'] ?? '')) ?></dd>
|
||||||
|
<dt>Telefon</dt><dd><?= $e((string) ($sellerData['phone'] ?? '-')) ?></dd>
|
||||||
|
<dt>Email</dt><dd><?= $e((string) ($sellerData['email'] ?? '-')) ?></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<?php if ($buyerData !== null): ?>
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title"><?= $e($t('receipts.show.buyer')) ?></h3>
|
||||||
|
<dl class="order-kv mt-8">
|
||||||
|
<?php if (($buyerData['company_name'] ?? '') !== ''): ?>
|
||||||
|
<dt>Firma</dt><dd><?= $e((string) $buyerData['company_name']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (($buyerData['name'] ?? '') !== ''): ?>
|
||||||
|
<dt>Nazwa</dt><dd><?= $e((string) $buyerData['name']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (($buyerData['tax_number'] ?? '') !== ''): ?>
|
||||||
|
<dt>NIP</dt><dd><?= $e((string) $buyerData['tax_number']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<dt>Adres</dt><dd><?= $e((string) ($buyerData['street'] ?? '')) ?>, <?= $e((string) ($buyerData['postal_code'] ?? '')) ?> <?= $e((string) ($buyerData['city'] ?? '')) ?></dd>
|
||||||
|
<?php if (($buyerData['phone'] ?? '') !== ''): ?>
|
||||||
|
<dt>Telefon</dt><dd><?= $e((string) $buyerData['phone']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (($buyerData['email'] ?? '') !== ''): ?>
|
||||||
|
<dt>Email</dt><dd><?= $e((string) $buyerData['email']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="section-title mt-16"><?= $e($t('receipts.show.items')) ?></h3>
|
||||||
|
<div class="table-wrap mt-8">
|
||||||
|
<table class="table table--details">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Lp.</th>
|
||||||
|
<th>Nazwa</th>
|
||||||
|
<th>Ilosc</th>
|
||||||
|
<th>Cena</th>
|
||||||
|
<th>Wartosc</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($itemsList as $idx => $item): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $e((string) ($idx + 1)) ?></td>
|
||||||
|
<td><?= $e((string) ($item['name'] ?? '')) ?></td>
|
||||||
|
<td><?= $e((string) ($item['quantity'] ?? 0)) ?></td>
|
||||||
|
<td class="text-nowrap"><?= $e(number_format((float) ($item['price'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
<td class="text-nowrap"><?= $e(number_format((float) ($item['total'] ?? 0), 2, '.', ' ')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-right"><strong><?= $e($t('receipts.show.total')) ?></strong></td>
|
||||||
|
<td><strong><?= $e(number_format($totalGross, 2, '.', ' ')) ?> PLN</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid-2 mt-16">
|
||||||
|
<dl class="order-kv">
|
||||||
|
<dt><?= $e($t('receipts.show.issue_date')) ?></dt><dd><?= $e((string) ($receiptData['issue_date'] ?? '-')) ?></dd>
|
||||||
|
<dt><?= $e($t('receipts.show.sale_date')) ?></dt><dd><?= $e((string) ($receiptData['sale_date'] ?? '-')) ?></dd>
|
||||||
|
<dt><?= $e($t('receipts.show.config')) ?></dt><dd><?= $e($configNameVal !== '' ? $configNameVal : '-') ?></dd>
|
||||||
|
<?php if (($receiptData['order_reference_value'] ?? null) !== null): ?>
|
||||||
|
<dt><?= $e($t('receipts.show.reference')) ?></dt><dd><?= $e((string) $receiptData['order_reference_value']) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -301,6 +301,9 @@ return static function (Application $app): void {
|
|||||||
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
||||||
$router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]);
|
$router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/receipt/store', [$receiptController, 'store'], [$authMiddleware]);
|
$router->post('/orders/{id}/receipt/store', [$receiptController, 'store'], [$authMiddleware]);
|
||||||
|
$router->get('/orders/{id}/receipt/{receiptId}', [$receiptController, 'show'], [$authMiddleware]);
|
||||||
|
$router->get('/orders/{id}/receipt/{receiptId}/print', [$receiptController, 'printView'], [$authMiddleware]);
|
||||||
|
$router->get('/orders/{id}/receipt/{receiptId}/pdf', [$receiptController, 'pdf'], [$authMiddleware]);
|
||||||
$router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]);
|
$router->get('/orders/{id}/shipment/prepare', [$shipmentController, 'prepare'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/shipment/create', [$shipmentController, 'create'], [$authMiddleware]);
|
$router->post('/orders/{id}/shipment/create', [$shipmentController, 'create'], [$authMiddleware]);
|
||||||
$router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]);
|
$router->get('/orders/{id}/shipment/{packageId}/status', [$shipmentController, 'checkStatus'], [$authMiddleware]);
|
||||||
|
|||||||
@@ -260,4 +260,95 @@ final class ReceiptController
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$receiptId = max(0, (int) $request->input('receiptId', 0));
|
||||||
|
|
||||||
|
$receipt = $this->receipts->findById($receiptId);
|
||||||
|
if ($receipt === null || (int) ($receipt['order_id'] ?? 0) !== $orderId) {
|
||||||
|
return Response::html('Not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->buildReceiptViewData($receipt, $orderId);
|
||||||
|
|
||||||
|
$html = $this->template->render('receipts/show', $data, 'layouts/app');
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function printView(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$receiptId = max(0, (int) $request->input('receiptId', 0));
|
||||||
|
|
||||||
|
$receipt = $this->receipts->findById($receiptId);
|
||||||
|
if ($receipt === null || (int) ($receipt['order_id'] ?? 0) !== $orderId) {
|
||||||
|
return Response::html('Not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->buildReceiptViewData($receipt, $orderId);
|
||||||
|
|
||||||
|
$html = $this->template->render('receipts/print', $data);
|
||||||
|
return Response::html($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pdf(Request $request): Response
|
||||||
|
{
|
||||||
|
$orderId = max(0, (int) $request->input('id', 0));
|
||||||
|
$receiptId = max(0, (int) $request->input('receiptId', 0));
|
||||||
|
|
||||||
|
$receipt = $this->receipts->findById($receiptId);
|
||||||
|
if ($receipt === null || (int) ($receipt['order_id'] ?? 0) !== $orderId) {
|
||||||
|
return Response::html('Not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->buildReceiptViewData($receipt, $orderId);
|
||||||
|
|
||||||
|
$html = $this->template->render('receipts/print', $data);
|
||||||
|
|
||||||
|
$dompdf = new \Dompdf\Dompdf();
|
||||||
|
$dompdf->loadHtml($html);
|
||||||
|
$dompdf->setPaper('A4');
|
||||||
|
$dompdf->render();
|
||||||
|
|
||||||
|
$filename = str_replace(['/', '\\'], '_', (string) ($receipt['receipt_number'] ?? 'receipt')) . '.pdf';
|
||||||
|
|
||||||
|
return new Response($dompdf->output() ?: '', 200, [
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $receipt
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildReceiptViewData(array $receipt, int $orderId): array
|
||||||
|
{
|
||||||
|
$seller = json_decode((string) ($receipt['seller_data_json'] ?? '{}'), true);
|
||||||
|
$buyer = ($receipt['buyer_data_json'] ?? null) !== null
|
||||||
|
? json_decode((string) $receipt['buyer_data_json'], true)
|
||||||
|
: null;
|
||||||
|
$items = json_decode((string) ($receipt['items_json'] ?? '[]'), true);
|
||||||
|
|
||||||
|
$configName = '';
|
||||||
|
$config = $this->receiptConfigs->findById((int) ($receipt['config_id'] ?? 0));
|
||||||
|
if ($config !== null) {
|
||||||
|
$configName = (string) ($config['name'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $this->translator->get('receipts.show.title') . ' ' . ($receipt['receipt_number'] ?? ''),
|
||||||
|
'activeMenu' => 'orders',
|
||||||
|
'activeOrders' => 'list',
|
||||||
|
'user' => $this->auth->user(),
|
||||||
|
'orderId' => $orderId,
|
||||||
|
'receipt' => $receipt,
|
||||||
|
'seller' => is_array($seller) ? $seller : [],
|
||||||
|
'buyer' => is_array($buyer) ? $buyer : null,
|
||||||
|
'items' => is_array($items) ? $items : [],
|
||||||
|
'configName' => $configName,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user