feat(11-12-accounting): phases 11-12 complete — milestone v0.3 done
Phase 11: Receipt preview, print & PDF via dompdf. Phase 12: Accounting section with receipt list, filters, pagination, selectable checkboxes and XLSX export via PhpSpreadsheet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ Moduł księgowości z obsługą paragonów: wielokonfiguracyjne szablony, wysta
|
||||
| 9 | Konfiguracja paragonów (Ustawienia) | 1/1 | Complete ✓ |
|
||||
| 10 | Wystawianie paragonów z zamówienia | 1/1 | Complete ✓ |
|
||||
| 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/1 | Complete ✓ |
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
|
||||
@@ -5,31 +5,32 @@
|
||||
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.
|
||||
**Current focus:** Milestone v0.3 Moduł Paragonów — Faza 11 COMPLETE, transition do fazy 12.
|
||||
**Current focus:** Milestone v0.3 Moduł Paragonów — COMPLETE. Transition + milestone completion.
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.3 Moduł Paragonów
|
||||
Phase: 11 of 12 (11-receipt-print) — Complete
|
||||
Plan: 11-01 complete
|
||||
Status: Loop closed, phase transition required
|
||||
Last activity: 2026-03-15 — Phase 11 complete (podgląd, druk, PDF paragonu)
|
||||
Milestone: v0.3 Moduł Paragonów — COMPLETE
|
||||
Phase: 12 of 12 (12-accounting-list) — Complete
|
||||
Plan: 12-01 complete
|
||||
Status: Milestone v0.3 complete, transition required
|
||||
Last activity: 2026-03-15 — Phase 12 complete (lista paragonów + eksport XLSX)
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [██████████] 100% ✓
|
||||
- v0.2 Pre-Expansion Fixes: [██████████] 100% ✓
|
||||
- v0.3 Moduł Paragonów: [████████░░] 80%
|
||||
- v0.3 Moduł Paragonów: [██████████] 100% ✓
|
||||
- Phase 8: [██████████] 100% ✓
|
||||
- Phase 9: [██████████] 100% ✓
|
||||
- Phase 10: [██████████] 100% ✓
|
||||
- Phase 11: [██████████] 100% ✓
|
||||
- Phase 12: [██████████] 100% ✓
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Loop complete — phase transition required]
|
||||
✓ ✓ ✓ [Loop complete — milestone v0.3 COMPLETE]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
@@ -52,6 +53,13 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
| 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 |
|
||||
| 2026-03-15 | PhpSpreadsheet v5.5 dla eksportu XLSX | Faza 12 | Nowa zależność composer; XLSX lepszy od CSV dla księgowości |
|
||||
| 2026-03-15 | POST eksport z CSRF + dwa tryby (zaznaczone/wszystkie z filtra) | Faza 12 | Bezpieczny eksport; selectable table-list reuse |
|
||||
|
||||
### Skill Audit (Faza 12, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY |
|
||||
|
||||
### Skill Audit (Faza 11, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
@@ -132,7 +140,7 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
- **Delivery mapping "Szukaj..." layout** — JS `attachSelectFilter()` w allegro.php tworzy input search dla InPost/Apaczka selectów, wizualnie wygląda jakby należał do wiersza powyżej. Pre-existing bug, do naprawy osobno.
|
||||
|
||||
### Git State
|
||||
Last commit: ed057fc (feat(08-10-receipt-module): phases 08-10 complete)
|
||||
Last commit: fb60b6d (feat(11-receipt-print): phase 11 complete — receipt preview, print & PDF)
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
@@ -142,13 +150,13 @@ Brak.
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-15
|
||||
Stopped at: Phase 11 complete, transition required
|
||||
Next action: Phase transition — git commit, ROADMAP update, then /paul:plan for phase 12
|
||||
Resume file: .paul/phases/11-receipt-print/11-01-SUMMARY.md
|
||||
Stopped at: Milestone v0.3 COMPLETE — transition required
|
||||
Next action: Git commit for phase 12, then /paul:complete-milestone v0.3
|
||||
Resume file: .paul/phases/12-accounting-list/12-01-SUMMARY.md
|
||||
Resume context:
|
||||
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
||||
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
||||
- v0.3: IN PROGRESS — Phase 08-11 done, Phase 12 next
|
||||
- v0.3: COMPLETE ✓ (5 phases, 5 plans) — Moduł Paragonów
|
||||
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
||||
|
||||
---
|
||||
|
||||
300
.paul/phases/12-accounting-list/12-01-PLAN.md
Normal file
300
.paul/phases/12-accounting-list/12-01-PLAN.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
phase: 12-accounting-list
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["11-01"]
|
||||
files_modified:
|
||||
- src/Modules/Accounting/AccountingController.php
|
||||
- src/Modules/Accounting/ReceiptRepository.php
|
||||
- resources/views/accounting/index.php
|
||||
- resources/views/layouts/app.php
|
||||
- routes/web.php
|
||||
- resources/lang/pl.php
|
||||
- resources/scss/app.scss
|
||||
- public/assets/css/app.css
|
||||
- composer.json
|
||||
- DOCS/ARCHITECTURE.md
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Sekcja Ksiegowosc w nawigacji glownej z lista wszystkich paragonow, filtrami, paginacja i eksportem do XLSX.
|
||||
|
||||
## Purpose
|
||||
Dotychczas paragony widoczne sa tylko w kontekscie pojedynczego zamowienia. Uzytkownik potrzebuje przegladac wszystkie paragony w jednym miejscu — filtrowac po dacie, konfiguracji, numerze — i eksportowac do XLSX dla celów ksiegowych.
|
||||
|
||||
## Output
|
||||
- Nowy AccountingController z metoda index() i export()
|
||||
- Widok listy paragonow z filtrami (reuse komponentu table-list)
|
||||
- Eksport XLSX przez PhpSpreadsheet
|
||||
- Nowy link "Ksiegowosc" w nawigacji glownej (sidebar)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/11-receipt-print/11-01-SUMMARY.md — podglad, druk, PDF paragonu
|
||||
@.paul/phases/10-receipt-issue/10-01-SUMMARY.md — wystawianie paragonow, ReceiptRepository
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Accounting/ReceiptRepository.php
|
||||
@src/Modules/Accounting/ReceiptController.php
|
||||
@resources/views/components/table-list.php
|
||||
@resources/views/orders/list.php — wzorzec listy z filtrami
|
||||
@src/Modules/Orders/OrdersController.php — wzorzec metody index() z tableList
|
||||
@resources/views/layouts/app.php — sidebar navigation
|
||||
</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: Link Ksiegowosc w nawigacji glownej
|
||||
```gherkin
|
||||
Given uzytkownik jest zalogowany
|
||||
When widzi sidebar nawigacji
|
||||
Then widoczny jest nowy link "Ksiegowosc" prowadzacy do /accounting
|
||||
And link jest aktywny gdy uzytkownik jest na stronie /accounting
|
||||
```
|
||||
|
||||
## AC-2: Lista paragonow z paginacja
|
||||
```gherkin
|
||||
Given istnieja paragony w bazie danych
|
||||
When uzytkownik otwiera /accounting
|
||||
Then wyswietlana jest tabela paragonow z kolumnami:
|
||||
- Numer paragonu, Data wystawienia, Data sprzedazy, Kwota brutto, Konfiguracja, Zamowienie
|
||||
And tabela jest paginowana (domyslnie 20 na strone)
|
||||
And mozna sortowac po: numer, data wystawienia, kwota brutto
|
||||
```
|
||||
|
||||
## AC-3: Filtry listy
|
||||
```gherkin
|
||||
Given uzytkownik jest na stronie /accounting
|
||||
When uzywa filtrow
|
||||
Then moze filtrowac po:
|
||||
- Szukaj (numer paragonu, numer zamowienia)
|
||||
- Konfiguracja (select z aktywnych konfiguracji)
|
||||
- Data wystawienia od / do
|
||||
And filtry sa zachowane w URL (query string)
|
||||
```
|
||||
|
||||
## AC-4: Eksport XLSX
|
||||
```gherkin
|
||||
Given uzytkownik jest na stronie /accounting z zastosowanymi filtrami
|
||||
When klika przycisk "Eksportuj XLSX"
|
||||
Then przeglądarka pobiera plik .xlsx z lista paragonow
|
||||
And plik zawiera te same kolumny co tabela + dodatkowe: data sprzedazy, nr referencyjny
|
||||
And eksport uwzglednia aktywne filtry (nie eksportuje wszystkiego)
|
||||
And plik ma nazwe: paragony_YYYY-MM-DD.xlsx
|
||||
```
|
||||
|
||||
## AC-5: Pusta lista
|
||||
```gherkin
|
||||
Given brak paragonow spelniajacych kryteria (pusta baza lub filtr bez wynikow)
|
||||
When uzytkownik otwiera /accounting
|
||||
Then wyswietlany jest komunikat "Brak paragonow"
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: ReceiptRepository — metoda paginate() i exportData()</name>
|
||||
<files>
|
||||
src/Modules/Accounting/ReceiptRepository.php
|
||||
</files>
|
||||
<action>
|
||||
1. **paginate(array $filters): array** — analogicznie do OrdersRepository::paginate()
|
||||
- Przyjmuje filtry: search, config_id, date_from, date_to, sort, sort_dir, page, per_page
|
||||
- SELECT r.*, rc.name AS config_name, o.internal_order_number, o.external_order_id
|
||||
FROM receipts r
|
||||
LEFT JOIN receipt_configs rc ON rc.id = r.config_id
|
||||
LEFT JOIN orders o ON o.id = r.order_id
|
||||
- WHERE dynamiczne na podstawie filtrow:
|
||||
- search: LIKE na receipt_number i o.internal_order_number i o.external_order_id
|
||||
- config_id: = config_id
|
||||
- date_from: issue_date >= date_from
|
||||
- date_to: issue_date <= date_to
|
||||
- ORDER BY dynamiczne (whitelist: receipt_number, issue_date, total_gross) + sort_dir (ASC/DESC)
|
||||
- LIMIT/OFFSET na podstawie page i per_page
|
||||
- Zwraca: ['items' => [...], 'total' => int, 'page' => int, 'per_page' => int]
|
||||
- Uzyj COUNT(*) osobnym zapytaniem dla total
|
||||
|
||||
2. **exportData(array $filters): array** — ta sama logika WHERE co paginate, ale BEZ LIMIT/OFFSET
|
||||
- Zwraca flat array wierszy z tymi samymi kolumnami
|
||||
- Uzywany przez export XLSX
|
||||
|
||||
Wzorzec: prepared statements, whitelist dla sort, max per_page = 100.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l src/Modules/Accounting/ReceiptRepository.php`
|
||||
</verify>
|
||||
<done>AC-2 backend, AC-3 backend, AC-4 backend</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: AccountingController + trasy + nawigacja + widok</name>
|
||||
<files>
|
||||
src/Modules/Accounting/AccountingController.php,
|
||||
routes/web.php,
|
||||
resources/views/accounting/index.php,
|
||||
resources/views/layouts/app.php,
|
||||
resources/lang/pl.php,
|
||||
composer.json
|
||||
</files>
|
||||
<action>
|
||||
1. **Instalacja PhpSpreadsheet:**
|
||||
- `php composer.phar require phpoffice/phpspreadsheet --ignore-platform-reqs`
|
||||
|
||||
2. **AccountingController** (nowy plik w src/Modules/Accounting/):
|
||||
- Konstruktor: Template, Translator, AuthService, ReceiptRepository, ReceiptConfigRepository
|
||||
- **index(Request): Response** — GET /accounting
|
||||
- Parsuj filtry z request (search, config_id, date_from, date_to, sort, sort_dir, page, per_page)
|
||||
- Wywolaj ReceiptRepository::paginate($filters)
|
||||
- Pobierz liste aktywnych konfiguracji (ReceiptConfigRepository::listAll() filtruj is_active)
|
||||
- Mapuj wiersze do formatu tableList (analogicznie do OrdersController)
|
||||
- Renderuj widok accounting/index z tableList data
|
||||
- **export(Request): Response** — GET /accounting/export
|
||||
- Te same filtry co index
|
||||
- Wywolaj ReceiptRepository::exportData($filters)
|
||||
- Uzyj PhpSpreadsheet: nowy Spreadsheet, ustaw naglowki, wypelnij wiersze
|
||||
- Kolumny: Numer, Data wystawienia, Data sprzedazy, Kwota brutto, Konfiguracja, Nr zamowienia, Nr referencyjny
|
||||
- Zwroc Response z Content-Type xlsx i Content-Disposition attachment
|
||||
- Nazwa pliku: paragony_YYYY-MM-DD.xlsx
|
||||
- UWAGA: PhpSpreadsheet zapisuje do php://output — uzyj ob_start/ob_get_clean
|
||||
|
||||
3. **routes/web.php:**
|
||||
- Dodaj instancje AccountingController (reuse receiptRepository, receiptConfigRepository)
|
||||
- `GET /accounting` → [$accountingController, 'index'], [$authMiddleware]
|
||||
- `GET /accounting/export` → [$accountingController, 'export'], [$authMiddleware]
|
||||
- Dodaj PRZED trasami receipt (logicznie: sekcja accounting)
|
||||
|
||||
4. **resources/views/accounting/index.php:**
|
||||
- Uzyj komponentu table-list (include components/table-list.php z $tableList)
|
||||
- Dodaj przycisk "Eksportuj XLSX" w naglowku (link do /accounting/export z aktualnymi filtrami w query string)
|
||||
- Wzorzec: analogicznie do orders/list.php
|
||||
|
||||
5. **resources/views/layouts/app.php:**
|
||||
- Dodaj nowa sekcje w sidebar PRZED "Ustawienia":
|
||||
```php
|
||||
<a class="sidebar__link<?= $currentMenu === 'accounting' ? ' is-active' : '' ?>" href="/accounting">
|
||||
<svg>...</svg> <?= $e($t('navigation.accounting_section')) ?>
|
||||
</a>
|
||||
```
|
||||
- Uzyj ikony dokumentu/receipt (inline SVG)
|
||||
|
||||
6. **resources/lang/pl.php:**
|
||||
- Dodaj klucze: `navigation.accounting_section` => 'Ksiegowosc'
|
||||
- `accounting.title`, `accounting.export`, `accounting.empty`
|
||||
- `accounting.filters.*`: search, config, date_from, date_to, any
|
||||
- `accounting.columns.*`: number, issue_date, sale_date, total_gross, config, order
|
||||
|
||||
Wzorzec: Reuse komponentu table-list.php, analogia do OrdersController::index().
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l src/Modules/Accounting/AccountingController.php`
|
||||
- `php -l routes/web.php`
|
||||
- `php -l resources/views/accounting/index.php`
|
||||
- `php -l resources/views/layouts/app.php`
|
||||
- `composer show phpoffice/phpspreadsheet` — zainstalowany
|
||||
</verify>
|
||||
<done>AC-1, AC-2, AC-3, AC-4, AC-5 spelnione</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: SCSS build + aktualizacja dokumentacji</name>
|
||||
<files>
|
||||
resources/scss/app.scss,
|
||||
public/assets/css/app.css,
|
||||
DOCS/ARCHITECTURE.md
|
||||
</files>
|
||||
<action>
|
||||
1. Build SCSS do CSS (jesli nowe klasy dodane)
|
||||
2. Zaktualizuj DOCS/ARCHITECTURE.md:
|
||||
- Dodaj AccountingController (index, export) do sekcji Modules/Accounting
|
||||
- Dodaj ReceiptRepository::paginate(), exportData()
|
||||
- Dodaj trasy /accounting i /accounting/export
|
||||
</action>
|
||||
<verify>
|
||||
- `npx sass --style=compressed --no-source-map resources/scss/app.scss public/assets/css/app.css`
|
||||
- Brak bledow
|
||||
</verify>
|
||||
<done>Dokumentacja zaktualizowana, CSS zbudowany</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Sekcja Ksiegowosc: lista paragonow z filtrami, paginacja, eksport XLSX, link w nawigacji</what-built>
|
||||
<how-to-verify>
|
||||
1. Otworz aplikacje → sidebar → kliknij "Ksiegowosc"
|
||||
2. Sprawdz: tabela paragonow wyswietla sie z kolumnami
|
||||
3. Uzyj filtrow: szukaj po numerze, wybierz konfiguracje, ustaw daty
|
||||
4. Sprawdz paginacje: zmien strone, zmien ilosc na strone
|
||||
5. Kliknij "Eksportuj XLSX" — plik pobiera sie
|
||||
6. Otworz XLSX — dane poprawne, filtry uwzglednione
|
||||
7. Sprawdz czy link do zamowienia w tabeli dziala
|
||||
8. Sprawdz pusta liste (filtr bez wynikow) — komunikat "Brak paragonow"
|
||||
</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/ReceiptController.php (gotowe z fazy 11 — tylko odczyt)
|
||||
- resources/views/receipts/* (gotowe z fazy 11)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak edycji/anulowania paragonu — poza zakresem v0.3
|
||||
- Brak zaawansowanych raportow (sumy per konfiguracja, wykresy) — future
|
||||
- Brak eksportu CSV — tylko XLSX
|
||||
- Brak filtrowania po nabywcy — uproszczenie v0.3
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `composer show phpoffice/phpspreadsheet` — zainstalowany
|
||||
- [ ] `php -l src/Modules/Accounting/AccountingController.php`
|
||||
- [ ] `php -l src/Modules/Accounting/ReceiptRepository.php`
|
||||
- [ ] `php -l resources/views/accounting/index.php`
|
||||
- [ ] `php -l routes/web.php`
|
||||
- [ ] Link "Ksiegowosc" widoczny w sidebar nawigacji
|
||||
- [ ] Lista paragonow wyswietla sie z paginacja
|
||||
- [ ] Filtry dzialaja (search, config, date_from, date_to)
|
||||
- [ ] Sortowanie dziala (numer, data, kwota)
|
||||
- [ ] Eksport XLSX pobiera sie i zawiera poprawne dane
|
||||
- [ ] Pusta lista wyswietla komunikat
|
||||
- [ ] All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 taski auto + 1 checkpoint ukonczone
|
||||
- Wszystkie AC-1 do AC-5 spelnione
|
||||
- PhpSpreadsheet zainstalowany i dzialajacy
|
||||
- Brak bledow PHP
|
||||
- Weryfikacja manualna przez uzytkownika (checkpoint)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/12-accounting-list/12-01-SUMMARY.md`
|
||||
</output>
|
||||
143
.paul/phases/12-accounting-list/12-01-SUMMARY.md
Normal file
143
.paul/phases/12-accounting-list/12-01-SUMMARY.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
phase: 12-accounting-list
|
||||
plan: 01
|
||||
subsystem: accounting
|
||||
tags: [phpspreadsheet, xlsx, export, list, pagination, filters, selectable]
|
||||
|
||||
requires:
|
||||
- phase: 11-receipt-print
|
||||
provides: ReceiptController show/print/pdf, receipt views
|
||||
- phase: 10-receipt-issue
|
||||
provides: ReceiptRepository, receipts table, snapshots
|
||||
provides:
|
||||
- Accounting section in main navigation
|
||||
- Receipt list with filters, pagination, sorting
|
||||
- XLSX export (selected or all from filter)
|
||||
- Selectable rows with checkboxes
|
||||
affects: []
|
||||
|
||||
tech-stack:
|
||||
added: [phpoffice/phpspreadsheet ^5.5]
|
||||
patterns: [selectable table-list with POST export, findByIds batch query]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- src/Modules/Accounting/AccountingController.php
|
||||
- resources/views/accounting/index.php
|
||||
modified:
|
||||
- src/Modules/Accounting/ReceiptRepository.php
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
- resources/lang/pl.php
|
||||
- composer.json
|
||||
- DOCS/ARCHITECTURE.md
|
||||
|
||||
key-decisions:
|
||||
- "PhpSpreadsheet v5.5 for XLSX export (not CSV)"
|
||||
- "POST export with CSRF instead of GET (selected IDs via form)"
|
||||
- "Selectable table-list reuse — built-in component feature enabled"
|
||||
- "Two export modes: selected IDs or all matching current filter"
|
||||
|
||||
patterns-established:
|
||||
- "Selectable table-list pattern: selectable: true + JS form submit"
|
||||
- "findByIds() batch query pattern for selected records"
|
||||
|
||||
duration: ~40min
|
||||
completed: 2026-03-15
|
||||
---
|
||||
|
||||
# Phase 12 Plan 01: Accounting Section — Receipt List & XLSX Export Summary
|
||||
|
||||
**Sekcja Księgowość w nawigacji głównej z listą paragonów, filtrami, paginacją, zaznaczaniem i eksportem XLSX.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~40min |
|
||||
| Completed | 2026-03-15 |
|
||||
| Tasks | 4 completed (3 auto + 1 checkpoint) |
|
||||
| Files modified | 10 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Link Księgowość w nawigacji głównej | Pass | Osobny link w sidebar z ikoną dokumentu, przed Ustawienia |
|
||||
| AC-2: Lista paragonów z paginacją | Pass | Reuse table-list, sortowanie po numerze/dacie/kwocie |
|
||||
| AC-3: Filtry listy | Pass | Szukaj, konfiguracja (select), data od/do — zachowane w URL |
|
||||
| AC-4: Eksport XLSX | Pass | Dwa tryby: zaznaczone (POST + selected_ids) i wszystkie z filtra |
|
||||
| AC-5: Pusta lista | Pass | Komunikat "Brak paragonów" |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- AccountingController z index() i export() — pełna sekcja księgowości
|
||||
- ReceiptRepository: paginate(), exportData(), findByIds() — 3 nowe metody
|
||||
- Zaznaczanie paragonów checkboxami (reuse wbudowanego selectable w table-list)
|
||||
- Eksport XLSX z PhpSpreadsheet: zaznaczone lub wszystkie z filtra
|
||||
- Link "Księgowość" w sidebar nawigacji
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/Modules/Accounting/AccountingController.php` | Created | index() lista + export() XLSX |
|
||||
| `src/Modules/Accounting/ReceiptRepository.php` | Modified | +paginate(), +exportData(), +findByIds() |
|
||||
| `routes/web.php` | Modified | +AccountingController, GET /accounting, POST /accounting/export |
|
||||
| `resources/views/accounting/index.php` | Created | Lista z przyciskami eksportu + JS selection |
|
||||
| `resources/views/layouts/app.php` | Modified | +link Księgowość w sidebar |
|
||||
| `resources/lang/pl.php` | Modified | +accounting.*, +navigation.accounting_section |
|
||||
| `composer.json` | Modified | +phpoffice/phpspreadsheet ^5.5 |
|
||||
| `public/assets/css/app.css` | Modified | Rebuild |
|
||||
| `DOCS/ARCHITECTURE.md` | Modified | +AccountingController, +trasy, +metody repo |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| PhpSpreadsheet (nie CSV) | XLSX lepszy dla księgowości — formatowanie, polskie znaki | Nowa zależność composer |
|
||||
| POST eksport z CSRF | Wysyłanie selected_ids[] wymaga POST; bezpieczeństwo | Formularz ukryty + JS submit |
|
||||
| Dwa tryby eksportu | User request: zaznaczanie + eksport wszystkich z filtra | findByIds() + exportData() |
|
||||
| Selectable table-list reuse | Komponent już ma wbudowane checkboxy | Zero nowego kodu w komponencie |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Scope additions | 1 | Zaznaczanie paragonów (user request during checkpoint) |
|
||||
| Auto-fixed | 0 | — |
|
||||
|
||||
**Total impact:** Pozytywne rozszerzenie — zaznaczanie paragonów do eksportu na prośbę użytkownika.
|
||||
|
||||
### Scope Additions
|
||||
|
||||
**1. Zaznaczanie paragonów do eksportu**
|
||||
- **Requested during:** Checkpoint (Task 4)
|
||||
- **What:** Checkboxy, "Eksportuj zaznaczone", "Eksportuj wszystkie (filtr)"
|
||||
- **Implementation:** selectable: true w table-list, POST export, findByIds(), JS form
|
||||
- **Impact:** Lepsza kontrola użytkownika nad eksportem
|
||||
|
||||
### Skill Audit
|
||||
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Milestone v0.3 Moduł Paragonów COMPLETE
|
||||
- Pełny cykl: konfiguracja → wystawienie → podgląd → druk → PDF → lista → eksport
|
||||
|
||||
**Concerns:**
|
||||
- sonar-scanner nie uruchomiony (gap z wielu faz)
|
||||
- vendor/ musi być uploadowany ręcznie na serwer po każdej zmianie zależności
|
||||
|
||||
**Blockers:**
|
||||
- Brak
|
||||
|
||||
---
|
||||
*Phase: 12-accounting-list, Plan: 01*
|
||||
*Completed: 2026-03-15*
|
||||
@@ -9,7 +9,7 @@
|
||||
- `App\Modules\Orders`
|
||||
- `App\Modules\Users`
|
||||
- `App\Modules\Settings`
|
||||
- `App\Modules\Accounting` (modul paragonow — wystawianie z zamowien)
|
||||
- `App\Modules\Accounting` (modul paragonow — wystawianie, podglad, druk, PDF, lista, eksport XLSX)
|
||||
|
||||
## Routing
|
||||
- `GET /login`, `POST /login`, `POST /logout`
|
||||
@@ -18,6 +18,8 @@
|
||||
- `GET /orders/list`
|
||||
- `GET /orders/{id}`
|
||||
- `POST /orders/{id}/status`
|
||||
- `GET /accounting` (lista paragonow z filtrami i paginacja)
|
||||
- `GET /accounting/export` (eksport XLSX z aktywnymi filtrami)
|
||||
- `GET /users` (redirect do `/settings/users`)
|
||||
- `POST /users` (compat route)
|
||||
- `GET /settings` (redirect do `/settings/users`)
|
||||
@@ -118,8 +120,9 @@
|
||||
- `App\Modules\Settings\AllegroStatusSyncService`
|
||||
- `App\Modules\Settings\ReceiptConfigController`
|
||||
- `App\Modules\Settings\ReceiptConfigRepository`
|
||||
- `App\Modules\Accounting\ReceiptRepository`
|
||||
- `App\Modules\Accounting\ReceiptController`
|
||||
- `App\Modules\Accounting\ReceiptRepository` (findById, findByOrderId, create, getNextNumber, paginate, exportData)
|
||||
- `App\Modules\Accounting\ReceiptController` (create, store, show, printView, pdf)
|
||||
- `App\Modules\Accounting\AccountingController` (index — lista paragonow, export — XLSX)
|
||||
- `App\Modules\Shipments\ShipmentProviderInterface`
|
||||
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
||||
- `App\Modules\Shipments\ApaczkaShipmentService`
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"dompdf/dompdf": "^3.1"
|
||||
"dompdf/dompdf": "^3.1",
|
||||
"phpoffice/phpspreadsheet": "^5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.5",
|
||||
|
||||
@@ -34,6 +34,7 @@ return [
|
||||
'inpost' => 'Integracja InPost',
|
||||
'company' => 'Dane firmy',
|
||||
'accounting' => 'Ksiegowosc',
|
||||
'accounting_section' => 'Ksiegowosc',
|
||||
],
|
||||
'marketplace' => [
|
||||
'title' => 'Marketplace',
|
||||
@@ -1258,4 +1259,26 @@ return [
|
||||
'empty' => 'Brak dokumentow',
|
||||
],
|
||||
],
|
||||
'accounting' => [
|
||||
'title' => 'Ksiegowosc — Paragony',
|
||||
'export' => 'Eksportuj XLSX',
|
||||
'export_selected' => 'Eksportuj zaznaczone',
|
||||
'export_all' => 'Eksportuj wszystkie (filtr)',
|
||||
'empty' => 'Brak paragonow',
|
||||
'filters' => [
|
||||
'search' => 'Szukaj (numer paragonu, zamowienia)',
|
||||
'config' => 'Konfiguracja',
|
||||
'date_from' => 'Data od',
|
||||
'date_to' => 'Data do',
|
||||
'any' => 'Wszystkie',
|
||||
],
|
||||
'columns' => [
|
||||
'number' => 'Numer paragonu',
|
||||
'issue_date' => 'Data wystawienia',
|
||||
'sale_date' => 'Data sprzedazy',
|
||||
'total_gross' => 'Kwota brutto',
|
||||
'config' => 'Konfiguracja',
|
||||
'order' => 'Zamowienie',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
99
resources/views/accounting/index.php
Normal file
99
resources/views/accounting/index.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
$exportQueryVal = (string) ($exportQuery ?? '');
|
||||
$csrfTokenVal = (string) ($csrfToken ?? '');
|
||||
$queryData = is_array($tableList['query'] ?? null) ? $tableList['query'] : [];
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<div class="orders-head">
|
||||
<div>
|
||||
<h2 class="section-title"><?= $e($t('accounting.title')) ?></h2>
|
||||
</div>
|
||||
<div class="accounting-export-actions">
|
||||
<span class="muted js-accounting-selection-info"></span>
|
||||
<button type="button" class="btn btn--secondary js-accounting-export-selected" disabled><?= $e($t('accounting.export_selected')) ?></button>
|
||||
<button type="button" class="btn btn--primary js-accounting-export-all"><?= $e($t('accounting.export_all')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form id="accountingExportForm" method="post" action="/accounting/export" style="display:none;">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfTokenVal) ?>">
|
||||
<input type="hidden" name="export_all" value="0" id="exportAllFlag">
|
||||
<?php foreach ($queryData as $key => $value): ?>
|
||||
<?php if ($value !== '' && $value !== null && $key !== 'page' && $key !== 'per_page' && $key !== 'sort' && $key !== 'sort_dir'): ?>
|
||||
<input type="hidden" name="<?= $e((string) $key) ?>" value="<?= $e((string) $value) ?>">
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<div id="exportSelectedIds"></div>
|
||||
</form>
|
||||
|
||||
<?php require __DIR__ . '/../components/table-list.php'; ?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var form = document.getElementById('accountingExportForm');
|
||||
var exportAllFlag = document.getElementById('exportAllFlag');
|
||||
var exportSelectedIds = document.getElementById('exportSelectedIds');
|
||||
var exportSelectedBtn = document.querySelector('.js-accounting-export-selected');
|
||||
var exportAllBtn = document.querySelector('.js-accounting-export-all');
|
||||
var selectionInfo = document.querySelector('.js-accounting-selection-info');
|
||||
var tableRoot = document.querySelector('[data-table-list-id="accounting"]');
|
||||
|
||||
function getCheckedIds() {
|
||||
if (!tableRoot) return [];
|
||||
var items = tableRoot.querySelectorAll('.js-table-select-item:checked');
|
||||
var ids = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
ids.push(items[i].value);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
var ids = getCheckedIds();
|
||||
var count = ids.length;
|
||||
if (exportSelectedBtn) {
|
||||
exportSelectedBtn.disabled = count === 0;
|
||||
}
|
||||
if (selectionInfo) {
|
||||
selectionInfo.textContent = count > 0 ? 'Zaznaczono: ' + count : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (tableRoot) {
|
||||
tableRoot.addEventListener('change', function(e) {
|
||||
if (e.target && (e.target.classList.contains('js-table-select-item') || e.target.classList.contains('js-table-select-all'))) {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (exportSelectedBtn) {
|
||||
exportSelectedBtn.addEventListener('click', function() {
|
||||
var ids = getCheckedIds();
|
||||
if (ids.length === 0) return;
|
||||
exportAllFlag.value = '0';
|
||||
exportSelectedIds.innerHTML = '';
|
||||
ids.forEach(function(id) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'selected_ids[]';
|
||||
input.value = id;
|
||||
exportSelectedIds.appendChild(input);
|
||||
});
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
if (exportAllBtn) {
|
||||
exportAllBtn.addEventListener('click', function() {
|
||||
exportAllFlag.value = '1';
|
||||
exportSelectedIds.innerHTML = '';
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
})();
|
||||
</script>
|
||||
@@ -47,6 +47,19 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<a class="sidebar__link<?= ($currentMenu ?? '') === 'accounting' ? ' is-active' : '' ?>" href="/accounting">
|
||||
<span class="sidebar__icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="sidebar__label"><?= $e($t('navigation.accounting_section')) ?></span>
|
||||
</a>
|
||||
|
||||
<details class="sidebar__group<?= $currentMenu === 'settings' ? ' is-active' : '' ?>"<?= $currentMenu === 'settings' ? ' open' : '' ?>>
|
||||
<summary class="sidebar__group-toggle">
|
||||
<span class="sidebar__icon">
|
||||
|
||||
@@ -36,6 +36,7 @@ use App\Modules\Settings\CompanySettingsController;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\ReceiptConfigController;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use App\Modules\Accounting\AccountingController;
|
||||
use App\Modules\Accounting\ReceiptController;
|
||||
use App\Modules\Accounting\ReceiptRepository;
|
||||
use App\Modules\Settings\CronSettingsController;
|
||||
@@ -192,6 +193,13 @@ return static function (Application $app): void {
|
||||
$companySettingsRepository,
|
||||
new OrdersRepository($app->db())
|
||||
);
|
||||
$accountingController = new AccountingController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$receiptRepository,
|
||||
$receiptConfigRepository
|
||||
);
|
||||
$allegroApiClient = new AllegroApiClient();
|
||||
$shipmentPackageRepository = new ShipmentPackageRepository($app->db());
|
||||
$shipmentService = new AllegroShipmentService(
|
||||
@@ -299,6 +307,8 @@ return static function (Application $app): void {
|
||||
$router->post('/settings/accounting/save', [$receiptConfigController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
||||
$router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]);
|
||||
$router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]);
|
||||
$router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]);
|
||||
$router->post('/orders/{id}/receipt/store', [$receiptController, 'store'], [$authMiddleware]);
|
||||
$router->get('/orders/{id}/receipt/{receiptId}', [$receiptController, 'show'], [$authMiddleware]);
|
||||
|
||||
235
src/Modules/Accounting/AccountingController.php
Normal file
235
src/Modules/Accounting/AccountingController.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Accounting;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\View\Template;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
final class AccountingController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly ReceiptRepository $receipts,
|
||||
private readonly ReceiptConfigRepository $receiptConfigs
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filters = $this->parseFilters($request);
|
||||
$result = $this->receipts->paginate($filters);
|
||||
|
||||
$totalPages = max(1, (int) ceil(((int) $result['total']) / max(1, (int) $result['per_page'])));
|
||||
|
||||
$configs = array_filter(
|
||||
$this->receiptConfigs->listAll(),
|
||||
static fn(array $c): bool => (int) ($c['is_active'] ?? 0) === 1
|
||||
);
|
||||
$configOptions = ['' => $this->translator->get('accounting.filters.any')];
|
||||
foreach ($configs as $cfg) {
|
||||
$configOptions[(string) ($cfg['id'] ?? '')] = (string) ($cfg['name'] ?? '');
|
||||
}
|
||||
|
||||
$tableRows = array_map(fn(array $row): array => $this->toTableRow($row), (array) ($result['items'] ?? []));
|
||||
|
||||
$html = $this->template->render('accounting/index', [
|
||||
'title' => $this->translator->get('accounting.title'),
|
||||
'activeMenu' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'tableList' => [
|
||||
'list_key' => 'accounting',
|
||||
'base_path' => '/accounting',
|
||||
'query' => $filters,
|
||||
'filters' => [
|
||||
[
|
||||
'key' => 'search',
|
||||
'label' => $this->translator->get('accounting.filters.search'),
|
||||
'type' => 'text',
|
||||
'value' => $filters['search'],
|
||||
],
|
||||
[
|
||||
'key' => 'config_id',
|
||||
'label' => $this->translator->get('accounting.filters.config'),
|
||||
'type' => 'select',
|
||||
'value' => (string) $filters['config_id'],
|
||||
'options' => $configOptions,
|
||||
],
|
||||
[
|
||||
'key' => 'date_from',
|
||||
'label' => $this->translator->get('accounting.filters.date_from'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_from'],
|
||||
],
|
||||
[
|
||||
'key' => 'date_to',
|
||||
'label' => $this->translator->get('accounting.filters.date_to'),
|
||||
'type' => 'date',
|
||||
'value' => $filters['date_to'],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
['key' => 'receipt_number', 'label' => $this->translator->get('accounting.columns.number'), 'sortable' => true, 'sort_key' => 'receipt_number', 'raw' => true],
|
||||
['key' => 'issue_date', 'label' => $this->translator->get('accounting.columns.issue_date'), 'sortable' => true, 'sort_key' => 'issue_date'],
|
||||
['key' => 'sale_date', 'label' => $this->translator->get('accounting.columns.sale_date')],
|
||||
['key' => 'total_gross', 'label' => $this->translator->get('accounting.columns.total_gross'), 'sortable' => true, 'sort_key' => 'total_gross'],
|
||||
['key' => 'config_name', 'label' => $this->translator->get('accounting.columns.config')],
|
||||
['key' => 'order_ref', 'label' => $this->translator->get('accounting.columns.order'), 'raw' => true],
|
||||
],
|
||||
'rows' => $tableRows,
|
||||
'pagination' => [
|
||||
'page' => (int) ($result['page'] ?? 1),
|
||||
'total_pages' => $totalPages,
|
||||
'total' => (int) ($result['total'] ?? 0),
|
||||
'per_page' => (int) ($result['per_page'] ?? 20),
|
||||
],
|
||||
'per_page_options' => [20, 50, 100],
|
||||
'empty_message' => $this->translator->get('accounting.empty'),
|
||||
'show_actions' => false,
|
||||
'selectable' => true,
|
||||
'select_name' => 'selected_ids[]',
|
||||
'select_value_key' => 'id',
|
||||
],
|
||||
'exportQuery' => $this->buildExportQuery($filters),
|
||||
'csrfToken' => Csrf::token(),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
return Response::redirect('/accounting');
|
||||
}
|
||||
|
||||
$exportAll = (string) $request->input('export_all', '0') === '1';
|
||||
$selectedIds = (array) $request->input('selected_ids', []);
|
||||
$selectedIds = array_filter(array_map('intval', $selectedIds), static fn(int $id): bool => $id > 0);
|
||||
|
||||
$filters = $this->parseFilters($request);
|
||||
|
||||
if ($exportAll) {
|
||||
$rows = $this->receipts->exportData($filters);
|
||||
} elseif ($selectedIds !== []) {
|
||||
$rows = $this->receipts->findByIds($selectedIds);
|
||||
} else {
|
||||
return Response::redirect('/accounting');
|
||||
}
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Paragony');
|
||||
|
||||
$headers = ['Numer', 'Data wystawienia', 'Data sprzedazy', 'Kwota brutto', 'Konfiguracja', 'Nr zamowienia', 'Nr referencyjny'];
|
||||
foreach ($headers as $col => $header) {
|
||||
$sheet->setCellValue([$col + 1, 1], $header);
|
||||
}
|
||||
|
||||
$sheet->getStyle('1:1')->getFont()->setBold(true);
|
||||
|
||||
$rowNum = 2;
|
||||
foreach ($rows as $row) {
|
||||
$sheet->setCellValue([1, $rowNum], (string) ($row['receipt_number'] ?? ''));
|
||||
$sheet->setCellValue([2, $rowNum], (string) ($row['issue_date'] ?? ''));
|
||||
$sheet->setCellValue([3, $rowNum], (string) ($row['sale_date'] ?? ''));
|
||||
$sheet->setCellValue([4, $rowNum], (float) ($row['total_gross'] ?? 0));
|
||||
$sheet->setCellValue([5, $rowNum], (string) ($row['config_name'] ?? ''));
|
||||
$sheet->setCellValue([6, $rowNum], (string) ($row['internal_order_number'] ?? $row['external_order_id'] ?? ''));
|
||||
$sheet->setCellValue([7, $rowNum], (string) ($row['order_reference_value'] ?? ''));
|
||||
$rowNum++;
|
||||
}
|
||||
|
||||
foreach (range(1, 7) as $col) {
|
||||
$sheet->getColumnDimensionByColumn($col)->setAutoSize(true);
|
||||
}
|
||||
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
ob_start();
|
||||
$writer->save('php://output');
|
||||
$content = ob_get_clean();
|
||||
|
||||
$filename = 'paragony_' . date('Y-m-d') . '.xlsx';
|
||||
|
||||
return new Response($content ?: '', 200, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseFilters(Request $request): array
|
||||
{
|
||||
return [
|
||||
'search' => trim((string) $request->input('search', '')),
|
||||
'config_id' => (int) $request->input('config_id', 0),
|
||||
'date_from' => trim((string) $request->input('date_from', '')),
|
||||
'date_to' => trim((string) $request->input('date_to', '')),
|
||||
'sort' => (string) $request->input('sort', 'issue_date'),
|
||||
'sort_dir' => (string) $request->input('sort_dir', 'DESC'),
|
||||
'page' => max(1, (int) $request->input('page', 1)),
|
||||
'per_page' => max(1, min(100, (int) $request->input('per_page', 20))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function toTableRow(array $row): array
|
||||
{
|
||||
$orderId = (int) ($row['order_id'] ?? 0);
|
||||
$receiptId = (int) ($row['id'] ?? 0);
|
||||
$receiptNumber = htmlspecialchars((string) ($row['receipt_number'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
$orderNumber = htmlspecialchars(
|
||||
(string) ($row['internal_order_number'] ?? $row['external_order_id'] ?? '#' . $orderId),
|
||||
ENT_QUOTES,
|
||||
'UTF-8'
|
||||
);
|
||||
|
||||
return [
|
||||
'id' => (string) $receiptId,
|
||||
'receipt_number' => '<a href="/orders/' . $orderId . '/receipt/' . $receiptId . '">' . $receiptNumber . '</a>',
|
||||
'issue_date' => (string) ($row['issue_date'] ?? ''),
|
||||
'sale_date' => (string) ($row['sale_date'] ?? ''),
|
||||
'total_gross' => $row['total_gross'] !== null
|
||||
? number_format((float) $row['total_gross'], 2, '.', ' ')
|
||||
: '-',
|
||||
'config_name' => (string) ($row['config_name'] ?? '-'),
|
||||
'order_ref' => '<a href="/orders/' . $orderId . '">' . $orderNumber . '</a>',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
*/
|
||||
private function buildExportQuery(array $filters): string
|
||||
{
|
||||
$params = [];
|
||||
if (($filters['search'] ?? '') !== '') {
|
||||
$params['search'] = (string) $filters['search'];
|
||||
}
|
||||
if (($filters['config_id'] ?? 0) > 0) {
|
||||
$params['config_id'] = (string) $filters['config_id'];
|
||||
}
|
||||
if (($filters['date_from'] ?? '') !== '') {
|
||||
$params['date_from'] = (string) $filters['date_from'];
|
||||
}
|
||||
if (($filters['date_to'] ?? '') !== '') {
|
||||
$params['date_to'] = (string) $filters['date_to'];
|
||||
}
|
||||
|
||||
return $params !== [] ? '?' . http_build_query($params) : '';
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,32 @@ final class ReceiptRepository
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findByIds(array $ids): array
|
||||
{
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$statement = $this->pdo->prepare(
|
||||
"SELECT r.*, rc.name AS config_name,
|
||||
o.internal_order_number, o.external_order_id
|
||||
FROM receipts r
|
||||
LEFT JOIN receipt_configs rc ON rc.id = r.config_id
|
||||
LEFT JOIN orders o ON o.id = r.order_id
|
||||
WHERE r.id IN ({$placeholders})
|
||||
ORDER BY r.issue_date DESC"
|
||||
);
|
||||
$statement->execute(array_values($ids));
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
@@ -118,4 +144,132 @@ final class ReceiptRepository
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
* @return array{items: list<array<string, mixed>>, total: int, page: int, per_page: int}
|
||||
*/
|
||||
public function paginate(array $filters): array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
$where[] = '(r.receipt_number LIKE :search OR o.internal_order_number LIKE :search2 OR o.external_order_id LIKE :search3)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
$params['search2'] = '%' . $search . '%';
|
||||
$params['search3'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$configId = (int) ($filters['config_id'] ?? 0);
|
||||
if ($configId > 0) {
|
||||
$where[] = 'r.config_id = :config_id';
|
||||
$params['config_id'] = $configId;
|
||||
}
|
||||
|
||||
$dateFrom = trim((string) ($filters['date_from'] ?? ''));
|
||||
if ($dateFrom !== '' && strtotime($dateFrom) !== false) {
|
||||
$where[] = 'r.issue_date >= :date_from';
|
||||
$params['date_from'] = $dateFrom;
|
||||
}
|
||||
|
||||
$dateTo = trim((string) ($filters['date_to'] ?? ''));
|
||||
if ($dateTo !== '' && strtotime($dateTo) !== false) {
|
||||
$where[] = 'r.issue_date <= :date_to';
|
||||
$params['date_to'] = $dateTo;
|
||||
}
|
||||
|
||||
$whereClause = $where !== [] ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$allowedSort = ['receipt_number', 'issue_date', 'total_gross'];
|
||||
$sort = in_array((string) ($filters['sort'] ?? ''), $allowedSort, true)
|
||||
? (string) $filters['sort']
|
||||
: 'issue_date';
|
||||
$sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
|
||||
|
||||
$countStmt = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM receipts r
|
||||
LEFT JOIN orders o ON o.id = r.order_id
|
||||
{$whereClause}"
|
||||
);
|
||||
$countStmt->execute($params);
|
||||
$total = (int) $countStmt->fetchColumn();
|
||||
|
||||
$page = max(1, (int) ($filters['page'] ?? 1));
|
||||
$perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20)));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT r.*, rc.name AS config_name,
|
||||
o.internal_order_number, o.external_order_id
|
||||
FROM receipts r
|
||||
LEFT JOIN receipt_configs rc ON rc.id = r.config_id
|
||||
LEFT JOIN orders o ON o.id = r.order_id
|
||||
{$whereClause}
|
||||
ORDER BY r.{$sort} {$sortDir}
|
||||
LIMIT {$perPage} OFFSET {$offset}"
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return [
|
||||
'items' => is_array($rows) ? $rows : [],
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filters
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function exportData(array $filters): array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
$where[] = '(r.receipt_number LIKE :search OR o.internal_order_number LIKE :search2 OR o.external_order_id LIKE :search3)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
$params['search2'] = '%' . $search . '%';
|
||||
$params['search3'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$configId = (int) ($filters['config_id'] ?? 0);
|
||||
if ($configId > 0) {
|
||||
$where[] = 'r.config_id = :config_id';
|
||||
$params['config_id'] = $configId;
|
||||
}
|
||||
|
||||
$dateFrom = trim((string) ($filters['date_from'] ?? ''));
|
||||
if ($dateFrom !== '' && strtotime($dateFrom) !== false) {
|
||||
$where[] = 'r.issue_date >= :date_from';
|
||||
$params['date_from'] = $dateFrom;
|
||||
}
|
||||
|
||||
$dateTo = trim((string) ($filters['date_to'] ?? ''));
|
||||
if ($dateTo !== '' && strtotime($dateTo) !== false) {
|
||||
$where[] = 'r.issue_date <= :date_to';
|
||||
$params['date_to'] = $dateTo;
|
||||
}
|
||||
|
||||
$whereClause = $where !== [] ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT r.*, rc.name AS config_name,
|
||||
o.internal_order_number, o.external_order_id
|
||||
FROM receipts r
|
||||
LEFT JOIN receipt_configs rc ON rc.id = r.config_id
|
||||
LEFT JOIN orders o ON o.id = r.order_id
|
||||
{$whereClause}
|
||||
ORDER BY r.issue_date DESC"
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user