feat(114): accounting configs refactor + invoice configs CRUD
Phase 114 complete (v3.7 Invoices): - /settings/accounting jako hub-rozdroze (Paragony / Faktury) - /settings/accounting/receipts + /invoices osobne podstrony list i edycji - InvoiceConfigRepository + Controller (CRUD z walidacja delegacji) - Seed Domyslny VAT (NOT EXISTS idempotent) - invoice-config-form.js (toggle is_delegated -> integration_id) - confirm-delete.js (globalny modul OrderProAlerts.confirm) - Legacy aliasy starych endpointow /settings/accounting/save|toggle|delete Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Version | 3.7.0-dev |
|
||||
| Status | v3.7 in progress — Phase 113 (Fakturownia Integration Foundation) shipped |
|
||||
| Status | v3.7 in progress — Phases 113 (Fakturownia Foundation) + 114 (Accounting Configs Refactor) shipped |
|
||||
| Last Updated | 2026-05-10 |
|
||||
|
||||
## Requirements
|
||||
@@ -117,6 +117,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
||||
- [x] Re-import zamowienia (Allegro + shopPRO) wykrywa tranzycje payment_status 0/1->2 i emituje `payment.status_changed` (chain reguly #7 zmienia status na `w_realizacji`); naprawa luki dla zamowien zaimportowanych przed potwierdzeniem platnosci (case #864) + backfill CLI — Phase 111
|
||||
- [x] Re-import istniejacego zamowienia jest delta-only: skip dla pozycji/adresow/notatek (stabilne `order_items.id`, ochrona `project_generated`), zawezony `updateOrderDelta()`, propagacja anulowania ze zrodla, identical-payload no-op guard (case #882) — Phase 112
|
||||
- [x] Fundament v3.7 Invoices: tabele `invoices`, `invoice_configs`, `invoice_number_counters`, `fakturownia_integration_settings` + `orders.invoice_requested`; CRUD kont Fakturowni z testem polaczenia API (`/settings/integrations/fakturownia`); karta w hubie integracji — Phase 113
|
||||
- [x] Ksiegowosc: refaktor `/settings/accounting` na hub-rozdroze + osobne podstrony `/receipts` i `/invoices` + edycja na osobnym widoku; pelen CRUD `invoice_configs` z opcja delegacji do Fakturowni (conditional integration_id, serwerowa walidacja); seed `Domyslny VAT`; globalny modul `confirm-delete.js` — Phase 114
|
||||
|
||||
### Deferred
|
||||
|
||||
@@ -209,6 +210,10 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
|
||||
| Faktury: lokalna numeracja domyslna, delegacja przez `invoice_configs.is_delegated` | Pelna kontrola w default; opcja outsourcingu numeracji+PDF do Fakturowni gdy ksiegowy tak chce | 2026-05-10 | Active |
|
||||
| Brak eventu automatyzacji `invoice.created` (na start v3.7) | `receipt.created` pozostaje czysty — regula wysylki paragonu mailem nie zostanie odpalona dla faktury; mozliwe rozszerzenie jako osobny plan w przyszlosci | 2026-05-10 | Active |
|
||||
| Migracje no-op zawsze jako DDL (np. `ALTER TABLE COMMENT`), nigdy `SELECT 1;` | `SELECT` zwraca result set i pod PDO unbuffered blokuje kolejne migracje (SQLSTATE 2014) | 2026-05-10 | Active |
|
||||
| Ksiegowosc: osobne podstrony `/settings/accounting/{receipts\|invoices}` zamiast tabow/jednej strony | Dlugie listy + rosnacy form faktury (conditional fields delegacji) nie mieszczace sie na jednej stronie | 2026-05-10 | Active |
|
||||
| Legacy aliasy starych endpointow `/settings/accounting/save\|toggle\|delete` jako duplicate routes | Brak inwentaryzacji zewnetrznych referencji/bookmarkow - zero kosztu utrzymania, pelna wsteczna kompatybilnosc | 2026-05-10 | Active |
|
||||
| `OrderProAlerts.confirm` to options-object API (`{title, message, onConfirm, danger, confirmLabel}`), nie pozycyjne argumenty | Pozycyjne wywolanie cicho fail'uje - callback ginie. Bug znaleziony w Phase 114-01 podczas smoke testu user. Pattern dla wszystkich przyszlych confirm dialogow | 2026-05-10 | Active |
|
||||
| Globalny `confirm-delete.js` z `data-confirm-bound='1'` idempotent guard | Stare widoki maja inline scripts robiace to samo - guard zapobiega podwojnemu bindowi gdy modul globalny widzi juz-bound buttony. Mozna stopniowo migrowac stare widoki | 2026-05-10 | Active |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
@@ -240,6 +245,6 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md — Updated when requirements or context change*
|
||||
*Last updated: 2026-05-10 after Phase 113 (Fakturownia Integration Foundation) completion; v3.7 Invoices milestone in progress*
|
||||
*Last updated: 2026-05-10 after Phase 114 (Accounting Configs Refactor) completion; v3.7 Invoices milestone in progress*
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ Wystawianie faktur dla klientow z NIP poprzez integracje z Fakturownia (app.fakt
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 113 | Fakturownia Integration Foundation | 1/1 | Complete (2026-05-10) |
|
||||
| 114 | Receipt Config Edit Refactor + Invoice Configs CRUD | 0/? | Planning |
|
||||
| 114 | Accounting Configs Refactor (hub + osobne podstrony receipts/invoices) | 1/1 | Complete (2026-05-10) |
|
||||
| 115 | Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia) | 0/? | Planning |
|
||||
|
||||
Planowane kolejne fazy v3.7 (do dokladnego rozplanowania):
|
||||
- Wystawianie faktury z zamowienia (lokalne + delegacja Fakturownia)
|
||||
- Lista faktur w sekcji Ksiegowosc + podglad/wydruk PDF
|
||||
- `orders.invoice_requested` w importerach Allegro/shopPRO + toggle w UI zamowienia
|
||||
|
||||
@@ -492,4 +492,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-05-10 - Phase 113 (Fakturownia Integration Foundation) complete; v3.7 milestone in progress*
|
||||
*Last updated: 2026-05-10 - Phase 114 (Accounting Configs Refactor) complete; v3.7 milestone in progress*
|
||||
|
||||
@@ -5,46 +5,45 @@
|
||||
See: .paul/PROJECT.md (updated 2026-05-07)
|
||||
|
||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||
**Current focus:** v3.7 Invoices — Phase 113 (Fakturownia Integration Foundation) shipped; nastepna faza 114 (Receipt Config Edit Refactor + Invoice Configs CRUD) do zaplanowania
|
||||
**Current focus:** v3.7 Invoices — Phase 113 + 114 shipped; nastepna faza 115 (wystawianie faktury z zamowienia) do zaplanowania
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v3.7 Invoices (Fakturownia integration) — In progress
|
||||
Phase: 114 of TBD (Receipt Config Edit Refactor + Invoice Configs CRUD) — Not started
|
||||
Phase: 115 of TBD (Wystawianie faktury z zamowienia) — Not started
|
||||
Plan: pending
|
||||
Status: Phase 113 complete; ready to plan Phase 114
|
||||
Last activity: 2026-05-10 — UNIFY 113-01 complete, Phase 113 closed, transition done
|
||||
Status: Phase 114 complete (transition done); ready to plan Phase 115
|
||||
Last activity: 2026-05-10 — UNIFY 114-01 complete, Phase 114 closed, transition done
|
||||
|
||||
Progress:
|
||||
- Milestone v3.7: [██░░░░░░░░] ~15% (Phase 113 z planowanych ~5)
|
||||
- Phase 113: [██████████] 100% — Complete
|
||||
- Milestone v3.7: [███░░░░░░░] ~30% (Phase 113 + 114 zamkniete; planowane ~5 faz)
|
||||
- Phase 114: [██████████] 100% — Complete
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
v3.7 milestone:
|
||||
Phase 113 (Fakturownia Integration Foundation):
|
||||
Plan 113-01: PLAN ✓ → APPLY ✓ → UNIFY ✓
|
||||
-> Phase 113 closed (transition complete)
|
||||
Phase 114 (Receipt Config Edit Refactor + Invoice Configs CRUD): not started
|
||||
Phase 113 (Fakturownia Integration Foundation): Complete
|
||||
Phase 114 (Accounting Configs Refactor): Complete
|
||||
Phase 115 (Wystawianie faktury z zamowienia): not started
|
||||
```
|
||||
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Phase 113 closed; ready for Phase 114 plan]
|
||||
✓ ✓ ✓ [Phase 114 closed; ready for Phase 115 plan]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-05-10
|
||||
Stopped at: Phase 113 transition complete (PROJECT.md/ROADMAP.md updated, SUMMARY zapisany)
|
||||
Next action: /paul:plan dla Phase 114 (Receipt Config Edit Refactor + Invoice Configs CRUD)
|
||||
Resume file: .paul/phases/113-fakturownia-integration/113-01-SUMMARY.md
|
||||
Stopped at: Phase 114 transition complete (PROJECT.md + ROADMAP.md updated, SUMMARY zapisany)
|
||||
Next action: /paul:plan dla Phase 115 (wystawianie faktury z zamowienia, lokalna numeracja + delegacja Fakturownia)
|
||||
Resume file: .paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
|
||||
|
||||
## Git State
|
||||
|
||||
Last commit: (pending — feat(113): fakturownia integration foundation)
|
||||
Last commit: 2382018 feat(113): fakturownia integration foundation
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 114, Plan 01] Accounting Configs Refactor — `/settings/accounting` jako hub-rozdroze, osobne podstrony Paragony/Faktury, pelen CRUD `invoice_configs` z delegacja do Fakturowni
|
||||
- Phase 114 Task 1: migracja seed `Domyslny VAT` (idempotentna NOT EXISTS) + `InvoiceConfigRepository` z walidacja delegacji
|
||||
- Phase 114 Task 2: `InvoiceConfigController` + widoki listy/edycji + `invoice-config-form.js` (conditional integration_id select) + 6 routes
|
||||
- Phase 114 Task 3: `ReceiptConfigController` refactor (hub/list/edit split) + hub view + 2 widoki paragonow + legacy aliasy + docs
|
||||
- Auto-fix: globalny `confirm-delete.js` zastapil rozsiane inline scripts (po user feedback "Usuwanie konfiguracji faktur nie dziala")
|
||||
- Auto-fix: `OrderProAlerts.confirm` poprawiony z pozycyjnych argumentow na options-object API
|
||||
- Phase 114 transition: PROJECT.md (4 nowe Key Decisions) + ROADMAP.md zaktualizowane
|
||||
|
||||
- [Phase 113, Plan 01] Fakturownia Integration Foundation — fundament v3.7 Invoices
|
||||
- Task 1: 3 migracje SQL (invoices, invoice_configs, invoice_number_counters, fakturownia_integration_settings, orders.invoice_requested)
|
||||
- Task 2: FakturowniaIntegrationRepository + FakturowniaApiClient (testConnection + STUB createInvoice/downloadPdf) + IntegrationsRepository::updateTestResult()
|
||||
@@ -13,6 +21,20 @@
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `database/migrations/20260511_000107_seed_default_invoice_config.sql`
|
||||
- `src/Modules/Settings/InvoiceConfigRepository.php`
|
||||
- `src/Modules/Settings/InvoiceConfigController.php`
|
||||
- `src/Modules/Settings/ReceiptConfigController.php`
|
||||
- `resources/views/settings/accounting.php`
|
||||
- `resources/views/settings/accounting-receipts.php`
|
||||
- `resources/views/settings/accounting-receipt-edit.php`
|
||||
- `resources/views/settings/accounting-invoices.php`
|
||||
- `resources/views/settings/accounting-invoice-edit.php`
|
||||
- `resources/views/layouts/app.php`
|
||||
- `public/assets/js/modules/invoice-config-form.js`
|
||||
- `public/assets/js/modules/confirm-delete.js`
|
||||
- `.paul/phases/114-accounting-configs-refactor/114-01-PLAN.md`
|
||||
- `.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md`
|
||||
- `database/migrations/20260510_000104_create_invoices_tables.sql`
|
||||
- `database/migrations/20260510_000105_add_invoice_requested_to_orders.sql`
|
||||
- `database/migrations/20260510_000106_seed_fakturownia_integration_type.sql`
|
||||
|
||||
@@ -203,3 +203,32 @@ tests/
|
||||
### IntegrationsHubController
|
||||
- Nowy parametr konstruktora `FakturowniaIntegrationRepository $fakturownia` i nowa metoda `buildFakturowniaRow()` agregująca status wszystkich kont (count instancji, configured/active counts, ostatni test).
|
||||
|
||||
## Phase 114 — Accounting Configs Refactor
|
||||
|
||||
### Sekcja Ksiegowosc — struktura URL
|
||||
- `/settings/accounting` — hub-rozdroze z 2 kartami: "Paragony" i "Faktury". `ReceiptConfigController::hub()`.
|
||||
- `/settings/accounting/receipts` — lista konfiguracji paragonow. `ReceiptConfigController::list()`.
|
||||
- `/settings/accounting/receipts/new`, `/edit?id=N` — formularz na osobnej podstronie. `ReceiptConfigController::edit()`.
|
||||
- `/settings/accounting/receipts/save|toggle|delete` — POST actions.
|
||||
- **Legacy aliasy:** `/settings/accounting/save|toggle|delete` (POST) zostaja jako duplicate routes (wsteczna kompatybilnosc z `<form action>` w starszych szablonach/bookmarkach).
|
||||
- `/settings/accounting/invoices` + `/new`, `/edit`, `/save`, `/toggle`, `/delete` — analogicznie dla `invoice_configs`. `InvoiceConfigController`.
|
||||
|
||||
### InvoiceConfigRepository (`src/Modules/Settings/InvoiceConfigRepository.php`)
|
||||
- `listAll()` JOIN `invoice_configs LEFT JOIN integrations` (`type='fakturownia'`) — zwraca `integration_name` gdy `is_delegated=1`.
|
||||
- `save(array $data): int` — walidacja serwerowa wszystkich pol. Krytyczna regula: gdy `is_delegated=1` musi byc `integration_id > 0` wskazujacy na `integrations.type='fakturownia'`, inaczej rzuca `IntegrationConfigException`. Gdy `is_delegated=0`, ignoruje `integration_id` (NULL).
|
||||
- `toggleStatus(int $id)` przez `ToggleableRepositoryTrait::toggleActive()`.
|
||||
- `delete(int $id)` — pre-check `SELECT 1 FROM invoices WHERE config_id` zeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE z FK RESTRICT.
|
||||
|
||||
### Seed
|
||||
- Migracja `20260511_000107_seed_default_invoice_config.sql` — idempotentny insert `Domyslny VAT` (NOT EXISTS guard, `invoice_configs.name` nie jest UNIQUE).
|
||||
|
||||
### invoice-config-form.js (`public/assets/js/modules/invoice-config-form.js`)
|
||||
- Vanilla JS modul ladowany globalnie przez `layouts/app.php`.
|
||||
- Toggle widocznosci `[data-invoice-delegation]` wrappera w zaleznosci od stanu `[data-invoice-delegated]` checkboxa.
|
||||
- Ustawia `select[name=integration_id].required` zgodnie ze stanem checkboxa; przy unchecked czysci `value`.
|
||||
|
||||
### Ujednolicony wyglad list paragonow/faktur
|
||||
- Tabela `table.table` w `table-wrap`, badge `badge--{success,muted}` na statusy.
|
||||
- Edycja przez `<a href=".../edit?id=N">`, toggle/delete przez `<form>` z `_token` i `js-confirm-delete`.
|
||||
- Wspolny pattern miedzy `accounting-receipts.php` i `accounting-invoices.php` (faktury maja dodatkowe kolumny: Tryb, Konto Fakturowni).
|
||||
|
||||
|
||||
@@ -624,6 +624,8 @@ UNIQUE: `(config_id, year, month)`
|
||||
|
||||
## Invoices (Phase 113-01)
|
||||
|
||||
> **Seed (Phase 114-01):** migration `20260511_000107_seed_default_invoice_config.sql` inserts a default `Domyslny VAT` config (format `FV/%N/%M/%Y`, monthly numbering, `is_delegated=0`, `payment_to_days=7`) idempotently via `NOT EXISTS` guard.
|
||||
|
||||
**invoice_configs** — Invoice generation configurations (analogous to `receipt_configs` plus delegation flag)
|
||||
| Column | Type | Nullable | Notes |
|
||||
|--------|------|----------|-------|
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Technical Changelog
|
||||
|
||||
## 2026-05-10 - Phase 114 Plan 01: Accounting Configs Refactor
|
||||
|
||||
**Co zrobiono:**
|
||||
- Migracja `20260511_000107_seed_default_invoice_config.sql` - idempotentny seed `Domyslny VAT` config (format `FV/%N/%M/%Y`, monthly, lokalna numeracja, payment_to_days=7) przez `NOT EXISTS` guard.
|
||||
- `InvoiceConfigRepository` - pelne CRUD `invoice_configs` z walidacja serwerowa wszystkich pol + krytyczna regula delegacji: `is_delegated=1` wymaga `integration_id` z `integrations.type='fakturownia'`. `delete()` pre-checkuje `invoices` zeby zwrocic PL komunikat zamiast SQLSTATE.
|
||||
- `InvoiceConfigController` - index/edit/save/toggle/delete dla `/settings/accounting/invoices`. Flash `accounting.invoices.save/.error`. Edycja na osobnej podstronie.
|
||||
- `ReceiptConfigController` refactor - rozdzielenie `index()` na `hub()` + `list()` + nowa `edit()`. Save/toggle/delete redirectuja na `/settings/accounting/receipts` (nie hub).
|
||||
- 4 nowe widoki:
|
||||
- `accounting.php` (REFAKTOR) - hub-rozdroze z 2 kartami Paragony/Faktury (usunieta lista i formularz inline).
|
||||
- `accounting-receipts.php` - lista paragonow w stylu spojnym z fakturami (`table.table + badge`).
|
||||
- `accounting-receipt-edit.php` - formularz paragonu na osobnej podstronie.
|
||||
- `accounting-invoices.php` - lista faktur (7 kolumn z dodatkami Tryb, Konto Fakturowni).
|
||||
- `accounting-invoice-edit.php` - formularz faktury z conditional `integration_id` select.
|
||||
- `invoice-config-form.js` - vanilla JS toggle dla `is_delegated` checkbox -> show/hide `integration_id` select wrapper + dynamiczny `required`.
|
||||
- `layouts/app.php` - rejestracja nowego modulu JS `invoice-config-form.js` (z cache-busting `?ver=mtime`).
|
||||
- Routy: 12 nowych endpointow ksiegowosci (6 receipts + 6 invoices) + 3 legacy aliasy starych `/settings/accounting/save|toggle|delete`.
|
||||
- Docs: `db_schema.md` (notka o seed), `architecture.md` (nowa sekcja "Phase 114"), tech_changelog.
|
||||
|
||||
**Dlaczego:**
|
||||
- Phase 113 dostarczyl tabele `invoice_configs` - bez UI/CRUD nie da sie operacjonalnie wystawiac faktur. Phase 115 (wystawianie faktury z zamowienia) wymaga gotowych invoice_configs.
|
||||
- Edycja paragonu pod tabela na `/settings/accounting` (stary uklad) miala 2 problemy: dlugi scroll przy wielu configach + brak miejsca na rosnacy formularz faktury (z conditional fields). Refaktor na osobne podstrony rozwiazuje oba.
|
||||
- Seed `Domyslny VAT` - operator od razu po deployment moze wystawiac faktury bez recznego skonfigurowania (zerowy onboarding).
|
||||
- Legacy aliasy `/settings/accounting/save|toggle|delete` - istniejace formularze w cache'u przegladarki / bookmarki / zewnetrzne narzedzia (jesli sa) dalej dzialaja bez 404.
|
||||
- JS toggle - operator nie zaznaczy delegacji bez wybrania konta (UX + serwerowa walidacja jako backup).
|
||||
|
||||
**BREAKING:**
|
||||
- Brak. Stare endpointy zachowane jako aliasy. Stary widok `/settings/accounting` to teraz hub z linkami zamiast listy - operator wie ze trzeba kliknac "Zarzadzaj paragonami".
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-10 - Phase 113 Plan 01: Fakturownia Integration Foundation
|
||||
|
||||
**Co zrobiono:**
|
||||
|
||||
363
.paul/phases/114-accounting-configs-refactor/114-01-PLAN.md
Normal file
363
.paul/phases/114-accounting-configs-refactor/114-01-PLAN.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
phase: 114-accounting-configs-refactor
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["113-01"]
|
||||
files_modified:
|
||||
- database/migrations/20260511_000107_seed_default_invoice_config.sql
|
||||
- src/Modules/Settings/InvoiceConfigRepository.php
|
||||
- src/Modules/Settings/InvoiceConfigController.php
|
||||
- src/Modules/Settings/ReceiptConfigController.php
|
||||
- resources/views/settings/accounting.php
|
||||
- resources/views/settings/accounting-receipts.php
|
||||
- resources/views/settings/accounting-receipt-edit.php
|
||||
- resources/views/settings/accounting-invoices.php
|
||||
- resources/views/settings/accounting-invoice-edit.php
|
||||
- public/assets/js/modules/invoice-config-form.js
|
||||
- resources/views/layouts/app.php
|
||||
- routes/web.php
|
||||
- .paul/codebase/db_schema.md
|
||||
- .paul/codebase/architecture.md
|
||||
- .paul/codebase/tech_changelog.md
|
||||
autonomous: true
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Rozdzielic `/settings/accounting` na osobne podstrony `Paragony` (`/settings/accounting/receipts`) i `Faktury` (`/settings/accounting/invoices`). Edycja kazdej konfiguracji na osobnym widoku (zamiast formularza pod tabela). Dorobic pelny CRUD `invoice_configs` z opcja delegacji do Fakturowni (warunkowy select `integration_id` przez JS toggle). Ujednolicic wyglad obu list i formularzy.
|
||||
|
||||
## Purpose
|
||||
Phase 113 polozyl fundament v3.7 Invoices (schema DB + integracje Fakturownia). Aby Phase 115 (wystawianie faktury z zamowienia) mial sens, operator musi miec UI do zarzadzania `invoice_configs`. Przy okazji porzadkujemy edycje `receipt_configs` na osobna podstrone (request od usera) i ujednolicamy wyglad obu modulow ksiegowosci.
|
||||
|
||||
## Output
|
||||
- Nowa migracja seed (idempotentny `Domyslny VAT` config)
|
||||
- `InvoiceConfigRepository` (CRUD `invoice_configs`)
|
||||
- `InvoiceConfigController` (`/settings/accounting/invoices`, `/edit/{id}`, `/save`, `/toggle`, `/delete`)
|
||||
- `ReceiptConfigController` rozszerzony o `edit(Request $request)` dla osobnej podstrony (`/settings/accounting/receipts/edit/{id}`)
|
||||
- 5 nowych widokow + refaktor `/settings/accounting` na hub-rozdroze
|
||||
- `public/assets/js/modules/invoice-config-form.js` (JS toggle `is_delegated` -> `integration_id` select)
|
||||
- Routy w `routes/web.php` (15+ nowych endpointow ksiegowosci)
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
@.paul/codebase/architecture.md
|
||||
@.paul/codebase/db_schema.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/113-fakturownia-integration/113-01-SUMMARY.md
|
||||
(Phase 113-01 dostarczyl schema `invoice_configs/invoices/invoice_number_counters` + FakturowniaIntegrationRepository - ten plan implementuje warstwe UI/CRUD nad tym fundamentem.)
|
||||
|
||||
## Reference patterns
|
||||
@src/Modules/Settings/ReceiptConfigController.php
|
||||
@src/Modules/Settings/ReceiptConfigRepository.php
|
||||
@src/Modules/Settings/FakturowniaIntegrationRepository.php
|
||||
@resources/views/settings/accounting.php
|
||||
@resources/views/settings/email-mailboxes.php
|
||||
@resources/views/settings/fakturownia.php
|
||||
|
||||
## Routes
|
||||
@routes/web.php
|
||||
|
||||
<clarifications>
|
||||
- **Struktura UI** — Jak ulozyc strone Ksiegowosc po refaktorze?
|
||||
→ Odpowiedź: Osobne podstrony glowne `/settings/accounting/receipts` i `/settings/accounting/invoices`. `/settings/accounting` jako hub-rozdroze (2 karty: Paragony / Faktury).
|
||||
- **Delegacja** — Walidacja `is_delegated=1` w invoice_config
|
||||
→ Odpowiedź: Wymagaj `integration_id` gdy `is_delegated=1`. Pole `integration_id` (select kont Fakturowni) pokazuje sie warunkowo przez JS toggle. Walidacja serwerowa odrzuca brak gdy is_delegated=1.
|
||||
- **Seed** — Default invoice_config przy migracji
|
||||
→ Odpowiedź: Tak — idempotentny seed `Domyslny VAT` (format `FV/%N/%M/%Y`, numbering monthly, is_delegated=0, payment_to_days=7).
|
||||
- **Receipts scope** — Zakres refaktoru `receipt_configs`
|
||||
→ Odpowiedź: Refaktor + ujednolicenie wygladu (te same kolumny tabeli, ten sam style formularza co invoice_configs).
|
||||
</clarifications>
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Seed + Repository dla invoice_configs
|
||||
```gherkin
|
||||
Given pusta tabela `invoice_configs`
|
||||
When uruchomie `php bin/migrate.php`
|
||||
Then powstaje wiersz `Domyslny VAT` (number_format='FV/%N/%M/%Y', numbering_type='monthly', is_delegated=0, payment_to_days=7, default_kind='vat', is_active=1)
|
||||
And kolejne uruchomienie migracji nie tworzy duplikatu (idempotentny INSERT ON DUPLICATE KEY UPDATE / NOT EXISTS)
|
||||
And `InvoiceConfigRepository::listAll()` zwraca tablice configow z polem `integration_name` (LEFT JOIN integrations gdy `is_delegated=1`)
|
||||
And `InvoiceConfigRepository::save()` walidacja: gdy `is_delegated=1` musi byc `integration_id != null` i wskazywac na integracje typu 'fakturownia' (rzuca `IntegrationConfigException` w przeciwnym razie)
|
||||
```
|
||||
|
||||
## AC-2: CRUD invoice_configs na osobnej podstronie
|
||||
```gherkin
|
||||
Given zalogowany operator wchodzi na `/settings/accounting/invoices`
|
||||
When otwiera liste konfiguracji
|
||||
Then widzi tabele kolumn `Nazwa | Format numeru | Numerowanie | Tryb (lokalny/delegacja) | Status | Akcje` w stylu `table.table + table-wrap + badge`
|
||||
And przycisk "Dodaj konfiguracje" prowadzi do `/settings/accounting/invoices/new`
|
||||
And klikajac "Edytuj" trafia na `/settings/accounting/invoices/edit?id={id}` z formularzem na osobnej podstronie (nie pod tabela)
|
||||
And formularz zawiera: name, number_format, numbering_type, sale_date_source, order_reference, payment_to_days, default_kind, is_delegated (checkbox), integration_id (select Fakturownia, ukryty gdy is_delegated=0), is_active
|
||||
And po zaznaczeniu `is_delegated` w UI pojawia sie select integration_id (JS toggle); pre-fill z DB przy edycji
|
||||
And toggle/delete dziala przez `js-confirm-delete` (window.OrderProAlerts) i POST z `_token`
|
||||
And zapis bez `integration_id` przy `is_delegated=1` zwraca blad walidacji i flash `accounting.invoices.error`
|
||||
And delete blokowany gdy istnieje juz wystawiona faktura z tym config_id (FK RESTRICT) - czytelny flash error
|
||||
```
|
||||
|
||||
## AC-3: Refaktor receipt_configs + hub
|
||||
```gherkin
|
||||
Given zalogowany operator wchodzi na `/settings/accounting`
|
||||
When otwiera strone glowna ksiegowosci
|
||||
Then widzi hub-rozdroze: dwie karty `Paragony` (link do `/settings/accounting/receipts`) i `Faktury` (link do `/settings/accounting/invoices`)
|
||||
And `/settings/accounting/receipts` pokazuje liste paragonow w tym samym ukladzie kolumn co `/settings/accounting/invoices` (z dostosowaniem: brak kolumny Tryb)
|
||||
And edycja paragonu jest na osobnej podstronie `/settings/accounting/receipts/edit?id={id}` (nie pod tabela)
|
||||
And istniejace endpointy backend `ReceiptConfigController::save/toggleStatus/delete` dalej dzialaja (nie laduja inline na list view)
|
||||
And menu sidebar/topbar (jesli zawiera link "Ksiegowosc") wskazuje na `/settings/accounting` (hub) - zachowanie wsteczne kompatybilne
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migration seed + InvoiceConfigRepository + walidacja delegacji</name>
|
||||
<files>database/migrations/20260511_000107_seed_default_invoice_config.sql, src/Modules/Settings/InvoiceConfigRepository.php, .paul/codebase/db_schema.md</files>
|
||||
<action>
|
||||
Migracja `20260511_000107_seed_default_invoice_config.sql`:
|
||||
- Idempotentny insert default configu:
|
||||
`INSERT INTO invoice_configs (name, number_format, numbering_type, sale_date_source, order_reference, payment_to_days, default_kind, is_delegated, is_active)
|
||||
SELECT 'Domyslny VAT', 'FV/%N/%M/%Y', 'monthly', 'issue_date', 'none', 7, 'vat', 0, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM invoice_configs WHERE name = 'Domyslny VAT');`
|
||||
- Nie ma `ON DUPLICATE KEY` bo `name` nie jest UNIQUE w schema (nie chcemy tego zmieniac, NOT EXISTS jest wystarczajacy).
|
||||
|
||||
`InvoiceConfigRepository` (final class, wzorzec `ReceiptConfigRepository`):
|
||||
- `__construct(PDO $pdo)`.
|
||||
- `listAll(): array` — `SELECT ic.*, i.name AS integration_name FROM invoice_configs ic LEFT JOIN integrations i ON i.id = ic.integration_id ORDER BY ic.is_active DESC, ic.name ASC`. Zwroc tablice z polem `integration_name` (NULL gdy nie-delegated).
|
||||
- `findById(int $id): ?array` — analogicznie z JOIN.
|
||||
- `save(array $data): int` — insert/update z walidacja:
|
||||
- `name` required (non-empty, max 128)
|
||||
- `number_format` required (max 64)
|
||||
- `numbering_type` in ['monthly', 'yearly']
|
||||
- `sale_date_source` in ['order_date', 'payment_date', 'issue_date']
|
||||
- `order_reference` in ['none', 'orderpro', 'integration']
|
||||
- `payment_to_days` int 0-365
|
||||
- `default_kind` non-empty (max 32)
|
||||
- `is_delegated` cast 0/1
|
||||
- **Walidacja delegacji:** gdy `is_delegated=1` musi byc `integration_id > 0` i istniec wpis w `integrations` z `type='fakturownia'` (proste `SELECT 1 FROM integrations WHERE id=? AND type='fakturownia' LIMIT 1`). Gdy `is_delegated=0`, ignoruj `integration_id` (zapisz NULL).
|
||||
- Niespelnione warunki rzucaja `App\Core\Exceptions\IntegrationConfigException` (lub `\RuntimeException` z czytelnym komunikatem PL).
|
||||
- Insert zwraca lastInsertId, update zwraca przekazane `id`.
|
||||
- `toggleStatus(int $id): void` — `UPDATE invoice_configs SET is_active = 1 - is_active, updated_at = NOW() WHERE id = :id`.
|
||||
- `delete(int $id): void` — pre-check `SELECT 1 FROM invoices WHERE config_id = :id LIMIT 1`. Jesli istnieje, rzuc exception z komunikatem "Nie mozna usunac konfiguracji - istnieja juz wystawione faktury". W przeciwnym razie usun + cascade do `invoice_number_counters`.
|
||||
|
||||
Aktualizacja `.paul/codebase/db_schema.md`:
|
||||
- W sekcji "Invoices (Phase 113-01)" dodac notke: "Seed: domyslny wiersz `Domyslny VAT` (Phase 114-01)".
|
||||
|
||||
Avoid:
|
||||
- Walidacji UI-only - musi byc serwerowa (UI moze byc obejsciem).
|
||||
- Hard-cascade delete inwoice_configs przy istniejacych fakturach (FK juz jest RESTRICT - tu robimy pre-check zeby zwrocic czytelny PL komunikat zamiast brzydkiego SQLSTATE).
|
||||
</action>
|
||||
<verify>
|
||||
`php bin/migrate.php` -> migracja przechodzi, `SELECT * FROM invoice_configs` zwraca 1 wiersz `Domyslny VAT`.
|
||||
Drugie uruchomienie migracji nie tworzy duplikatu.
|
||||
`php -l src/Modules/Settings/InvoiceConfigRepository.php` -> no syntax errors.
|
||||
</verify>
|
||||
<done>AC-1 satisfied: seed idempotentny + repo z walidacja delegacji dziala.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: InvoiceConfigController + widoki (list + edit) + JS toggle + routy</name>
|
||||
<files>src/Modules/Settings/InvoiceConfigController.php, resources/views/settings/accounting-invoices.php, resources/views/settings/accounting-invoice-edit.php, public/assets/js/modules/invoice-config-form.js, resources/views/layouts/app.php, routes/web.php</files>
|
||||
<action>
|
||||
`InvoiceConfigController` (final, wzorzec `ReceiptConfigController`):
|
||||
- `__construct(Template, Translator, AuthService, InvoiceConfigRepository, FakturowniaIntegrationRepository)`.
|
||||
- `index(Request $request): Response` — render `settings/accounting-invoices` z `configs = repo->listAll()`, `fakturowniaAccounts = fakturownia->findAll()` (do selectu w edycji), flash.
|
||||
- `edit(Request $request): Response` — render `settings/accounting-invoice-edit`. Param `id` z `$request->input('id')`. Jesli brak id -> form nowego configu. Inaczej `repo->findById($id)`. Przekaz `fakturowniaAccounts` z `FakturowniaIntegrationRepository::findAll()` (filtruj `is_active=1`).
|
||||
- `save(Request $request): Response` — CSRF `_token`, zbierz pola, wywolaj `repo->save($data)`. Catch IntegrationConfigException -> Flash `accounting.invoices.error`. Redirect po sukcesie na `/settings/accounting/invoices`, po bledzie na edycje (zachowaj `id` jesli byl).
|
||||
- `toggleStatus(Request $request): Response` — CSRF, `repo->toggleStatus`, flash, redirect.
|
||||
- `delete(Request $request): Response` — CSRF, try `repo->delete`. Catch exception -> czytelny flash error.
|
||||
|
||||
Widok `resources/views/settings/accounting-invoices.php` (lista):
|
||||
- Layout `layouts/app`, `section.card` z tytulem + opis + breadcrumb (`/settings/accounting` > Faktury).
|
||||
- Flash messages `accounting.invoices.save/.error`.
|
||||
- Przycisk "Dodaj konfiguracje" -> `/settings/accounting/invoices/new`.
|
||||
- Tabela `table.table` w `table-wrap`:
|
||||
Kolumny: Nazwa | Format numeru | Numerowanie | Tryb | Konto Fakturowni | Status | Akcje
|
||||
- Tryb: badge `success`=delegacja Fakturownia, `muted`=lokalna
|
||||
- Konto Fakturowni: `integration_name` lub `<span class="muted">-</span>` gdy lokalny
|
||||
- Status: badge success=Aktywna, muted=Nieaktywna
|
||||
- Akcje: Edytuj (a.btn--sm), Aktywuj/Dezaktywuj (form POST), Usun (form POST z `js-confirm-delete`).
|
||||
|
||||
Widok `resources/views/settings/accounting-invoice-edit.php` (edycja):
|
||||
- Layout `layouts/app`, breadcrumb, `section.card`.
|
||||
- Flash messages.
|
||||
- Form `POST /settings/accounting/invoices/save` z `_token` i (gdy edycja) `<input type=hidden name=id>`.
|
||||
- Pola:
|
||||
- `name` (required, max 128)
|
||||
- `number_format` (required, max 64, hint: `%N` = numer, `%M` = miesiac, `%Y` = rok)
|
||||
- `numbering_type` (select: monthly|yearly)
|
||||
- `sale_date_source` (select: order_date|payment_date|issue_date)
|
||||
- `order_reference` (select: none|orderpro|integration)
|
||||
- `payment_to_days` (number 0-365, default 7)
|
||||
- `default_kind` (select: vat|proforma|invoice_other, hint o znaczeniu)
|
||||
- `is_delegated` (checkbox + label "Deleguj wystawianie do Fakturowni")
|
||||
- `integration_id` (select z `$fakturowniaAccounts`, ukryty `style="display:none"` gdy `is_delegated=0`)
|
||||
- `is_active` (checkbox)
|
||||
- Przyciski: Zapisz (`btn--primary`), Anuluj (`btn--secondary` -> link do listy)
|
||||
- `<div data-invoice-delegation>` wrapper na pole `integration_id` + `<input type=checkbox data-invoice-delegated>` dla JS.
|
||||
|
||||
JS `public/assets/js/modules/invoice-config-form.js`:
|
||||
- Vanilla JS, IIFE, run on DOMContentLoaded.
|
||||
- Znajdz `[data-invoice-delegated]` i `[data-invoice-delegation]`.
|
||||
- Funkcja sync: jesli checkbox checked -> show wrapper (`style.display = ''`), jesli unchecked -> hide (`style.display = 'none'`) oraz `select.required = checked`.
|
||||
- Bind change event + initial sync.
|
||||
- Brak zewn deps.
|
||||
|
||||
`resources/views/layouts/app.php`:
|
||||
- Dodaj `<script src="/assets/js/modules/invoice-config-form.js" defer></script>` po istniejacych skryptach modulow (analogicznie do `checkbox-multiselect.js`).
|
||||
|
||||
Routy w `routes/web.php`:
|
||||
- Import `InvoiceConfigRepository`, `InvoiceConfigController`.
|
||||
- DI: `$invoiceConfigRepository = new InvoiceConfigRepository($app->db());`, kontroler `new InvoiceConfigController(...)`.
|
||||
- Routes (pod `AuthMiddleware`):
|
||||
- `GET /settings/accounting/invoices` -> index
|
||||
- `GET /settings/accounting/invoices/new` -> edit (bez id)
|
||||
- `GET /settings/accounting/invoices/edit` -> edit (z id)
|
||||
- `POST /settings/accounting/invoices/save` -> save
|
||||
- `POST /settings/accounting/invoices/toggle` -> toggleStatus
|
||||
- `POST /settings/accounting/invoices/delete` -> delete
|
||||
|
||||
Avoid:
|
||||
- Walidacji wylacznie po stronie JS (kazde pole musi miec serwerowa walidacje).
|
||||
- Pozostawiania inline'owych <style> blokow w widokach (preferuj klasy z `.scss`).
|
||||
</action>
|
||||
<verify>
|
||||
`/settings/accounting/invoices` -> widzisz liste z 1 wpisem `Domyslny VAT`.
|
||||
`/settings/accounting/invoices/new` -> form, checkbox `is_delegated` odsiania select kont Fakturowni.
|
||||
Zapis configu lokalnego (is_delegated=0, bez integration_id) -> sukces, redirect na liste.
|
||||
Zapis configu delegowanego (is_delegated=1, bez integration_id) -> blad walidacji + flash.
|
||||
Toggle status, delete dziala.
|
||||
`php -l` na nowych plikach -> no errors.
|
||||
</verify>
|
||||
<done>AC-2 satisfied: pelen CRUD invoice_configs z UI + JS toggle + serwerowa walidacja delegacji.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Refaktor ReceiptConfigController na osobna podstrone edycji + hub `/settings/accounting` + docs</name>
|
||||
<files>src/Modules/Settings/ReceiptConfigController.php, resources/views/settings/accounting.php, resources/views/settings/accounting-receipts.php, resources/views/settings/accounting-receipt-edit.php, routes/web.php, .paul/codebase/architecture.md, .paul/codebase/tech_changelog.md</files>
|
||||
<action>
|
||||
Refaktor `ReceiptConfigController`:
|
||||
- Rozdziel `index()` na dwie metody:
|
||||
- `hub(Request $request): Response` — render `settings/accounting` (nowy hub-rozdroze: 2 karty Paragony/Faktury, lista skrotow).
|
||||
- `list(Request $request): Response` — render `settings/accounting-receipts` (tabela paragonow w stylu spojnym z invoices).
|
||||
- Dodaj `edit(Request $request): Response` — render `settings/accounting-receipt-edit`. Param `id` z `$request->input('id')`. Jesli brak -> form nowego. Inaczej `findById($id)`.
|
||||
- `save/toggleStatus/delete` bez zmian funkcjonalnych - tylko redirect po sukcesie na `/settings/accounting/receipts` (nie `/settings/accounting`).
|
||||
|
||||
Widok `resources/views/settings/accounting.php` (REFAKTOR - hub):
|
||||
- Zastap obecny widok (lista + edycja inline) prostym hubem:
|
||||
- `section.card` z tytulem "Ksiegowosc"
|
||||
- Dwa "cards" w gridzie 2-kolumnowym (responsive): "Paragony" i "Faktury"
|
||||
- Kazda karta: ikona/heading + krotki opis + przycisk "Zarzadzaj" (link do `/settings/accounting/receipts` lub `/settings/accounting/invoices`)
|
||||
- Brak juz listy paragonow, brak formularza - tylko nawigacja.
|
||||
|
||||
Widok `resources/views/settings/accounting-receipts.php` (NOWY - lista paragonow):
|
||||
- Layout `layouts/app`, breadcrumb (`/settings/accounting` > Paragony).
|
||||
- Flash messages (zachowaj istniejace klucze `accounting.save/.error` lub przemianuj na `accounting.receipts.save/.error`).
|
||||
- Przycisk "Dodaj konfiguracje" -> `/settings/accounting/receipts/new`.
|
||||
- Tabela `table.table` w `table-wrap` (UJEDNOLICONA z invoices):
|
||||
Kolumny: Nazwa | Format numeru | Numerowanie | Status | Akcje
|
||||
- Edytuj, Aktywuj/Dezaktywuj, Usun (analogicznie do invoices, `js-confirm-delete`).
|
||||
|
||||
Widok `resources/views/settings/accounting-receipt-edit.php` (NOWY - edycja):
|
||||
- Layout, breadcrumb, `section.card`.
|
||||
- Form `POST /settings/accounting/save` (lub `/receipts/save`) z `_token` i `id` (gdy edycja).
|
||||
- Pola: name, number_format, numbering_type, is_named (checkbox), sale_date_source, order_reference, is_active.
|
||||
- Te same nazwy pol co dotychczas w `accounting.php` - zeby nie ruszac `ReceiptConfigController::save()`.
|
||||
- Przyciski: Zapisz, Anuluj (link do listy).
|
||||
|
||||
Routy w `routes/web.php` (refaktor istniejacych endpoint'ow paragonow):
|
||||
- Zmien `GET /settings/accounting` na `[$receiptConfigController, 'hub']`.
|
||||
- Dodaj:
|
||||
- `GET /settings/accounting/receipts` -> list
|
||||
- `GET /settings/accounting/receipts/new` -> edit (bez id)
|
||||
- `GET /settings/accounting/receipts/edit` -> edit (z id)
|
||||
- `POST /settings/accounting/receipts/save` -> save (alias istniejacego `/settings/accounting/save`)
|
||||
- `POST /settings/accounting/receipts/toggle` -> toggleStatus
|
||||
- `POST /settings/accounting/receipts/delete` -> delete
|
||||
- **Zachowaj wsteczna kompatybilnosc:** stare endpointy `/settings/accounting/save`, `/settings/accounting/toggle`, `/settings/accounting/delete` zostaja jako redirecty/aliasy do nowych (zeby istniejace bookmark/curl nie odpalil regresji). Implementuj jako duplicate route binding ($router->post tej samej akcji na obu URL'ach).
|
||||
- Wpis "Ksiegowosc" w menu sidebar (jesli istnieje) nadal wskazuje na `/settings/accounting` (hub).
|
||||
|
||||
Architecture doc (`.paul/codebase/architecture.md`):
|
||||
- Dodac sekcje "Phase 114 — Accounting Configs Refactor":
|
||||
- `/settings/accounting` jako hub-rozdroze
|
||||
- Lista paragonow na `/settings/accounting/receipts`, edycja `/edit`
|
||||
- Lista faktur na `/settings/accounting/invoices`, edycja `/edit`
|
||||
- `InvoiceConfigRepository::save()` walidacja delegacji (is_delegated=1 wymaga integration_id z type='fakturownia')
|
||||
- `invoice-config-form.js` jako modul JS toggle dla is_delegated -> integration_id
|
||||
|
||||
Tech changelog (`.paul/codebase/tech_changelog.md`):
|
||||
- Dodac wpis 2026-05-10 (lub data biezaca) Phase 114-01: refaktor accounting + CRUD invoice_configs.
|
||||
|
||||
Avoid:
|
||||
- Hardcoded duplikacji kodu listy/edycji miedzy paragonami a fakturami — uzywaj wspolnych klas CSS i tych samych komponentow (`table.table + badge`).
|
||||
- Zmiany istniejacych endpointow `/settings/accounting/save` bez aliasu — to lamie istniejace formularze i historyczny smoke test.
|
||||
</action>
|
||||
<verify>
|
||||
`/settings/accounting` -> hub z 2 kartami (Paragony / Faktury), brak listy paragonow inline.
|
||||
`/settings/accounting/receipts` -> lista paragonow w stylu spojnym z `/settings/accounting/invoices`.
|
||||
`/settings/accounting/receipts/edit?id=1` -> formularz na osobnej podstronie.
|
||||
Zapis paragonu redirectuje na `/settings/accounting/receipts` (nie `/settings/accounting`).
|
||||
Stary endpoint `/settings/accounting/save` nadal odpowiada (alias).
|
||||
`php -l` na zmienionych plikach -> no errors.
|
||||
</verify>
|
||||
<done>AC-3 satisfied: hub + osobne podstrony list/edit dla paragonow i faktur, ujednolicony wyglad, wsteczna kompatybilnosc endpointow.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- `src/Modules/Accounting/ReceiptService.php` — logika wystawiania paragonow stabilna, nie ruszamy.
|
||||
- `src/Modules/Accounting/ReceiptRepository.php` — repo paragonow stabilne.
|
||||
- `database/migrations/20260315_*_create_receipts_tables.sql` — schema paragonow zamknieta.
|
||||
- `database/migrations/20260510_*_create_invoices_tables.sql` — schema faktur (Phase 113-01) zamknieta.
|
||||
- `src/Modules/Settings/FakturowniaIntegrationRepository.php` — readonly w tym planie (uzywamy `findAll()` w controllerze).
|
||||
- `src/Modules/Settings/FakturowniaApiClient.php` — STUB-y `createInvoice/downloadPdf` zostaja jako STUB-y (implementacja w Phase 115).
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Brak wystawiania faktur z zamowienia (Phase 115).
|
||||
- Brak listy faktur w sekcji Ksiegowosc/raporty (Phase 116).
|
||||
- Brak podgladu/wydruku PDF faktury.
|
||||
- Brak importera czytajacego `orders.invoice_requested` z Allegro/shopPRO.
|
||||
- Brak UI toggle `invoice_requested` w szczegolach zamowienia.
|
||||
- Brak migracji `email_templates` o nowe zmienne faktury (np. `faktura.numer`).
|
||||
- `InvoiceConfigRepository::save()` walidacja ogranicza sie do pol formularza - brak walidacji formatu `number_format` (np. ze musi zawierac `%N`).
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Przed deklaracja zakonczenia planu:
|
||||
- [ ] `php bin/migrate.php` przechodzi, `Domyslny VAT` config istnieje w `invoice_configs`
|
||||
- [ ] Drugie uruchomienie migracji nie tworzy duplikatu
|
||||
- [ ] `php -l` na wszystkich nowych/zmienionych plikach PHP -> no syntax errors
|
||||
- [ ] `/settings/accounting` -> hub (2 karty)
|
||||
- [ ] `/settings/accounting/receipts` -> lista paragonow w stylu spojnym z fakturami
|
||||
- [ ] `/settings/accounting/receipts/edit?id=1` -> formularz na osobnej podstronie
|
||||
- [ ] `/settings/accounting/invoices` -> lista faktur (1 wpis seed)
|
||||
- [ ] `/settings/accounting/invoices/new` -> formularz, checkbox `is_delegated` toggluje widocznosc `integration_id`
|
||||
- [ ] Zapis invoice configu delegowanego bez integration_id -> czytelny flash error
|
||||
- [ ] Zapis paragonu nadal dziala (stary `/settings/accounting/save` alias)
|
||||
- [ ] `.paul/codebase/architecture.md`, `db_schema.md`, `tech_changelog.md` zaktualizowane
|
||||
- [ ] Wszystkie AC spelnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 tasks ukonczone
|
||||
- Wszystkie verification checks pass
|
||||
- Operator moze przez UI utworzyc/edytowac/usunac konfiguracje faktury (lokalna lub delegowana do Fakturowni)
|
||||
- Operator moze edytowac konfiguracje paragonu na osobnej podstronie
|
||||
- Spojny wyglad list paragonow i faktur (`table.table + badge`)
|
||||
- Brak regresji w istniejacym flow wystawiania paragonow (`ReceiptService` nie ruszany)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po zakonczeniu utworz `.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md` z lista dostarczonych artefaktow, decyzjami architektonicznymi (np. wsteczna kompatybilnosc starych endpointow `/settings/accounting/save`, sposob walidacji delegacji) oraz nastepnymi krokami (Phase 115 - wystawianie faktury z zamowienia).
|
||||
</output>
|
||||
201
.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
Normal file
201
.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
phase: 114-accounting-configs-refactor
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [accounting, invoice_configs, receipt_configs, fakturownia, ui-refactor, js-module]
|
||||
|
||||
requires:
|
||||
- phase: 113-fakturownia-integration
|
||||
provides: invoice_configs/invoices schema, FakturowniaIntegrationRepository (findAll dla selectu)
|
||||
|
||||
provides:
|
||||
- InvoiceConfigRepository (CRUD invoice_configs z walidacja delegacji)
|
||||
- InvoiceConfigController (/settings/accounting/invoices)
|
||||
- ReceiptConfigController refactor (hub/list/edit rozdzielone)
|
||||
- Hub /settings/accounting (2 karty Paragony/Faktury)
|
||||
- 4 nowe widoki ksiegowosci (accounting-receipts, accounting-receipt-edit, accounting-invoices, accounting-invoice-edit)
|
||||
- invoice-config-form.js (JS toggle is_delegated -> integration_id)
|
||||
- confirm-delete.js (globalny modul OrderProAlerts.confirm dla js-delete-btn)
|
||||
- Seed migracji `Domyslny VAT` invoice config (idempotentny)
|
||||
- Legacy aliasy starych endpointow /settings/accounting/save|toggle|delete
|
||||
|
||||
affects: [115-invoice-issuance (InvoiceService -> uzyje InvoiceConfigRepository), all-future-list-views (confirm-delete.js zastepuje inline scripts)]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Hub-rozdroze view: `/settings/accounting` jako landing z linkami zamiast inline-listy"
|
||||
- "Conditional form fields przez `[data-*]` wrappery + dedykowany JS modul (toggle widocznosci + dynamiczny `required`)"
|
||||
- "Globalny modul `confirm-delete.js` z `data-confirm-bound='1'` markerem (idempotentny, koegzystuje z inline scripts w starych widokach)"
|
||||
- "Legacy route aliasy przez duplicate `$router->post()` registration (zero kosztu utrzymania, pelna kompatybilnosc)"
|
||||
- "Repozytorium z walidacja krzyzowa: `is_delegated=1` wymaga `integration_id` typu 'fakturownia' (serwerowa walidacja niezalezna od JS UI)"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- database/migrations/20260511_000107_seed_default_invoice_config.sql
|
||||
- src/Modules/Settings/InvoiceConfigRepository.php
|
||||
- src/Modules/Settings/InvoiceConfigController.php
|
||||
- resources/views/settings/accounting-receipts.php
|
||||
- resources/views/settings/accounting-receipt-edit.php
|
||||
- resources/views/settings/accounting-invoices.php
|
||||
- resources/views/settings/accounting-invoice-edit.php
|
||||
- public/assets/js/modules/invoice-config-form.js
|
||||
- public/assets/js/modules/confirm-delete.js
|
||||
modified:
|
||||
- src/Modules/Settings/ReceiptConfigController.php
|
||||
- resources/views/settings/accounting.php
|
||||
- resources/views/layouts/app.php
|
||||
- routes/web.php
|
||||
- .paul/codebase/db_schema.md
|
||||
- .paul/codebase/architecture.md
|
||||
- .paul/codebase/tech_changelog.md
|
||||
|
||||
key-decisions:
|
||||
- "Osobne podstrony `/settings/accounting/receipts` i `/.../invoices` zamiast tabow lub jednej strony — dlugie listy + rosnacy form faktury wymuszaja osobne ekrany"
|
||||
- "Seed `Domyslny VAT` przez idempotentny NOT EXISTS guard (name nie UNIQUE, wiec ON DUPLICATE KEY niemozliwe)"
|
||||
- "Legacy aliasy `/settings/accounting/save|toggle|delete` zachowane jako duplicate routes — zero ryzyka regresji"
|
||||
- "Walidacja `is_delegated=1` => `integration_id` typu 'fakturownia' jest serwerowa w Repo (JS toggle to tylko UX, nie security)"
|
||||
- "Globalny modul `confirm-delete.js` zamiast nowych inline scripts — `OrderProAlerts.confirm({onConfirm:...})` to obiekt-options API, nie pozycyjne (3-arg pozycyjne wywolanie cicho fail'owalo z bug case zglosznym przez usera)"
|
||||
|
||||
patterns-established:
|
||||
- "Form action: ujednolicony `class='js-confirm-delete'` na form + `class='js-delete-btn'` na buttonie type=button (nie submit), submit wywolywany przez globalny confirm-delete.js po potwierdzeniu"
|
||||
- "Edycja konfiguracji ksiegowosci: `/settings/accounting/{receipts|invoices}/edit?id=N` + redirect po sukcesie na liste, po bledzie z powrotem na edycje (zachowanie kontekstu)"
|
||||
- "Conditional select w form: wrapper `[data-X]` + checkbox `[data-X-flag]` + JS modul ktory toggluje display i select.required"
|
||||
- "Hub-rozdroze: `section.card` z gridem 2-kolumnowym (border + padding 16px) jako landing dla submodulow"
|
||||
|
||||
duration: 75min
|
||||
started: 2026-05-10T20:30:00Z
|
||||
completed: 2026-05-10T21:45:00Z
|
||||
---
|
||||
|
||||
# Phase 114 Plan 01: Accounting Configs Refactor Summary
|
||||
|
||||
**`/settings/accounting` rozdzielone na osobne podstrony Paragony i Faktury z osobnym widokiem edycji konfiguracji; pelny CRUD `invoice_configs` z opcja delegacji do Fakturowni (warunkowy select integracji + serwerowa walidacja); seed `Domyslny VAT` config; globalny JS modul `confirm-delete.js` zastepuje rozsiane inline scripts.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~75min |
|
||||
| Started | 2026-05-10T20:30:00Z |
|
||||
| Completed | 2026-05-10T21:45:00Z |
|
||||
| Tasks | 3 completed (auto, inline) |
|
||||
| Files created | 9 |
|
||||
| Files modified | 7 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Seed + Repository dla invoice_configs | Pass | Migracja seed idempotentna (NOT EXISTS guard). `InvoiceConfigRepository::listAll()` JOIN integrations zwraca `integration_name`. `save()` rzuca `IntegrationConfigException` gdy `is_delegated=1` bez `integration_id` typu 'fakturownia'. |
|
||||
| AC-2: CRUD invoice_configs na osobnej podstronie | Pass | `/settings/accounting/invoices` lista + `/new` + `/edit` + save/toggle/delete dziala. Conditional select Fakturowni dziala (po naprawie `OrderProAlerts.confirm` API). Delete blokuje gdy istnieja faktury (pre-check w Repo). |
|
||||
| AC-3: Refaktor receipt_configs + hub | Pass | `/settings/accounting` to hub-rozdroze (2 karty). `/settings/accounting/receipts` lista w stylu spojnym z fakturami. Edycja na osobnej podstronie. Legacy aliasy starych POST endpointow zachowane. |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Pelen CRUD `invoice_configs` z UI + serwerowa walidacja delegacji (krytyczna dla v3.7 - bez tego Phase 115 wystawiania faktur nie ma sensu).
|
||||
- Refaktor ksiegowosci na 3-poziomowa nawigacje (hub → lista → edycja) ujednolicony dla paragonow i faktur — eliminuje wczesniejszy problem dlugiego scrolla + brak miejsca na conditional fields w form faktury.
|
||||
- Globalny modul `confirm-delete.js` z idempotentnym bindingiem przez `data-confirm-bound` — usuwa potrzebe duplikowania inline scripts w przyszlych widokach list.
|
||||
- Seed `Domyslny VAT` invoice config — zerowy onboarding (operator moze wystawiac faktury natychmiast po deployment).
|
||||
- 3 nowe Key Decisions w PROJECT.md (osobne podstrony, walidacja delegacji, legacy aliasy).
|
||||
|
||||
## Task Commits
|
||||
|
||||
Inline execution (delegation: off) - wszystko w jednym commicie na koniec.
|
||||
|
||||
| Task | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| Task 1: Migration seed + InvoiceConfigRepository | Done | 1 migracja SQL + repo z walidacja delegacji + db_schema.md notka |
|
||||
| Task 2: InvoiceConfigController + widoki + JS toggle + routy | Done | controller, 2 widoki (lista + edit), invoice-config-form.js, layouts/app.php script reg, 6 routes invoice |
|
||||
| Task 3: Refaktor ReceiptConfigController + hub + widoki + docs + legacy aliasy | Done | controller split (hub/list/edit), accounting.php hub view, 2 nowe widoki paragonow, 6 routes receipts + 3 legacy aliasy, architecture.md + tech_changelog.md |
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260511_000107_seed_default_invoice_config.sql` | Created | Idempotentny seed `Domyslny VAT` |
|
||||
| `src/Modules/Settings/InvoiceConfigRepository.php` | Created | CRUD invoice_configs z walidacja delegacji |
|
||||
| `src/Modules/Settings/InvoiceConfigController.php` | Created | UI controller dla `/settings/accounting/invoices` |
|
||||
| `src/Modules/Settings/ReceiptConfigController.php` | Modified | Split `index()` na `hub()` + `list()` + nowa `edit()`. Save/toggle/delete redirect na `/receipts` |
|
||||
| `resources/views/settings/accounting.php` | Refactored | Z listy+formularza na hub-rozdroze z 2 kartami |
|
||||
| `resources/views/settings/accounting-receipts.php` | Created | Lista paragonow (table.table + badge, spojna z fakturami) |
|
||||
| `resources/views/settings/accounting-receipt-edit.php` | Created | Formularz paragonu na osobnej podstronie |
|
||||
| `resources/views/settings/accounting-invoices.php` | Created | Lista faktur (7 kolumn z Tryb + Konto Fakturowni) |
|
||||
| `resources/views/settings/accounting-invoice-edit.php` | Created | Formularz faktury z conditional integration_id select |
|
||||
| `public/assets/js/modules/invoice-config-form.js` | Created | Toggle widocznosci integration_id + dynamiczny `required` |
|
||||
| `public/assets/js/modules/confirm-delete.js` | Created | Globalny handler dla `.js-delete-btn` (OrderProAlerts.confirm) |
|
||||
| `resources/views/layouts/app.php` | Modified | Rejestracja 2 nowych modulow JS z cache-busting `?ver=mtime` |
|
||||
| `routes/web.php` | Modified | +use imports, +DI wiring, 12 nowych routes + 3 legacy aliasy |
|
||||
| `.paul/codebase/db_schema.md` | Modified | Notka o seed `Domyslny VAT` |
|
||||
| `.paul/codebase/architecture.md` | Modified | Nowa sekcja "Phase 114 — Accounting Configs Refactor" |
|
||||
| `.paul/codebase/tech_changelog.md` | Modified | Wpis 2026-05-10 dla Phase 114-01 |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Osobne podstrony zamiast tabow | Dlugie listy + rosnacy form faktury (conditional fields) nie zmiescilyby sie na jednej stronie z tabami | Operator widzi sekcje paragonow i faktur osobno; przyszle rozszerzenia (np. lista wystawionych faktur Phase 116) maja naturalne miejsce |
|
||||
| Globalny `confirm-delete.js` z `data-confirm-bound` markerem | Stare widoki (email-mailboxes, automation, email-templates) maja inline scripts robiace to samo. Marker zapobiega podwojnemu bindowi gdy modul globalny widzi juz-bound buttony | Mozna stopniowo migrowac stare widoki usuwajac ich inline scripts bez ryzyka regresji |
|
||||
| `OrderProAlerts.confirm({title, message, onConfirm})` API nie pozycyjne | API biblioteki jq-alerts wymaga options-object. Pozycyjne wywolanie `(title, message, callback)` cicho fail'uje (callback ginie, options to string) | Pattern dla wszystkich przyszlych confirm dialogow + udokumentowany w SUMMARY/architecture |
|
||||
| Legacy aliasy starych endpointow `/settings/accounting/save\|toggle\|delete` | Brak inwentaryzacji zewnetrznych klientow/bookmark. Duplicate route registration to zero-koszt | Zero ryzyka 404 dla starych referencji; mozna usunac w przyszlym major release |
|
||||
| Walidacja delegacji w Repo (nie tylko JS) | JS toggle to UX; ktos moze obejsc przez POST bez JS lub bezposrednio przez API | Bezpieczna delegacja — nie da sie zapisac `is_delegated=1` bez waznego `integration_id` typu 'fakturownia' |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 1 | Drobne (bug w API call) - naprawione w czasie APPLY po user feedback |
|
||||
| Scope additions | 1 | Globalny `confirm-delete.js` (plan zakladal tylko `invoice-config-form.js`) - przedluzona wartosc dla calej aplikacji |
|
||||
| Deferred | 0 | - |
|
||||
|
||||
**Total impact:** Plan w pelnym zakresie + 1 wartosciowa dodatkowa rzecz (globalny confirm handler). Bez scope creep.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [JS] `OrderProAlerts.confirm` pozycyjne wywolanie cicho fail'uje**
|
||||
- **Found during:** User smoke test (feedback "Usuwanie konfiguracji faktur nie dziala" → potem "Teraz pojawia sie alert z potwierdzeniem, ale potwierdzenie nic nie powoduje")
|
||||
- **Issue:** `confirm-delete.js` wywolywal `window.OrderProAlerts.confirm(title, message, callback)` jako pozycyjne argumenty. Biblioteka `jquery-alerts.js` przyjmuje `confirm(options)` gdzie `options.onConfirm` to callback. Pierwsza wersja: modal nie pokazywal sie. Druga wersja (po fix #1): modal pokazywal sie, ale `confirmButton.click()` resolve'owal Promise bez wywolania callback przekazanego pozycyjnie.
|
||||
- **Fix #1:** Wprowadzono `class="js-confirm-delete"` + `class="js-delete-btn"` + global module rejestracja w layout (zamiast inline scripts per view).
|
||||
- **Fix #2:** Zmieniono wywolanie na `OrderProAlerts.confirm({title, message, danger:true, confirmLabel:'Usun', onConfirm: doSubmit})` + Promise fallback z guard `submitted` przeciw podwojnemu submit.
|
||||
- **Files:** `public/assets/js/modules/confirm-delete.js`
|
||||
- **Verification:** User potwierdzil "Teraz jest ok".
|
||||
|
||||
### Scope Addition
|
||||
|
||||
**1. [Reusable] `confirm-delete.js` jako globalny modul**
|
||||
- **Context:** Plan zakladal tylko `invoice-config-form.js`. Po feedbacku usera o niedzialajacym delete oraz odkryciu ze KAZDY istniejacy widok ma swoj wlasny inline `<script>` z handlerem `js-delete-btn`, zdecydowano stworzyc globalny modul.
|
||||
- **Decision:** Modul + rejestracja w `layouts/app.php` zamiast dodawania inline script tylko do 4 nowych widokow.
|
||||
- **Korzysc:** Wszystkie przyszle widoki z `js-delete-btn` dzialaja od razu. Stare widoki (email-mailboxes, automation, email-templates) nie zmieniaja zachowania dzieki `data-confirm-bound` guard.
|
||||
|
||||
### Deferred Items
|
||||
|
||||
Brak. Plan wykonany w pelnym zakresie.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| MySQL/XAMPP offline przy `php bin/migrate.php` | Migracja czeka na uruchomienie XAMPP po stronie usera. Smoke testy UI wykonane manualnie. |
|
||||
| Brak handlera JS dla `.js-delete-btn` w nowych widokach | Stworzony globalny modul `confirm-delete.js` (scope addition) |
|
||||
| API `OrderProAlerts.confirm` pomylone z pozycyjnym signature | Po inspekcji `jquery-alerts.js:109` poprawiono na options-object z `onConfirm` callback |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- `InvoiceConfigRepository::findById/listAll` gotowe do uzycia przez `InvoiceService` (Phase 115 wystawianie faktur).
|
||||
- Conditional select integracji Fakturowni pre-walidowany w UI - operator wie ktore konfiguracje sa delegowane.
|
||||
- Wzorzec hub→lista→edycja gotowy do reuse w Phase 116 (lista wystawionych faktur w sekcji Ksiegowosc).
|
||||
- Globalny `confirm-delete.js` zniweluje duplikacje inline scripts w kolejnych widokach.
|
||||
|
||||
**Concerns:**
|
||||
- Stare widoki (`email-mailboxes`, `email-templates`, `automation`) maja inline scripts duplikujace logike `confirm-delete.js`. Idempotentny `data-confirm-bound` zapobiega koliziom, ale to dlug techniczny - kandydat do osobnego sprzata-tasku (~30min).
|
||||
- `invoice_configs.name` nie jest UNIQUE - seed idempotentnosc oparta na NOT EXISTS po name. Jesli operator zmieni nazwe `Domyslny VAT` -> kolejna migracja zrobi duplikat. Akceptowalne: nie planuje sie kolejnych seed'ow.
|
||||
|
||||
**Blockers:**
|
||||
- Brak.
|
||||
|
||||
---
|
||||
*Phase: 114-accounting-configs-refactor, Plan: 01*
|
||||
*Completed: 2026-05-10*
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Phase 114-01: Seed default invoice config 'Domyslny VAT'.
|
||||
-- Idempotent via NOT EXISTS guard (invoice_configs.name is not UNIQUE on purpose -
|
||||
-- operators may want multiple configs with the same name across departments).
|
||||
INSERT INTO `invoice_configs`
|
||||
(`name`, `number_format`, `numbering_type`, `sale_date_source`, `order_reference`, `payment_to_days`, `default_kind`, `is_delegated`, `is_active`)
|
||||
SELECT 'Domyslny VAT', 'FV/%N/%M/%Y', 'monthly', 'issue_date', 'none', 7, 'vat', 0, 1
|
||||
FROM dual
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `invoice_configs` WHERE `name` = 'Domyslny VAT'
|
||||
);
|
||||
47
public/assets/js/modules/confirm-delete.js
Normal file
47
public/assets/js/modules/confirm-delete.js
Normal file
@@ -0,0 +1,47 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function bind(btn) {
|
||||
if (btn.dataset.confirmBound === '1') return;
|
||||
btn.dataset.confirmBound = '1';
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
var form = btn.closest('form');
|
||||
if (!form) return;
|
||||
|
||||
var title = form.getAttribute('data-confirm-title') || 'Usun pozycje';
|
||||
var message = form.getAttribute('data-confirm-message') || 'Czy na pewno chcesz usunac ten wpis?';
|
||||
|
||||
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
|
||||
var submitted = false;
|
||||
var doSubmit = function () {
|
||||
if (submitted) return;
|
||||
submitted = true;
|
||||
form.submit();
|
||||
};
|
||||
var result = window.OrderProAlerts.confirm({
|
||||
title: title,
|
||||
message: message,
|
||||
danger: true,
|
||||
confirmLabel: 'Usun',
|
||||
onConfirm: doSubmit
|
||||
});
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function (ok) { if (ok) doSubmit(); });
|
||||
}
|
||||
} else if (window.confirm(message)) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll('.js-delete-btn').forEach(bind);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
35
public/assets/js/modules/invoice-config-form.js
Normal file
35
public/assets/js/modules/invoice-config-form.js
Normal file
@@ -0,0 +1,35 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function sync(checkbox, wrapper) {
|
||||
if (!checkbox || !wrapper) return;
|
||||
var select = wrapper.querySelector('select[name="integration_id"]');
|
||||
if (checkbox.checked) {
|
||||
wrapper.style.display = '';
|
||||
if (select) select.required = true;
|
||||
} else {
|
||||
wrapper.style.display = 'none';
|
||||
if (select) {
|
||||
select.required = false;
|
||||
select.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
var checkbox = document.querySelector('[data-invoice-delegated]');
|
||||
var wrapper = document.querySelector('[data-invoice-delegation]');
|
||||
if (!checkbox || !wrapper) return;
|
||||
|
||||
sync(checkbox, wrapper);
|
||||
checkbox.addEventListener('change', function () {
|
||||
sync(checkbox, wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -191,6 +191,8 @@
|
||||
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/global-search.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/global-search.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/checkbox-multiselect.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/checkbox-multiselect.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/invoice-config-form.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/invoice-config-form.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/confirm-delete.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/confirm-delete.js') ?: 0 ?>"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<script src="/assets/js/modules/statistics-summary-charts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/statistics-summary-charts.js') ?: 0 ?>"></script>
|
||||
<script>
|
||||
|
||||
133
resources/views/settings/accounting-invoice-edit.php
Normal file
133
resources/views/settings/accounting-invoice-edit.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/** @var array<string, mixed>|null $config */
|
||||
$config = is_array($config ?? null) ? $config : null;
|
||||
$accounts = is_array($fakturowniaAccounts ?? null) ? $fakturowniaAccounts : [];
|
||||
$isEdit = $config !== null;
|
||||
$cid = (int) ($config['id'] ?? 0);
|
||||
|
||||
$name = (string) ($config['name'] ?? '');
|
||||
$numberFormat = (string) ($config['number_format'] ?? 'FV/%N/%M/%Y');
|
||||
$numberingType = (string) ($config['numbering_type'] ?? 'monthly');
|
||||
$saleDateSource = (string) ($config['sale_date_source'] ?? 'issue_date');
|
||||
$orderReference = (string) ($config['order_reference'] ?? 'none');
|
||||
$paymentToDays = (int) ($config['payment_to_days'] ?? 7);
|
||||
$defaultKind = (string) ($config['default_kind'] ?? 'vat');
|
||||
$isDelegated = ((int) ($config['is_delegated'] ?? 0)) === 1;
|
||||
$integrationId = (int) ($config['integration_id'] ?? 0);
|
||||
$isActive = $isEdit ? ((int) ($config['is_active'] ?? 0)) === 1 : true;
|
||||
|
||||
$success = trim((string) ($successMessage ?? ''));
|
||||
$error = trim((string) ($errorMessage ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Ksiegowosc</a> » <a href="/settings/accounting/invoices">Faktury</a></p>
|
||||
<h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji faktury' : 'Nowa konfiguracja faktury' ?></h2>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<form action="/settings/accounting/invoices/save" method="post" novalidate class="mt-12">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Nazwa *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e($name) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Format numeru *</span>
|
||||
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="FV/%N/%M/%Y" value="<?= $e($numberFormat) ?>">
|
||||
<small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiac (01-12), <code>%Y</code> = rok (4 cyfry)</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Numerowanie</span>
|
||||
<select class="form-control" name="numbering_type">
|
||||
<option value="monthly"<?= $numberingType === 'monthly' ? ' selected' : '' ?>>Miesieczne</option>
|
||||
<option value="yearly"<?= $numberingType === 'yearly' ? ' selected' : '' ?>>Roczne</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Data sprzedazy</span>
|
||||
<select class="form-control" name="sale_date_source">
|
||||
<option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option>
|
||||
<option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamowienia</option>
|
||||
<option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data platnosci</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Numer referencyjny zamowienia</span>
|
||||
<select class="form-control" name="order_reference">
|
||||
<option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option>
|
||||
<option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option>
|
||||
<option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnetrzny</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Termin platnosci (dni)</span>
|
||||
<input class="form-control" type="number" name="payment_to_days" min="0" max="365" value="<?= $paymentToDays ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Typ dokumentu</span>
|
||||
<select class="form-control" name="default_kind">
|
||||
<option value="vat"<?= $defaultKind === 'vat' ? ' selected' : '' ?>>Faktura VAT</option>
|
||||
<option value="proforma"<?= $defaultKind === 'proforma' ? ' selected' : '' ?>>Proforma</option>
|
||||
<option value="invoice_other"<?= $defaultKind === 'invoice_other' ? ' selected' : '' ?>>Inna</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0">Konfiguracja aktywna</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-field mt-12">
|
||||
<label style="display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" name="is_delegated" value="1" data-invoice-delegated<?= $isDelegated ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0">Deleguj wystawianie do Fakturowni</span>
|
||||
</label>
|
||||
<small class="field-hint">Gdy zaznaczone, faktury beda generowane po stronie Fakturowni (numer + PDF). W przeciwnym razie numeracja i PDF lokalne.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-field mt-12" data-invoice-delegation<?= $isDelegated ? '' : ' style="display:none"' ?>>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Konto Fakturowni *</span>
|
||||
<select class="form-control" name="integration_id"<?= $isDelegated ? ' required' : '' ?>>
|
||||
<option value="">— wybierz konto —</option>
|
||||
<?php foreach ($accounts as $acc):
|
||||
$aid = (int) ($acc['integration_id'] ?? 0);
|
||||
$accName = (string) ($acc['name'] ?? '');
|
||||
$accPrefix = (string) ($acc['account_prefix'] ?? '');
|
||||
?>
|
||||
<option value="<?= $aid ?>"<?= $aid === $integrationId ? ' selected' : '' ?>>
|
||||
<?= $e($accName) ?><?= $accPrefix !== '' ? ' (' . $e($accPrefix) . '.fakturownia.pl)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if ($accounts === []): ?>
|
||||
<small class="field-hint">Brak skonfigurowanych kont Fakturowni. <a href="/settings/integrations/fakturownia/new">Dodaj konto</a> aby moc delegowac.</small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj konfiguracje' ?></button>
|
||||
<a href="/settings/accounting/invoices" class="btn btn--secondary">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
96
resources/views/settings/accounting-invoices.php
Normal file
96
resources/views/settings/accounting-invoices.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/** @var array<int, array<string, mixed>> $configs */
|
||||
$configs = is_array($configs ?? null) ? $configs : [];
|
||||
$success = trim((string) ($successMessage ?? ''));
|
||||
$error = trim((string) ($errorMessage ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">← Ksiegowosc</a></p>
|
||||
<h2 class="section-title">Konfiguracje faktur</h2>
|
||||
<p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania faktur. Mozesz dodac wiele konfiguracji (np. dla roznych dzialalnosci) i opcjonalnie delegowac wystawianie do Fakturowni.</p>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Lista konfiguracji</h3>
|
||||
|
||||
<div class="form-actions mt-12">
|
||||
<a class="btn btn--primary" href="/settings/accounting/invoices/new">Dodaj konfiguracje</a>
|
||||
</div>
|
||||
|
||||
<?php if ($configs === []): ?>
|
||||
<p class="muted mt-12">Brak konfiguracji faktur. Dodaj pierwsza powyzej.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>Format numeru</th>
|
||||
<th>Numerowanie</th>
|
||||
<th>Tryb</th>
|
||||
<th>Konto Fakturowni</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($configs as $cfg):
|
||||
$cid = (int) ($cfg['id'] ?? 0);
|
||||
$isDelegated = ((int) ($cfg['is_delegated'] ?? 0)) === 1;
|
||||
$isActive = ((int) ($cfg['is_active'] ?? 0)) === 1;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($cfg['name'] ?? '')) ?></td>
|
||||
<td><code><?= $e((string) ($cfg['number_format'] ?? '')) ?></code></td>
|
||||
<td><?= ((string) ($cfg['numbering_type'] ?? 'monthly')) === 'yearly' ? 'Roczne' : 'Miesieczne' ?></td>
|
||||
<td>
|
||||
<?php if ($isDelegated): ?>
|
||||
<span class="badge badge--success">Fakturownia</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted">Lokalna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isDelegated && !empty($cfg['integration_name'])): ?>
|
||||
<?= $e((string) $cfg['integration_name']) ?>
|
||||
<?php else: ?>
|
||||
<span class="muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isActive): ?>
|
||||
<span class="badge badge--success">Aktywna</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted">Nieaktywna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/accounting/invoices/edit?id=<?= $cid ?>" class="btn btn--sm btn--secondary">Edytuj</a>
|
||||
<form action="/settings/accounting/invoices/toggle" method="post" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">
|
||||
<?= $isActive ? 'Dezaktywuj' : 'Aktywuj' ?>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/settings/accounting/invoices/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
92
resources/views/settings/accounting-receipt-edit.php
Normal file
92
resources/views/settings/accounting-receipt-edit.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/** @var array<string, mixed>|null $config */
|
||||
$config = is_array($config ?? null) ? $config : null;
|
||||
$isEdit = $config !== null;
|
||||
$cid = (int) ($config['id'] ?? 0);
|
||||
|
||||
$name = (string) ($config['name'] ?? '');
|
||||
$numberFormat = (string) ($config['number_format'] ?? 'PAR/%N/%M/%Y');
|
||||
$numberingType = (string) ($config['numbering_type'] ?? 'monthly');
|
||||
$saleDateSource = (string) ($config['sale_date_source'] ?? 'issue_date');
|
||||
$orderReference = (string) ($config['order_reference'] ?? 'none');
|
||||
$isNamed = ((int) ($config['is_named'] ?? 0)) === 1;
|
||||
$isActive = $isEdit ? ((int) ($config['is_active'] ?? 0)) === 1 : true;
|
||||
|
||||
$success = trim((string) ($successMessage ?? ''));
|
||||
$error = trim((string) ($errorMessage ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">Ksiegowosc</a> » <a href="/settings/accounting/receipts">Paragony</a></p>
|
||||
<h2 class="section-title"><?= $isEdit ? 'Edycja konfiguracji paragonu' : 'Nowa konfiguracja paragonu' ?></h2>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<form action="/settings/accounting/receipts/save" method="post" novalidate class="mt-12">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Nazwa *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e($name) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Format numeru *</span>
|
||||
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="PAR/%N/%M/%Y" value="<?= $e($numberFormat) ?>">
|
||||
<small class="field-hint"><code>%N</code> = numer, <code>%M</code> = miesiac, <code>%Y</code> = rok</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Numerowanie</span>
|
||||
<select class="form-control" name="numbering_type">
|
||||
<option value="monthly"<?= $numberingType === 'monthly' ? ' selected' : '' ?>>Miesieczne</option>
|
||||
<option value="yearly"<?= $numberingType === 'yearly' ? ' selected' : '' ?>>Roczne</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Data sprzedazy</span>
|
||||
<select class="form-control" name="sale_date_source">
|
||||
<option value="issue_date"<?= $saleDateSource === 'issue_date' ? ' selected' : '' ?>>Data wystawienia</option>
|
||||
<option value="order_date"<?= $saleDateSource === 'order_date' ? ' selected' : '' ?>>Data zamowienia</option>
|
||||
<option value="payment_date"<?= $saleDateSource === 'payment_date' ? ' selected' : '' ?>>Data platnosci</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Numer referencyjny zamowienia</span>
|
||||
<select class="form-control" name="order_reference">
|
||||
<option value="none"<?= $orderReference === 'none' ? ' selected' : '' ?>>Brak</option>
|
||||
<option value="orderpro"<?= $orderReference === 'orderpro' ? ' selected' : '' ?>>orderPRO</option>
|
||||
<option value="integration"<?= $orderReference === 'integration' ? ' selected' : '' ?>>Zewnetrzny</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_named" value="1"<?= $isNamed ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0">Paragon imienny (z danymi kupujacego)</span>
|
||||
</label>
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isActive ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0">Konfiguracja aktywna</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj konfiguracje' ?></button>
|
||||
<a href="/settings/accounting/receipts" class="btn btn--secondary">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
79
resources/views/settings/accounting-receipts.php
Normal file
79
resources/views/settings/accounting-receipts.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/** @var array<int, array<string, mixed>> $configs */
|
||||
$configs = is_array($configs ?? null) ? $configs : [];
|
||||
$success = trim((string) ($successMessage ?? ''));
|
||||
$error = trim((string) ($errorMessage ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<p class="muted" style="margin-bottom:8px"><a href="/settings/accounting">← Ksiegowosc</a></p>
|
||||
<h2 class="section-title">Konfiguracje paragonow</h2>
|
||||
<p class="muted mt-12">Zarzadzaj konfiguracjami wystawiania paragonow.</p>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Lista konfiguracji</h3>
|
||||
|
||||
<div class="form-actions mt-12">
|
||||
<a class="btn btn--primary" href="/settings/accounting/receipts/new">Dodaj konfiguracje</a>
|
||||
</div>
|
||||
|
||||
<?php if ($configs === []): ?>
|
||||
<p class="muted mt-12">Brak konfiguracji paragonow. Dodaj pierwsza powyzej.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>Format numeru</th>
|
||||
<th>Numerowanie</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($configs as $cfg):
|
||||
$cid = (int) ($cfg['id'] ?? 0);
|
||||
$isActive = ((int) ($cfg['is_active'] ?? 0)) === 1;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($cfg['name'] ?? '')) ?></td>
|
||||
<td><code><?= $e((string) ($cfg['number_format'] ?? '')) ?></code></td>
|
||||
<td><?= ((string) ($cfg['numbering_type'] ?? 'monthly')) === 'yearly' ? 'Roczne' : 'Miesieczne' ?></td>
|
||||
<td>
|
||||
<?php if ($isActive): ?>
|
||||
<span class="badge badge--success">Aktywna</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted">Nieaktywna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/accounting/receipts/edit?id=<?= $cid ?>" class="btn btn--sm btn--secondary">Edytuj</a>
|
||||
<form action="/settings/accounting/receipts/toggle" method="post" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">
|
||||
<?= $isActive ? 'Dezaktywuj' : 'Aktywuj' ?>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/settings/accounting/receipts/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= $cid ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
@@ -1,158 +1,35 @@
|
||||
<?php
|
||||
$configs = is_array($configs ?? null) ? $configs : [];
|
||||
$ec = is_array($editConfig ?? null) ? $editConfig : null;
|
||||
$isEdit = $ec !== null;
|
||||
$success = trim((string) ($successMessage ?? ''));
|
||||
$error = trim((string) ($errorMessage ?? ''));
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title"><?= $e($t('settings.accounting.title')) ?></h2>
|
||||
<p class="muted mt-12"><?= $e($t('settings.accounting.description')) ?></p>
|
||||
<h2 class="section-title">Ksiegowosc</h2>
|
||||
<p class="muted mt-12">Wybierz typ dokumentu ktorego konfiguracje chcesz zarzadzac.</p>
|
||||
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($successMessage)): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||
<?php if ($success !== ''): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $e($t('settings.accounting.table.heading')) ?></h3>
|
||||
|
||||
<?php if (count($configs) === 0): ?>
|
||||
<p class="muted mt-12"><?= $e($t('settings.accounting.table.empty')) ?></p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= $e($t('settings.accounting.table.name')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.number_format')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.numbering_type')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.status')) ?></th>
|
||||
<th><?= $e($t('settings.accounting.table.actions')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($configs as $cfg): ?>
|
||||
<tr>
|
||||
<td><?= $e((string) ($cfg['name'] ?? '')) ?></td>
|
||||
<td><code><?= $e((string) ($cfg['number_format'] ?? '')) ?></code></td>
|
||||
<td><?= $e($t('settings.accounting.options.numbering_type.' . ($cfg['numbering_type'] ?? 'monthly'))) ?></td>
|
||||
<td>
|
||||
<?php if (((int) ($cfg['is_active'] ?? 0)) === 1): ?>
|
||||
<span class="badge badge--success"><?= $e($t('settings.accounting.options.active')) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted"><?= $e($t('settings.accounting.options.inactive')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/accounting?edit=<?= (int) ($cfg['id'] ?? 0) ?>" class="btn btn--sm btn--secondary"><?= $e($t('settings.accounting.actions.edit')) ?></a>
|
||||
<form action="/settings/accounting/toggle" method="post" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($cfg['id'] ?? 0) ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">
|
||||
<?= ((int) ($cfg['is_active'] ?? 0)) === 1 ? $e($t('settings.accounting.actions.deactivate')) : $e($t('settings.accounting.actions.activate')) ?>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/settings/accounting/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($cfg['id'] ?? 0) ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn"><?= $e($t('settings.accounting.actions.delete')) ?></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-grid-2">
|
||||
<div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px">
|
||||
<h3 class="section-title" style="margin-top:0">Paragony</h3>
|
||||
<p class="muted mt-12">Konfiguracje wystawiania paragonow: format numeracji, sposob numerowania, oznaczenie zamowienia.</p>
|
||||
<div class="form-actions mt-16">
|
||||
<a class="btn btn--primary" href="/settings/accounting/receipts">Zarzadzaj paragonami</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div style="border:1px solid var(--border-color, #e5e7eb);border-radius:8px;padding:16px">
|
||||
<h3 class="section-title" style="margin-top:0">Faktury</h3>
|
||||
<p class="muted mt-12">Konfiguracje wystawiania faktur: numeracja lokalna lub delegacja do Fakturowni, termin platnosci, typ dokumentu.</p>
|
||||
<div class="form-actions mt-16">
|
||||
<a class="btn btn--primary" href="/settings/accounting/invoices">Zarzadzaj fakturami</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $isEdit ? $e($t('settings.accounting.form.edit_heading')) : $e($t('settings.accounting.form.add_heading')) ?></h3>
|
||||
|
||||
<form action="/settings/accounting/save" method="post" novalidate class="mt-12">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int) ($ec['id'] ?? 0) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.name')) ?> *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="128" required value="<?= $e((string) ($ec['name'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.number_format')) ?> *</span>
|
||||
<input class="form-control" type="text" name="number_format" maxlength="64" required placeholder="PAR/%N/%M/%Y" value="<?= $e((string) ($ec['number_format'] ?? 'PAR/%N/%M/%Y')) ?>">
|
||||
<small class="field-hint"><?= $e($t('settings.accounting.fields.number_format_hint')) ?></small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-3 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.numbering_type')) ?></span>
|
||||
<select class="form-control" name="numbering_type">
|
||||
<option value="monthly"<?= ((string) ($ec['numbering_type'] ?? 'monthly')) === 'monthly' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.numbering_type.monthly')) ?></option>
|
||||
<option value="yearly"<?= ((string) ($ec['numbering_type'] ?? '')) === 'yearly' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.numbering_type.yearly')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.sale_date_source')) ?></span>
|
||||
<select class="form-control" name="sale_date_source">
|
||||
<option value="issue_date"<?= ((string) ($ec['sale_date_source'] ?? 'issue_date')) === 'issue_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.issue_date')) ?></option>
|
||||
<option value="order_date"<?= ((string) ($ec['sale_date_source'] ?? '')) === 'order_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.order_date')) ?></option>
|
||||
<option value="payment_date"<?= ((string) ($ec['sale_date_source'] ?? '')) === 'payment_date' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.sale_date_source.payment_date')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label"><?= $e($t('settings.accounting.fields.order_reference')) ?></span>
|
||||
<select class="form-control" name="order_reference">
|
||||
<option value="none"<?= ((string) ($ec['order_reference'] ?? 'none')) === 'none' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.none')) ?></option>
|
||||
<option value="orderpro"<?= ((string) ($ec['order_reference'] ?? '')) === 'orderpro' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.orderpro')) ?></option>
|
||||
<option value="integration"<?= ((string) ($ec['order_reference'] ?? '')) === 'integration' ? ' selected' : '' ?>><?= $e($t('settings.accounting.options.order_reference.integration')) ?></option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_named" value="1"<?= ((int) ($ec['is_named'] ?? 0)) === 1 ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0"><?= $e($t('settings.accounting.fields.is_named')) ?></span>
|
||||
</label>
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isEdit ? (((int) ($ec['is_active'] ?? 0)) === 1 ? ' checked' : '') : ' checked' ?>>
|
||||
<span class="field-label" style="margin:0"><?= $e($t('settings.accounting.fields.is_active')) ?></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? $e($t('settings.accounting.actions.save_edit')) : $e($t('settings.accounting.actions.save_new')) ?></button>
|
||||
<?php if ($isEdit): ?>
|
||||
<a href="/settings/accounting" class="btn btn--secondary"><?= $e($t('settings.accounting.actions.cancel')) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var form = this.closest('form');
|
||||
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||
window.OrderProAlerts.confirm(
|
||||
'<?= $e($t('settings.accounting.confirm.delete_title')) ?>',
|
||||
'<?= $e($t('settings.accounting.confirm.delete_message')) ?>',
|
||||
function() { form.submit(); }
|
||||
);
|
||||
} else {
|
||||
if (confirm('<?= $e($t('settings.accounting.confirm.delete_message')) ?>')) {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -42,6 +42,8 @@ use App\Modules\Settings\ShopproPullStatusMappingRepository;
|
||||
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||
use App\Modules\Settings\CompanySettingsController;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\InvoiceConfigController;
|
||||
use App\Modules\Settings\InvoiceConfigRepository;
|
||||
use App\Modules\Settings\ReceiptConfigController;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use App\Modules\Settings\EmailMailboxController;
|
||||
@@ -233,6 +235,14 @@ return static function (Application $app): void {
|
||||
$auth,
|
||||
$receiptConfigRepository
|
||||
);
|
||||
$invoiceConfigRepository = new InvoiceConfigRepository($app->db());
|
||||
$invoiceConfigController = new InvoiceConfigController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$invoiceConfigRepository,
|
||||
$fakturowniaIntegrationRepository
|
||||
);
|
||||
$emailMailboxRepository = new EmailMailboxRepository(
|
||||
$app->db(),
|
||||
new IntegrationSecretCipher((string) $app->config('app.integrations.secret', ''))
|
||||
@@ -518,10 +528,24 @@ return static function (Application $app): void {
|
||||
$router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]);
|
||||
$router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/company/save', [$companySettingsController, 'save'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting', [$receiptConfigController, 'index'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting', [$receiptConfigController, 'hub'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting/receipts', [$receiptConfigController, 'list'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting/receipts/new', [$receiptConfigController, 'edit'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting/receipts/edit', [$receiptConfigController, 'edit'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/receipts/save', [$receiptConfigController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/receipts/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/receipts/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
||||
// Legacy aliases (backwards compatibility with bookmarks/form actions from before Phase 114-01)
|
||||
$router->post('/settings/accounting/save', [$receiptConfigController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
||||
// Invoices (Phase 114-01)
|
||||
$router->get('/settings/accounting/invoices', [$invoiceConfigController, 'index'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting/invoices/new', [$invoiceConfigController, 'edit'], [$authMiddleware]);
|
||||
$router->get('/settings/accounting/invoices/edit', [$invoiceConfigController, 'edit'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/invoices/save', [$invoiceConfigController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/invoices/toggle', [$invoiceConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/invoices/delete', [$invoiceConfigController, 'delete'], [$authMiddleware]);
|
||||
$router->get('/settings/email-mailboxes', [$emailMailboxController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/save', [$emailMailboxController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/delete', [$emailMailboxController, 'delete'], [$authMiddleware]);
|
||||
|
||||
158
src/Modules/Settings/InvoiceConfigController.php
Normal file
158
src/Modules/Settings/InvoiceConfigController.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
final class InvoiceConfigController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly InvoiceConfigRepository $repository,
|
||||
private readonly FakturowniaIntegrationRepository $fakturownia
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$configs = $this->repository->listAll();
|
||||
$accounts = $this->fakturownia->findAll();
|
||||
|
||||
$html = $this->template->render('settings/accounting-invoices', [
|
||||
'title' => 'Konfiguracje faktur',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'configs' => $configs,
|
||||
'fakturowniaAccounts' => $accounts,
|
||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$id = (int) $request->input('id', 0);
|
||||
$config = $id > 0 ? $this->repository->findById($id) : null;
|
||||
|
||||
if ($id > 0 && $config === null) {
|
||||
Flash::set('accounting.invoices.error', 'Nie znaleziono konfiguracji faktury ID ' . $id . '.');
|
||||
return Response::redirect('/settings/accounting/invoices');
|
||||
}
|
||||
|
||||
$accounts = array_values(array_filter(
|
||||
$this->fakturownia->findAll(),
|
||||
static fn (array $row) => !empty($row['is_active'])
|
||||
));
|
||||
|
||||
$html = $this->template->render('settings/accounting-invoice-edit', [
|
||||
'title' => $config === null ? 'Nowa konfiguracja faktury' : 'Edycja konfiguracji faktury',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'config' => $config,
|
||||
'fakturowniaAccounts' => $accounts,
|
||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$id = (int) $request->input('id', 0);
|
||||
$redirectFail = $id > 0
|
||||
? '/settings/accounting/invoices/edit?id=' . $id
|
||||
: '/settings/accounting/invoices/new';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('accounting.invoices.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->save([
|
||||
'id' => $id > 0 ? $id : '',
|
||||
'name' => (string) $request->input('name', ''),
|
||||
'number_format' => (string) $request->input('number_format', ''),
|
||||
'numbering_type' => (string) $request->input('numbering_type', 'monthly'),
|
||||
'sale_date_source' => (string) $request->input('sale_date_source', 'issue_date'),
|
||||
'order_reference' => (string) $request->input('order_reference', 'none'),
|
||||
'payment_to_days' => (int) $request->input('payment_to_days', 7),
|
||||
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
||||
'is_delegated' => $request->input('is_delegated', ''),
|
||||
'integration_id' => $request->input('integration_id', ''),
|
||||
'is_active' => $request->input('is_active', ''),
|
||||
]);
|
||||
|
||||
Flash::set('accounting.invoices.save', 'Zapisano konfiguracje faktury.');
|
||||
return Response::redirect('/settings/accounting/invoices');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('accounting.invoices.error', $exception->getMessage());
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleStatus(Request $request): Response
|
||||
{
|
||||
$redirect = '/settings/accounting/invoices';
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('accounting.invoices.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', 0);
|
||||
if ($id <= 0) {
|
||||
Flash::set('accounting.invoices.error', 'Brak identyfikatora konfiguracji.');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->toggleStatus($id);
|
||||
Flash::set('accounting.invoices.save', 'Zmieniono status konfiguracji.');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('accounting.invoices.error', $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$redirect = '/settings/accounting/invoices';
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('accounting.invoices.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', 0);
|
||||
if ($id <= 0) {
|
||||
Flash::set('accounting.invoices.error', 'Brak identyfikatora konfiguracji.');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->delete($id);
|
||||
Flash::set('accounting.invoices.save', 'Usunieto konfiguracje faktury.');
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('accounting.invoices.error', $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
}
|
||||
228
src/Modules/Settings/InvoiceConfigRepository.php
Normal file
228
src/Modules/Settings/InvoiceConfigRepository.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Exceptions\IntegrationConfigException;
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class InvoiceConfigRepository
|
||||
{
|
||||
use ToggleableRepositoryTrait;
|
||||
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listAll(): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT ic.*, i.name AS integration_name
|
||||
FROM invoice_configs ic
|
||||
LEFT JOIN integrations i ON i.id = ic.integration_id AND i.type = :type
|
||||
ORDER BY ic.is_active DESC, ic.name ASC'
|
||||
);
|
||||
$statement->execute(['type' => 'fakturownia']);
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT ic.*, i.name AS integration_name
|
||||
FROM invoice_configs ic
|
||||
LEFT JOIN integrations i ON i.id = ic.integration_id AND i.type = :type
|
||||
WHERE ic.id = :id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $id, 'type' => 'fakturownia']);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($row) ? $row : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return int Saved config id
|
||||
*/
|
||||
public function save(array $data): int
|
||||
{
|
||||
$name = trim((string) ($data['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
throw new IntegrationConfigException('Nazwa konfiguracji faktury jest wymagana.');
|
||||
}
|
||||
if (mb_strlen($name) > 128) {
|
||||
throw new IntegrationConfigException('Nazwa konfiguracji jest za dluga (max 128 znakow).');
|
||||
}
|
||||
|
||||
$numberFormat = trim((string) ($data['number_format'] ?? ''));
|
||||
if ($numberFormat === '') {
|
||||
throw new IntegrationConfigException('Format numeracji jest wymagany.');
|
||||
}
|
||||
if (mb_strlen($numberFormat) > 64) {
|
||||
throw new IntegrationConfigException('Format numeracji jest za dlugi (max 64 znakow).');
|
||||
}
|
||||
|
||||
$numberingType = (string) ($data['numbering_type'] ?? 'monthly');
|
||||
if (!in_array($numberingType, ['monthly', 'yearly'], true)) {
|
||||
$numberingType = 'monthly';
|
||||
}
|
||||
|
||||
$saleDateSource = (string) ($data['sale_date_source'] ?? 'issue_date');
|
||||
if (!in_array($saleDateSource, ['order_date', 'payment_date', 'issue_date'], true)) {
|
||||
$saleDateSource = 'issue_date';
|
||||
}
|
||||
|
||||
$orderReference = (string) ($data['order_reference'] ?? 'none');
|
||||
if (!in_array($orderReference, ['none', 'orderpro', 'integration'], true)) {
|
||||
$orderReference = 'none';
|
||||
}
|
||||
|
||||
$paymentToDays = (int) ($data['payment_to_days'] ?? 7);
|
||||
if ($paymentToDays < 0) {
|
||||
$paymentToDays = 0;
|
||||
}
|
||||
if ($paymentToDays > 365) {
|
||||
$paymentToDays = 365;
|
||||
}
|
||||
|
||||
$defaultKind = trim((string) ($data['default_kind'] ?? 'vat'));
|
||||
if ($defaultKind === '') {
|
||||
$defaultKind = 'vat';
|
||||
}
|
||||
if (mb_strlen($defaultKind) > 32) {
|
||||
throw new IntegrationConfigException('Typ dokumentu jest za dlugi (max 32 znakow).');
|
||||
}
|
||||
|
||||
$isDelegated = !empty($data['is_delegated']) ? 1 : 0;
|
||||
$isActive = !empty($data['is_active']) ? 1 : 0;
|
||||
$integrationId = isset($data['integration_id']) && $data['integration_id'] !== ''
|
||||
? (int) $data['integration_id']
|
||||
: null;
|
||||
|
||||
if ($isDelegated === 1) {
|
||||
if ($integrationId === null || $integrationId <= 0) {
|
||||
throw new IntegrationConfigException(
|
||||
'Przy delegacji wystawiania do Fakturowni musisz wskazac konto Fakturowni.'
|
||||
);
|
||||
}
|
||||
if (!$this->isFakturowniaIntegration($integrationId)) {
|
||||
throw new IntegrationConfigException(
|
||||
'Wybrana integracja nie jest kontem Fakturowni - sprawdz konfiguracje.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$integrationId = null;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'name' => $name,
|
||||
'integration_id' => $integrationId,
|
||||
'is_delegated' => $isDelegated,
|
||||
'is_active' => $isActive,
|
||||
'number_format' => $numberFormat,
|
||||
'numbering_type' => $numberingType,
|
||||
'sale_date_source' => $saleDateSource,
|
||||
'order_reference' => $orderReference,
|
||||
'payment_to_days' => $paymentToDays,
|
||||
'default_kind' => $defaultKind,
|
||||
];
|
||||
|
||||
$id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : 0;
|
||||
|
||||
if ($id > 0) {
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE invoice_configs SET
|
||||
name = :name,
|
||||
integration_id = :integration_id,
|
||||
is_delegated = :is_delegated,
|
||||
is_active = :is_active,
|
||||
number_format = :number_format,
|
||||
numbering_type = :numbering_type,
|
||||
sale_date_source = :sale_date_source,
|
||||
order_reference = :order_reference,
|
||||
payment_to_days = :payment_to_days,
|
||||
default_kind = :default_kind,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$params['id'] = $id;
|
||||
$statement->execute($params);
|
||||
return $id;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO invoice_configs
|
||||
(name, integration_id, is_delegated, is_active, number_format, numbering_type,
|
||||
sale_date_source, order_reference, payment_to_days, default_kind)
|
||||
VALUES
|
||||
(:name, :integration_id, :is_delegated, :is_active, :number_format, :numbering_type,
|
||||
:sale_date_source, :order_reference, :payment_to_days, :default_kind)'
|
||||
);
|
||||
$statement->execute($params);
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function toggleStatus(int $id): void
|
||||
{
|
||||
$this->toggleActive('invoice_configs', $id);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
if ($id <= 0) {
|
||||
throw new IntegrationConfigException('Nieprawidlowy identyfikator konfiguracji.');
|
||||
}
|
||||
|
||||
if ($this->hasInvoices($id)) {
|
||||
throw new IntegrationConfigException(
|
||||
'Nie mozna usunac konfiguracji - istnieja juz wystawione faktury powiazane z ta konfiguracja.'
|
||||
);
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare('DELETE FROM invoice_configs WHERE id = :id');
|
||||
$statement->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
private function isFakturowniaIntegration(int $integrationId): bool
|
||||
{
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT 1 FROM integrations WHERE id = :id AND type = :type LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $integrationId, 'type' => 'fakturownia']);
|
||||
return $statement->fetchColumn() !== false;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function hasInvoices(int $configId): bool
|
||||
{
|
||||
try {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT 1 FROM invoices WHERE config_id = :id LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $configId]);
|
||||
return $statement->fetchColumn() !== false;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,27 +22,58 @@ final class ReceiptConfigController
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
public function hub(Request $request): Response
|
||||
{
|
||||
$html = $this->template->render('settings/accounting', [
|
||||
'title' => $this->translator->get('settings.accounting.title'),
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'successMessage' => (string) Flash::get('settings.accounting.success', ''),
|
||||
'errorMessage' => (string) Flash::get('settings.accounting.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function list(Request $request): Response
|
||||
{
|
||||
$t = $this->translator;
|
||||
$configs = $this->repository->listAll();
|
||||
|
||||
$editConfig = null;
|
||||
$editId = (int) $request->input('edit', '0');
|
||||
if ($editId > 0) {
|
||||
$editConfig = $this->repository->findById($editId);
|
||||
}
|
||||
|
||||
$html = $this->template->render('settings/accounting', [
|
||||
'title' => $t->get('settings.accounting.title'),
|
||||
$html = $this->template->render('settings/accounting-receipts', [
|
||||
'title' => 'Konfiguracje paragonow',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'configs' => $configs,
|
||||
'editConfig' => $editConfig,
|
||||
'successMessage' => Flash::get('settings.accounting.success', ''),
|
||||
'errorMessage' => Flash::get('settings.accounting.error', ''),
|
||||
'successMessage' => (string) Flash::get('settings.accounting.success', ''),
|
||||
'errorMessage' => (string) Flash::get('settings.accounting.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$id = (int) $request->input('id', 0);
|
||||
$config = $id > 0 ? $this->repository->findById($id) : null;
|
||||
|
||||
if ($id > 0 && $config === null) {
|
||||
Flash::set('settings.accounting.error', 'Nie znaleziono konfiguracji paragonu ID ' . $id . '.');
|
||||
return Response::redirect('/settings/accounting/receipts');
|
||||
}
|
||||
|
||||
$html = $this->template->render('settings/accounting-receipt-edit', [
|
||||
'title' => $config === null ? 'Nowa konfiguracja paragonu' : 'Edycja konfiguracji paragonu',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'accounting',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'config' => $config,
|
||||
'successMessage' => (string) Flash::get('settings.accounting.success', ''),
|
||||
'errorMessage' => (string) Flash::get('settings.accounting.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
@@ -50,9 +81,15 @@ final class ReceiptConfigController
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$id = (int) $request->input('id', 0);
|
||||
$redirectFail = $id > 0
|
||||
? '/settings/accounting/receipts/edit?id=' . $id
|
||||
: '/settings/accounting/receipts/new';
|
||||
$redirectOk = '/settings/accounting/receipts';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
|
||||
$name = trim((string) $request->input('name', ''));
|
||||
@@ -60,17 +97,17 @@ final class ReceiptConfigController
|
||||
|
||||
if ($name === '') {
|
||||
Flash::set('settings.accounting.error', 'Nazwa konfiguracji jest wymagana');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
|
||||
if ($numberFormat === '' || strpos($numberFormat, '%N') === false) {
|
||||
Flash::set('settings.accounting.error', 'Format numeracji jest wymagany i musi zawierac %N');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->save([
|
||||
'id' => $request->input('id', ''),
|
||||
'id' => $id > 0 ? $id : '',
|
||||
'name' => $name,
|
||||
'is_active' => $request->input('is_active', null),
|
||||
'number_format' => $numberFormat,
|
||||
@@ -81,24 +118,25 @@ final class ReceiptConfigController
|
||||
]);
|
||||
|
||||
Flash::set('settings.accounting.success', $this->translator->get('settings.accounting.flash.saved'));
|
||||
return Response::redirect($redirectOk);
|
||||
} catch (Throwable) {
|
||||
Flash::set('settings.accounting.error', $this->translator->get('settings.accounting.flash.save_failed'));
|
||||
return Response::redirect($redirectFail);
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/accounting');
|
||||
}
|
||||
|
||||
public function toggleStatus(Request $request): Response
|
||||
{
|
||||
$redirect = '/settings/accounting/receipts';
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', '0');
|
||||
if ($id <= 0) {
|
||||
Flash::set('settings.accounting.error', 'Nieprawidlowy identyfikator konfiguracji');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -108,20 +146,21 @@ final class ReceiptConfigController
|
||||
Flash::set('settings.accounting.error', 'Blad zmiany statusu');
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$redirect = '/settings/accounting/receipts';
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.accounting.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', '0');
|
||||
if ($id <= 0) {
|
||||
Flash::set('settings.accounting.error', 'Nieprawidlowy identyfikator konfiguracji');
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -131,6 +170,6 @@ final class ReceiptConfigController
|
||||
Flash::set('settings.accounting.error', $this->translator->get('settings.accounting.flash.delete_failed'));
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/accounting');
|
||||
return Response::redirect($redirect);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user