--- 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 --- ## 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) ## 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 - **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). ## 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 ``` Task 1: Migration seed + InvoiceConfigRepository + walidacja delegacji database/migrations/20260511_000107_seed_default_invoice_config.sql, src/Modules/Settings/InvoiceConfigRepository.php, .paul/codebase/db_schema.md 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). `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. AC-1 satisfied: seed idempotentny + repo z walidacja delegacji dziala. Task 2: InvoiceConfigController + widoki (list + edit) + JS toggle + routy 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 `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 `-` 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) ``. - 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) - `
` wrapper na pole `integration_id` + `` 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 `` 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