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 ✓ |
|
| 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/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
|
## Completed Milestones
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,32 @@
|
|||||||
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 COMPLETE, transition do fazy 12.
|
**Current focus:** Milestone v0.3 Moduł Paragonów — COMPLETE. Transition + milestone completion.
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v0.3 Moduł Paragonów
|
Milestone: v0.3 Moduł Paragonów — COMPLETE
|
||||||
Phase: 11 of 12 (11-receipt-print) — Complete
|
Phase: 12 of 12 (12-accounting-list) — Complete
|
||||||
Plan: 11-01 complete
|
Plan: 12-01 complete
|
||||||
Status: Loop closed, phase transition required
|
Status: Milestone v0.3 complete, transition required
|
||||||
Last activity: 2026-03-15 — Phase 11 complete (podgląd, druk, PDF paragonu)
|
Last activity: 2026-03-15 — Phase 12 complete (lista paragonów + eksport XLSX)
|
||||||
|
|
||||||
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: [████████░░] 80%
|
- v0.3 Moduł Paragonów: [██████████] 100% ✓
|
||||||
- Phase 8: [██████████] 100% ✓
|
- Phase 8: [██████████] 100% ✓
|
||||||
- Phase 9: [██████████] 100% ✓
|
- Phase 9: [██████████] 100% ✓
|
||||||
- Phase 10: [██████████] 100% ✓
|
- Phase 10: [██████████] 100% ✓
|
||||||
- Phase 11: [██████████] 100% ✓
|
- Phase 11: [██████████] 100% ✓
|
||||||
|
- Phase 12: [██████████] 100% ✓
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete — phase transition required]
|
✓ ✓ ✓ [Loop complete — milestone v0.3 COMPLETE]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Accumulated Context
|
## 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 | 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 | 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 | 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)
|
### Skill Audit (Faza 11, Plan 01)
|
||||||
| Oczekiwany | Wywołany | Uwagi |
|
| 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.
|
- **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: 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
|
Branch: main
|
||||||
Feature branches merged: none
|
Feature branches merged: none
|
||||||
|
|
||||||
@@ -142,13 +150,13 @@ Brak.
|
|||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-15
|
Last session: 2026-03-15
|
||||||
Stopped at: Phase 11 complete, transition required
|
Stopped at: Milestone v0.3 COMPLETE — transition required
|
||||||
Next action: Phase transition — git commit, ROADMAP update, then /paul:plan for phase 12
|
Next action: Git commit for phase 12, then /paul:complete-milestone v0.3
|
||||||
Resume file: .paul/phases/11-receipt-print/11-01-SUMMARY.md
|
Resume file: .paul/phases/12-accounting-list/12-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-11 done, Phase 12 next
|
- v0.3: COMPLETE ✓ (5 phases, 5 plans) — Moduł Paragonów
|
||||||
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
- 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\Orders`
|
||||||
- `App\Modules\Users`
|
- `App\Modules\Users`
|
||||||
- `App\Modules\Settings`
|
- `App\Modules\Settings`
|
||||||
- `App\Modules\Accounting` (modul paragonow — wystawianie z zamowien)
|
- `App\Modules\Accounting` (modul paragonow — wystawianie, podglad, druk, PDF, lista, eksport XLSX)
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
- `GET /login`, `POST /login`, `POST /logout`
|
- `GET /login`, `POST /login`, `POST /logout`
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
- `GET /orders/list`
|
- `GET /orders/list`
|
||||||
- `GET /orders/{id}`
|
- `GET /orders/{id}`
|
||||||
- `POST /orders/{id}/status`
|
- `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`)
|
- `GET /users` (redirect do `/settings/users`)
|
||||||
- `POST /users` (compat route)
|
- `POST /users` (compat route)
|
||||||
- `GET /settings` (redirect do `/settings/users`)
|
- `GET /settings` (redirect do `/settings/users`)
|
||||||
@@ -118,8 +120,9 @@
|
|||||||
- `App\Modules\Settings\AllegroStatusSyncService`
|
- `App\Modules\Settings\AllegroStatusSyncService`
|
||||||
- `App\Modules\Settings\ReceiptConfigController`
|
- `App\Modules\Settings\ReceiptConfigController`
|
||||||
- `App\Modules\Settings\ReceiptConfigRepository`
|
- `App\Modules\Settings\ReceiptConfigRepository`
|
||||||
- `App\Modules\Accounting\ReceiptRepository`
|
- `App\Modules\Accounting\ReceiptRepository` (findById, findByOrderId, create, getNextNumber, paginate, exportData)
|
||||||
- `App\Modules\Accounting\ReceiptController`
|
- `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\ShipmentProviderInterface`
|
||||||
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
||||||
- `App\Modules\Shipments\ApaczkaShipmentService`
|
- `App\Modules\Shipments\ApaczkaShipmentService`
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
"dompdf/dompdf": "^3.1"
|
"dompdf/dompdf": "^3.1",
|
||||||
|
"phpoffice/phpspreadsheet": "^5.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^11.5",
|
"phpunit/phpunit": "^11.5",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ return [
|
|||||||
'inpost' => 'Integracja InPost',
|
'inpost' => 'Integracja InPost',
|
||||||
'company' => 'Dane firmy',
|
'company' => 'Dane firmy',
|
||||||
'accounting' => 'Ksiegowosc',
|
'accounting' => 'Ksiegowosc',
|
||||||
|
'accounting_section' => 'Ksiegowosc',
|
||||||
],
|
],
|
||||||
'marketplace' => [
|
'marketplace' => [
|
||||||
'title' => 'Marketplace',
|
'title' => 'Marketplace',
|
||||||
@@ -1258,4 +1259,26 @@ return [
|
|||||||
'empty' => 'Brak dokumentow',
|
'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>
|
</div>
|
||||||
</details>
|
</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' : '' ?>>
|
<details class="sidebar__group<?= $currentMenu === 'settings' ? ' is-active' : '' ?>"<?= $currentMenu === 'settings' ? ' open' : '' ?>>
|
||||||
<summary class="sidebar__group-toggle">
|
<summary class="sidebar__group-toggle">
|
||||||
<span class="sidebar__icon">
|
<span class="sidebar__icon">
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use App\Modules\Settings\CompanySettingsController;
|
|||||||
use App\Modules\Settings\CompanySettingsRepository;
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
use App\Modules\Settings\ReceiptConfigController;
|
use App\Modules\Settings\ReceiptConfigController;
|
||||||
use App\Modules\Settings\ReceiptConfigRepository;
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
|
use App\Modules\Accounting\AccountingController;
|
||||||
use App\Modules\Accounting\ReceiptController;
|
use App\Modules\Accounting\ReceiptController;
|
||||||
use App\Modules\Accounting\ReceiptRepository;
|
use App\Modules\Accounting\ReceiptRepository;
|
||||||
use App\Modules\Settings\CronSettingsController;
|
use App\Modules\Settings\CronSettingsController;
|
||||||
@@ -192,6 +193,13 @@ return static function (Application $app): void {
|
|||||||
$companySettingsRepository,
|
$companySettingsRepository,
|
||||||
new OrdersRepository($app->db())
|
new OrdersRepository($app->db())
|
||||||
);
|
);
|
||||||
|
$accountingController = new AccountingController(
|
||||||
|
$template,
|
||||||
|
$translator,
|
||||||
|
$auth,
|
||||||
|
$receiptRepository,
|
||||||
|
$receiptConfigRepository
|
||||||
|
);
|
||||||
$allegroApiClient = new AllegroApiClient();
|
$allegroApiClient = new AllegroApiClient();
|
||||||
$shipmentPackageRepository = new ShipmentPackageRepository($app->db());
|
$shipmentPackageRepository = new ShipmentPackageRepository($app->db());
|
||||||
$shipmentService = new AllegroShipmentService(
|
$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/save', [$receiptConfigController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
$router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||||
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$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->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}', [$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;
|
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
|
* @param array<string, mixed> $data
|
||||||
*/
|
*/
|
||||||
@@ -118,4 +144,132 @@ final class ReceiptRepository
|
|||||||
|
|
||||||
return $number;
|
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