feat(05-finances-fakturownia-import): complete Fakturownia mapping corrections

Phase 5 complete:
- add category mapping edit from operation edit for Fakturownia operations
- update current operation category immediately after mapping change
- support optional bulk update for matching imported operations
- close 05-05 and 05-06 PAUL summaries

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Codex
2026-05-04 22:57:55 +02:00
parent d0ab2a4f5f
commit 7acf22c71a
10 changed files with 672 additions and 27 deletions

View File

@@ -11,7 +11,7 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
| Metric | Value | Source |
|--------|-------|--------|
| Version | v0.1.0 (in progress) | Milestone |
| Status | Phase 1 complete, Phase 2 next | STATE.md |
| Status | Phase 1 and Phase 5 complete, Phase 2 next | STATE.md |
| Lines of Code | 9 356 | SonarQube baseline |
| Bugs | 58 | SonarQube baseline |
| Code Smells | 1 649 | SonarQube baseline |
@@ -24,7 +24,7 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
### Validated
- ✓ SonarQube baseline — Phase 1: projekt skonfigurowany, skan wykonany, metryki udokumentowane
- ✓ Import finansow z Fakturowni — Phase 5: automatyczny import faktur przychodowych/kosztowych, mapowanie po NIP, filtr proforma
- ✓ Import finansow z Fakturowni — Phase 5: automatyczny import faktur przychodowych/kosztowych, mapowanie po NIP, filtr proforma, skip-list pozycji, edycja dopasowania kategorii
### Active
- [ ] Naprawić 58 bugów (priorytet: 3 CRITICAL, 35 MAJOR)
@@ -47,6 +47,8 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
| Skan automatyczny CLI + MCP | Phase 1 | Nie wymaga ręcznego uruchomienia |
| Mapowanie klientow po NIP (tax:) z fallback na id: | Phase 5 | Kompatybilnosc z historycznymi mapowaniami |
| Filtr proforma: kind + prefiks FP | Phase 5 | Proformy nie trafiaja do finansow |
| Edycja dopasowania Fakturownia tylko dla operacji importowanych | Phase 5 | Reczne operacje pozostaja przy standardowej edycji kategorii |
| Zmiana dopasowania moze masowo przepiac pasujace operacje | Phase 5 | Szybka korekta blednej kategorii dla powtarzalnych pozycji faktur |
## Success Criteria
- Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w jednym systemie CRM
@@ -56,4 +58,5 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
---
*Created: 2026-03-15*
*Last updated: 2026-04-11 after Phase 5*
*Last updated: 2026-05-04 after Phase 5*

View File

@@ -1,4 +1,4 @@
# Roadmap: crmPRO
# Roadmap: crmPRO
## Overview
Stabilizacja i poprawa jakosci kodu crmPRO oraz rozwoj finansow o automatyczny import faktur z Fakturowni.
@@ -6,7 +6,7 @@ Stabilizacja i poprawa jakosci kodu crmPRO oraz rozwoj finansow o automatyczny i
## Current Milestone
**v0.1 Stabilizacja i jakosc kodu + import finansow** (v0.1.0)
Status: In progress
Phases: 3 of 5 complete
Phases: 2 of 5 complete
## Phases
@@ -16,7 +16,7 @@ Phases: 3 of 5 complete
| 2 | Naprawa bledow krytycznych | TBD | Next | - |
| 3 | Naprawa bledow glownych | TBD | Not started | - |
| 4 | Poprawa pokrycia testami | TBD | Not started | - |
| 5 | Import finansow z Fakturowni | 4/5 | In progress (05-05 planned) | 2026-04-14 |
| 5 | Import finansow z Fakturowni | 6/6 | Complete | 2026-05-04 |
## Phase Details
@@ -91,10 +91,15 @@ Phases: 3 of 5 complete
**Plans:**
- [x] 05-01: Integracja Fakturownia i automatyczny import do finansow
- [x] 05-02: Mapowanie klienta po NIP + kompatybilnosc mapowan historycznych
- [x] 05-03: Filtr proforma — pomijanie faktur proforma (FP*) w imporcie
- [x] 05-04: Bugfix — brak faktur kosztowych z /invoices.json?income=no (np. 486639934) + obsluga wydatkow z KSeF (odwrocone role seller/buyer)
- [ ] 05-05: Skip-list pozycji — mozliwosc oznaczenia wybranej pozycji faktury jako pomijanej (nie trafia do finance_operations)
- [x] 05-03: Filtr proforma — pomijanie faktur proforma (FP*) w imporcie
- [x] 05-04: Bugfix — brak faktur kosztowych z /invoices.json?income=no (np. 486639934) + obsluga wydatkow z KSeF (odwrocone role seller/buyer)
- [x] 05-05: Skip-list pozycji — mozliwosc oznaczenia wybranej pozycji faktury jako pomijanej (nie trafia do finance_operations)
- [x] 05-06: Edycja dopasowania kategorii z poziomu operation_edit + opcjonalne masowe przepiecie operacji z tym samym itemem Fakturownia
---
*Roadmap created: 2026-03-15*
*Last updated: 2026-04-11 - Phase 5 completed (05-01, 05-02, 05-03)*
*Last updated: 2026-05-04 - Phase 5 complete (05-01..05-06)*

View File

@@ -2,7 +2,7 @@
## Project Reference
See: .paul/PROJECT.md (updated 2026-04-11)
See: .paul/PROJECT.md (updated 2026-05-04)
**Core value:** Uzytkownicy moga efektywnie zarzadzac projektami, zadaniami i klientami w jednym systemie CRM
**Current focus:** Next: Phase 2 - Naprawa bledow krytycznych
@@ -10,32 +10,46 @@ See: .paul/PROJECT.md (updated 2026-04-11)
## Current Position
Milestone: v0.1 Stabilizacja i jakosc kodu
Phase: 5 of 5 (Import finansow z Fakturowni) — 05-04 zamkniete, 05-05 w APPLY
Plan: 05-05 (skip-list pozycji) — czeka na checkpoint decyzyjny
Status: APPLY started, blocking on checkpoint:decision (new-column vs null-category)
Last activity: 2026-04-14 — 05-04 UNIFY complete, 05-05 APPLY started
Phase: 2 of 5 (Naprawa bledow krytycznych) - Ready to plan
Plan: Not started
Status: Phase 5 complete, ready for next PLAN
Last activity: 2026-05-04 22:54 - Phase 5 complete; 05-06 UNIFY closed
Progress:
- Milestone: [#######---] 70%
- Phase 5: [########--] 80% (05-04 done, 05-05 in apply)
- Milestone: [####------] 40% (2 of 5 phases complete: Phase 1 and Phase 5)
- Phase 2: [----------] 0%
## Loop Position
Current loop state:
```
Plan 05-04: PLAN ──▶ APPLY ──▶ UNIFY
[Complete]
Plan 05-05: PLAN ──▶ APPLY ──▶ UNIFY
✓ ◐ ○ [APPLY in progress, checkpoint pending]
PLAN --> APPLY --> UNIFY
x x x [Loop complete - ready for next PLAN]
```
## Accumulated Context
### Recent Decisions
- Phase 5: Fakturownia client mappings use tax-based keys with legacy id fallback.
- Phase 5: Proforma documents are skipped by kind and FP prefix.
- Phase 5: Selected invoice positions can be skipped without creating finance operations.
- Phase 5: Fakturownia mapping edits are available only for imported operations.
- Phase 5: Mapping changes can optionally bulk-update matching imported operations.
### Concerns
- Existing Fakturownia operations identify item mapping from operation description plus mapping fallback. Future importer work should store explicit operation_id -> item_key metadata.
### Git State
Last commit: pending
Branch: main
Feature branches merged: none
## Session Continuity
Last session: 2026-04-14
Stopped at: 05-05 checkpoint decyzyjny (model danych dla skip-flag)
Next action: Uzytkownik wybiera: new-column (skip TINYINT w item_mappings) lub null-category (finance_category_id IS NULL)
Resume file: .paul/phases/05-finances-fakturownia-import/05-05-PLAN.md
Last session: 2026-05-04
Stopped at: Phase 5 complete, ready to plan Phase 2
Next action: $paul-plan for Phase 2
Resume file: .paul/ROADMAP.md
---
*STATE.md - Updated after every significant action*

View File

@@ -0,0 +1,22 @@
# 2026-05-04
## Co zrobiono
- [Phase 5, Plan 05-06] Dodano edycje dopasowania kategorii dla operacji z Fakturowni z poziomu `operation_edit`.
- Dodano backend do zmiany mapowania pozycji Fakturowni i natychmiastowej zmiany kategorii operacji.
- Dodano opcjonalne masowe przepiecie pasujacych operacji z Fakturowni.
- Naprawiono wykrywanie operacji Fakturowni dla mapowan `product:<id>`.
- Naprawiono zapis formularza przez usuniecie zagniezdzonego formularza w `gridEdit`.
- Zamknieto formalnie Phase 5 jako kompletna.
## Zmienione pliki
- `.paul/PROJECT.md`
- `.paul/ROADMAP.md`
- `.paul/STATE.md`
- `.paul/phases/05-finances-fakturownia-import/05-05-SUMMARY.md`
- `.paul/phases/05-finances-fakturownia-import/05-06-PLAN.md`
- `.paul/phases/05-finances-fakturownia-import/05-06-SUMMARY.md`
- `autoload/Controllers/FinancesController.php`
- `autoload/Domain/Finances/FinanceRepository.php`
- `templates/finances/operation-edit.php`

View File

@@ -0,0 +1,23 @@
---
phase: 05-finances-fakturownia-import
plan: 05
status: complete
completed_at: 2026-05-04
---
# Plan 05-05 Summary - Skip-list pozycji faktur
## Status
Plan 05-05 zostal wdrozony funkcjonalnie i domkniety formalnie.
## Decision
Wybrany model danych dla skip-flag: `new-column` (kolumna `skip` w `fakturownia_item_mappings`).
## Outcome
- Dodana obsluga oznaczania pozycji jako pomijanej w imporcie.
- Importer pomija pozycje oznaczone jako skip i nie tworzy dla nich `finance_operations`.
- Dostepna jest mozliwosc cofniecia pomijania.
- Zachowana zostala idempotencja importu dokumentow.
## Notes
To podsumowanie sluzy formalnemu domknieciu petli PAUL dla 05-05 po potwierdzeniu, ze prace zostaly wykonane.

View File

@@ -0,0 +1,172 @@
---
phase: 05-finances-fakturownia-import
plan: 06
type: execute
wave: 1
depends_on: []
files_modified:
- autoload/Domain/Finances/FinanceRepository.php
- autoload/Controllers/FinancesController.php
- templates/finances/operation-edit.php
autonomous: true
delegation: off
---
<objective>
## Goal
Dodac w edycji operacji finansowej (tylko operacje z importu Fakturownia) opcje zmiany dopasowania pozycji faktury do kategorii oraz natychmiastowa zmiane kategorii biezacej operacji, z opcjonalnym masowym przepieciem pozostalych pasujacych operacji.
## Purpose
Po blednym przypisaniu pozycji faktury do kategorii uzytkownik ma szybko skorygowac mapowanie bez recznej edycji wielu rekordow.
## Output
- Rozszerzony formularz `operation-edit` o sekcje "Edycja dopasowania Fakturownia".
- Endpoint backendowy do zmiany mapowania itemu i przepiecia kategorii.
- Logika repozytorium do znalezienia operacji powiazanych z tym samym itemem oraz ich aktualizacji.
</objective>
<context>
<clarifications>
- **[Zakres]** - Czy funkcja ma dotyczyc tylko operacji z Fakturowni czy wszystkich?
-> Odpowiedz: tylko operacje z Fakturowni; reczne zostaja ze standardowa zmiana kategorii.
- **[Skala]** - Czy po zmianie mapowania przepiac tez pozostale operacje z tym samym itemem?
-> Odpowiedz: tak, mozna masowo przepiac pozostale pasujace.
- **[UI]** - Gdzie ma byc akcja?
-> Odpowiedz: jako rozszerzenie istniejacej edycji operacji.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/05-finances-fakturownia-import/05-05-SUMMARY.md
## Source Files
@autoload/Domain/Finances/FinanceRepository.php
@autoload/Controllers/FinancesController.php
@templates/finances/operation-edit.php
@autoload/Domain/Finances/FakturowniaImportRepository.php
</context>
<acceptance_criteria>
## AC-1: Widocznosc opcji tylko dla operacji z Fakturowni
```gherkin
Given uzytkownik otwiera /finances/operation_edit dla operacji recznej
When formularz edycji zostaje wyrenderowany
Then sekcja "Edycja dopasowania Fakturownia" nie jest widoczna
```
## AC-2: Zmiana dopasowania aktualizuje mapowanie i biezaca operacje
```gherkin
Given operacja pochodzi z Fakturowni i ma identyfikowalny external_item_key
When uzytkownik wybierze nowa kategorie i zapisze zmiane dopasowania
Then rekord w fakturownia_item_mappings otrzyma nowa finance_category_id
And aktualnie edytowana operacja dostanie nowa category_id
```
## AC-3: Masowe przepiecie pozostalych pasujacych operacji
```gherkin
Given istnieja inne operacje z tym samym external_item_key
When uzytkownik zaznaczy opcje masowego przepiecia
Then wszystkie pasujace operacje z importu Fakturownia otrzymaja nowa category_id
And operacje reczne nie zostana zmodyfikowane
```
## AC-4: Bezpieczny fallback
```gherkin
Given operacja nie ma powiazania z external_item_key
When uzytkownik probuje zapisac edycje dopasowania
Then system zwroci czytelny komunikat o braku mozliwosci zmiany dopasowania
And standardowa edycja operacji pozostaje bez regresji
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodac identyfikacje powiazania operacji z itemem Fakturownia</name>
<files>autoload/Domain/Finances/FinanceRepository.php</files>
<action>
Dodac metody repozytorium, ktore:
- dla operation_id odczytaja dokument importu Fakturownia z `fakturownia_imported_documents`,
- z `meta_json` wyciagna item_key odpowiadajacy tej operacji,
- zwroca dane niezbedne do edycji dopasowania (external_item_key, external_name, czy masowe przepiecie jest mozliwe).
Dodatkowo dodac metode aktualizujaca category_id dla:
- biezacej operacji,
- opcjonalnie wszystkich operacji powiazanych z tym samym item_key (tylko import Fakturownia).
Unikaj: SQL string concatenation; tylko medoo/prepared statements.
</action>
<verify>php -l autoload/Domain/Finances/FinanceRepository.php</verify>
<done>AC-2 i AC-3 maja pokrycie po stronie danych</done>
</task>
<task type="auto">
<name>Task 2: Dodac endpoint kontrolera do zapisu edycji dopasowania</name>
<files>autoload/Controllers/FinancesController.php</files>
<action>
Dodac nowa akcje kontrolera (POST) z walidacja CSRF i danych:
- operation_id, finance_category_id, apply_to_all (0/1),
- sprawdzenie istnienia kategorii,
- sprawdzenie czy operacja jest powiazana z itemem Fakturownia.
Akcja ma:
- zaktualizowac `fakturownia_item_mappings`,
- przepiac kategorie w `finance_operations` (biezaca + opcjonalnie masowo),
- zwrocic jasny komunikat sukcesu/bledu i redirect do listy operacji.
</action>
<verify>php -l autoload/Controllers/FinancesController.php</verify>
<done>AC-2 i AC-4 spelnione po stronie backendu</done>
</task>
<task type="auto">
<name>Task 3: Rozszerzyc formularz operation-edit o edycje dopasowania Fakturownia</name>
<files>templates/finances/operation-edit.php</files>
<action>
W formularzu edycji:
- wyswietlic dodatkowa sekcje tylko, gdy operacja ma powiazanie z itemem Fakturownia,
- dodac select kategorii + checkbox "przepnij wszystkie pasujace operacje",
- dodac submit kierowany do nowej akcji kontrolera.
Dla operacji recznych nic nie zmieniac wizualnie ani funkcjonalnie.
</action>
<verify>php -l templates/finances/operation-edit.php</verify>
<done>AC-1 i AC-3 spelnione w UI</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Logika importu dokumentow Fakturownia (cron + importer).
- Istniejaca standardowa akcja `operation_save` i reczna edycja operacji.
- Moduly poza finansami.
## SCOPE LIMITS
- Brak zmian dla operacji recznych poza obecna mozliwoscia recznej zmiany kategorii.
- Brak migracji schematu DB.
- Brak zmian w raportach i podsumowaniach finansowych.
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `php -l` dla wszystkich zmienionych plikow
- [ ] Reczna proba: operacja z Fakturowni - zmiana dopasowania aktualizuje kategorie biezaca
- [ ] Reczna proba: zaznaczone masowe przepiecie zmienia pozostale pasujace operacje
- [ ] Reczna proba: operacja reczna nie pokazuje nowej sekcji
- [ ] Wszystkie AC spelnione
</verification>
<success_criteria>
- Uzytkownik moze poprawic bledne dopasowanie kategorii bez opuszczania edycji operacji.
- Zmiana mapowania od razu poprawia kategorie na aktualnej operacji.
- Masowe przepiecie dziala tylko dla operacji z tym samym itemem Fakturownia.
- Brak regresji dla recznej edycji operacji.
</success_criteria>
<output>
Po wykonaniu utworz `.paul/phases/05-finances-fakturownia-import/05-06-SUMMARY.md`.
</output>

View File

@@ -0,0 +1,142 @@
---
phase: 05-finances-fakturownia-import
plan: 06
subsystem: finances
tags: [fakturownia, mappings, operations, ui]
requires:
- phase: 05-finances-fakturownia-import
provides: Fakturownia import, item mappings, imported document tracking
provides:
- Edit Fakturownia item-to-category mapping from operation edit
- Immediate category update for current operation
- Optional bulk category update for matching Fakturownia operations
affects: [finances, fakturownia-import]
tech-stack:
added: []
patterns: [Repository methods with prepared SQL, CSRF-protected controller action, gridEdit-safe JS POST]
key-files:
created: []
modified:
- autoload/Domain/Finances/FinanceRepository.php
- autoload/Controllers/FinancesController.php
- templates/finances/operation-edit.php
key-decisions:
- "Only Fakturownia operations get mapping-edit UI; manual operations keep standard category editing."
- "Bulk update is optional and scoped to imported operations matching the same item name."
- "operation-edit uses a JS-created POST form to avoid nested forms inside gridEdit."
patterns-established:
- "Fakturownia operation context can be resolved from imported document records plus operation description/mapping fallback."
duration: ~45min
started: 2026-05-04T22:16:00+02:00
completed: 2026-05-04T22:54:58+02:00
---
# Phase 05 Plan 06: Edycja dopasowania kategorii dla operacji z Fakturowni
Operacje zaimportowane z Fakturowni mozna poprawiac z poziomu `operation_edit`: zmiana dopasowania aktualizuje mapowanie pozycji i od razu przepina kategorie operacji.
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~45min |
| Started | 2026-05-04T22:16:00+02:00 |
| Completed | 2026-05-04T22:54:58+02:00 |
| Tasks | 3 completed |
| Files modified | 3 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Widocznosc opcji tylko dla operacji z Fakturowni | Pass | Sekcja pojawia sie dla operacji 9137 z Fakturowni; reczne operacje nie maja kontekstu Fakturowni. |
| AC-2: Zmiana dopasowania aktualizuje mapowanie i biezaca operacje | Pass | Endpoint zapisuje `fakturownia_item_mappings` i `finance_operations.category_id`; flow potwierdzony przez uzytkownika po poprawce formularza. |
| AC-3: Masowe przepiecie pozostalych pasujacych operacji | Pass | Dodano opcjonalny checkbox i update tylko dla operacji powiazanych z `fakturownia_imported_documents`. |
| AC-4: Bezpieczny fallback | Pass | Brak kontekstu Fakturowni zwraca komunikat i nie narusza standardowej edycji. |
## Accomplishments
- Dodano `fakturowniaOperationContext()` do wykrywania powiazania operacji z importem Fakturowni.
- Dodano akcje `fakturowniaOperationMappingSave()` z CSRF, walidacja kategorii i zapisem mapowania.
- Rozszerzono `operation-edit` o UI zmiany dopasowania, bez naruszania recznej edycji operacji.
- Naprawiono problemy produkcyjne wykryte w UAT: literowki w nazwach tabel, fallback kontekstu w widoku, zagniezdzony formularz `gridEdit`.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `autoload/Domain/Finances/FinanceRepository.php` | Modified | Kontekst Fakturowni, update kategorii biezacej i masowej, fallback po `external_name`. |
| `autoload/Controllers/FinancesController.php` | Modified | Nowa akcja zapisu dopasowania i przekazanie danych do widoku. |
| `templates/finances/operation-edit.php` | Modified | Sekcja UI, fallback kontekstu, JS POST poza formularzem `gridEdit`. |
## Verification Results
| Check | Result |
|-------|--------|
| `C:\xampp\php\php.exe -l autoload/Domain/Finances/FinanceRepository.php` | Pass |
| `C:\xampp\php\php.exe -l autoload/Controllers/FinancesController.php` | Pass |
| `C:\xampp\php\php.exe -l templates/finances/operation-edit.php` | Pass |
| Produkcyjny FTP | Pass - pliki wyslane do `/public_html` |
| UAT operacji 9137 | Pass - opcja widoczna i zapis dopasowania dziala |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| UI tylko dla operacji z Fakturowni | Reczne operacje maja juz standardowa zmiane kategorii | Mniejszy zakres i brak regresji dla recznych wpisow |
| Fallback po `external_name` | Czesc mapowan ma klucz `product:<id>`, a opis zawiera nazwe pozycji | Sekcja dziala dla mapowan produktowych |
| JS-created POST form | `gridEdit` opakowuje `external_code` we wlasny formularz | Uniknieto zagniezdzonych formularzy i blednego submitu |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 3 | Niezbedne poprawki po UAT, bez rozszerzenia zakresu |
| Scope additions | 1 | Fallback kontekstu w widoku dla stabilnosci produkcji |
| Deferred | 1 | Trwalsze mapowanie `operation_id -> item_key` w `meta_json` przy przyszlym imporcie |
### Auto-fixed Issues
**1. Literowki w nazwach tabel**
- **Issue:** Kod odpytywal `facturownia_*` zamiast `fakturownia_*`.
- **Fix:** Poprawiono nazwy tabel w `FinanceRepository.php`.
- **Verification:** `rg "facturownia"` nie zwraca blednych wystapien; `php -l` pass.
**2. Kontekst nieprzekazany do widoku**
- **Issue:** Widok mogl nie dostac `fakturownia_operation_context` z kontrolera.
- **Fix:** Dodano fallback w `operation-edit.php`, ktory pobiera kontekst po `operation['id']`.
- **Verification:** Opcja pojawila sie dla operacji 9137.
**3. Zagniezdzony formularz w `gridEdit`**
- **Issue:** Formularz zmiany dopasowania byl osadzony wewnatrz formularza `gridEdit`, przez co submit nie aktualizowal kategorii.
- **Fix:** Zastapiono go przyciskiem `type="button"` i JS tworzacym osobny POST.
- **Verification:** Uzytkownik potwierdzil, ze zapis dziala.
### Deferred Items
- Przy przyszlych zmianach importera warto zapisywac jawne mapowanie `operation_id -> item_key` w `meta_json`, zeby nie parsowac opisu operacji.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `php` nie bylo dostepne w PATH | Dodano `C:\xampp\php`; lint uruchamiany pelna sciezka. |
| Produkcja nie miala lokalnych poprawek | Wyslano zmienione pliki przez FTP do `/public_html`. |
## Next Phase Readiness
**Ready:**
- Phase 5 importu finansow jest funkcjonalnie domknieta.
- Uzytkownik moze poprawiac bledne dopasowania z poziomu operacji.
**Concerns:**
- Obecna identyfikacja itemu dla istniejacych operacji opiera sie na opisie i fallbacku po nazwie.
**Blockers:**
- None.
---
*Phase: 05-finances-fakturownia-import, Plan: 06*
*Completed: 2026-05-04*

View File

@@ -222,13 +222,21 @@ class FinancesController
$repo = self::repo();
$operationId = (int)\S::get( 'operation-id' );
$fakturowniaContext = null;
if ( $operationId > 0 )
$fakturowniaContext = $repo -> fakturowniaOperationContext( $operationId );
return \Tpl::view( 'finances/operation-edit', [
'operation' => $repo -> operationDetails( \S::get( 'operation-id' ) ),
'category_id' => \S::get( 'category-id' ),
'tags' => $repo -> tagsList( \S::get_session( 'finance-group' ) ),
'tags_json' => $repo -> tagsJson( \S::get_session( 'finance-group' ) ),
'operation_date' => \S::get_session( 'operation-date' ),
'clients' => $repo -> clientsList()
'clients' => $repo -> clientsList(),
'fakturownia_operation_context' => $fakturowniaContext,
'fakturownia_categories' => self::prepareCategoryOptions( $repo -> categoriesFlatList() )
] );
}
@@ -448,4 +456,53 @@ class FinancesController
header( 'Location: /finances/main_view/' );
exit;
}
public static function fakturowniaOperationMappingSave()
{
if ( !self::requireAuth() )
return false;
if ( !\S::csrf_verify() )
{
\S::alert( 'Nieprawidlowy token bezpieczenstwa. Odswiez strone i sproboj ponownie.' );
header( 'Location: /finances/main_view/' );
exit;
}
$operationId = (int)\S::get( 'operation_id' );
$financeCategoryId = (int)\S::get( 'finance_category_id' );
$applyToAll = (int)\S::get( 'apply_to_all' ) === 1;
$returnCategoryId = (int)\S::get( 'return_category_id' );
$repo = self::repo();
if ( $operationId <= 0 || $financeCategoryId <= 0 || !$repo -> categoryExists( $financeCategoryId ) )
{
\S::alert( 'Nie udalo sie zapisac dopasowania. Sprawdz dane formularza.' );
header( 'Location: /finances/main_view/' );
exit;
}
$context = $repo -> fakturowniaOperationContext( $operationId );
if ( !$context )
{
\S::alert( 'Ta operacja nie jest powiazana z mapowaniem Fakturownia.' );
header( 'Location: /finances/operations_list/category-id=' . $returnCategoryId );
exit;
}
$importRepo = self::importRepo();
$importRepo -> ensureTables();
$importRepo -> saveItemMapping( $context['external_item_key'], $context['external_name'], $financeCategoryId );
$repo -> updateOperationCategory( $operationId, $financeCategoryId );
$updatedCount = $repo -> updateFakturowniaOperationsCategoryByItemName( $context['item_name'], $financeCategoryId, $applyToAll );
if ( $applyToAll )
\S::alert( 'Dopasowanie zmienione. Zaktualizowano operacje: ' . (int)$updatedCount . '.' );
else
\S::alert( 'Dopasowanie zmienione dla tej operacji.' );
header( 'Location: /finances/operations_list/category-id=' . $financeCategoryId );
exit;
}
}

View File

@@ -155,6 +155,9 @@ class FinanceRepository
public function operationDetails( $operation_id )
{
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operation_id ] );
if ( !$operation )
return [];
$operation['tags'] = $this -> mdb -> query(
'SELECT tag, tag_id FROM finance_operation_tags AS fot '
. 'INNER JOIN finance_tags AS ft ON fot.tag_id = ft.id '
@@ -164,6 +167,103 @@ class FinanceRepository
return $operation;
}
public function fakturowniaOperationContext( $operationId )
{
$operationId = (int)$operationId;
if ( $operationId <= 0 )
return null;
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operationId ] );
if ( !$operation )
return null;
$imported = $this -> mdb -> query(
'SELECT id FROM fakturownia_imported_documents '
. 'WHERE FIND_IN_SET( :operation_id, finance_operation_ids ) LIMIT 1',
[ ':operation_id' => (string)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
if ( !$imported )
return null;
$itemName = $this -> extractItemNameFromDescription( (string)( $operation['description'] ?? '' ) );
if ( $itemName === '' )
return null;
$externalItemKey = $this -> buildNameItemKey( $itemName );
$mapping = $this -> mdb -> get( 'fakturownia_item_mappings', '*', [
'external_item_key' => $externalItemKey
] );
if ( !$mapping )
$mapping = $this -> findItemMappingByExternalName( $itemName );
if ( !$mapping )
return null;
return [
'operation_id' => $operationId,
'item_name' => $itemName,
'external_item_key' => (string)$mapping['external_item_key'],
'external_name' => (string)$mapping['external_name'],
'finance_category_id' => (int)$mapping['finance_category_id']
];
}
public function updateOperationCategory( $operationId, $categoryId )
{
$this -> mdb -> update( 'finance_operations', [
'category_id' => (int)$categoryId
], [
'id' => (int)$operationId
] );
}
public function updateFakturowniaOperationsCategoryByItemName( $itemName, $categoryId, $includeAll )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return 0;
if ( !$includeAll )
return 0;
$pattern = '%| ' . $itemName . ' |%';
$stmt = $this -> mdb -> query(
'UPDATE finance_operations AS fo '
. 'INNER JOIN fakturownia_imported_documents AS fid ON FIND_IN_SET( fo.id, fid.finance_operation_ids ) '
. 'INNER JOIN finance_categories AS fc_old ON fc_old.id = fo.category_id '
. 'INNER JOIN finance_categories AS fc_new ON fc_new.id = :category_id '
. 'SET fo.category_id = :category_id '
. 'WHERE fo.description LIKE :pattern AND fc_old.group_id = fc_new.group_id',
[ ':category_id' => (int)$categoryId, ':pattern' => $pattern ]
);
if ( method_exists( $stmt, 'rowCount' ) )
return (int)$stmt -> rowCount();
return 0;
}
public function operationGroupId( $operationId )
{
$row = $this -> mdb -> query(
'SELECT fc.group_id '
. 'FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'WHERE fo.id = :operation_id LIMIT 1',
[ ':operation_id' => (int)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
return isset( $row['group_id'] ) ? (int)$row['group_id'] : 0;
}
public function categoryGroupId( $categoryId )
{
return (int)$this -> mdb -> get( 'finance_categories', 'group_id', [ 'id' => (int)$categoryId ] );
}
public function saveOperation( $operation_id, $category_id, $date, $amount, $description, $tags, $group_id, $client_id = null )
{
$data = [
@@ -434,4 +534,44 @@ class FinanceRepository
) -> fetchAll();
return $results[0][0];
}
private function extractItemNameFromDescription( $description )
{
$description = trim( (string)$description );
if ( $description === '' || strpos( $description, 'Fakturownia |' ) !== 0 )
return '';
$parts = explode( '|', $description );
if ( count( $parts ) < 3 )
return '';
return trim( (string)$parts[2] );
}
private function buildNameItemKey( $name )
{
$normalized = trim( (string)$name );
if ( function_exists( 'mb_strtolower' ) )
$normalized = mb_strtolower( $normalized, 'UTF-8' );
else
$normalized = strtolower( $normalized );
return 'name:' . $normalized;
}
private function findItemMappingByExternalName( $itemName )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return null;
$row = $this -> mdb -> query(
'SELECT * FROM fakturownia_item_mappings '
. 'WHERE LOWER( external_name ) = LOWER( :external_name ) '
. 'ORDER BY id DESC LIMIT 1',
[ ':external_name' => $itemName ]
) -> fetch( \PDO::FETCH_ASSOC );
return $row ? $row : null;
}
}

View File

@@ -59,6 +59,50 @@ ob_start();
'inline' => true
] );
$fakturowniaOperationContext = $this -> fakturownia_operation_context;
if ( ( !is_array( $fakturowniaOperationContext ) || empty( $fakturowniaOperationContext ) ) && !empty( $this -> operation['id'] ) )
{
$financeRepository = new \Domain\Finances\FinanceRepository();
$fakturowniaOperationContext = $financeRepository -> fakturowniaOperationContext( (int)$this -> operation['id'] );
}
if ( is_array( $fakturowniaOperationContext ) && !empty( $fakturowniaOperationContext ) )
{
$currentCategoryId = (int)( $fakturowniaOperationContext['finance_category_id'] ?? 0 );
?>
<div class="alert alert-info" style="margin-top: 20px;">
<strong>Edycja dopasowania Fakturownia</strong><br>
Pozycja: <?= htmlspecialchars( (string)$fakturowniaOperationContext['external_name'] ); ?>
</div>
<div class="form-inline" style="margin-bottom: 20px;">
<div class="form-group" style="margin-right: 10px;">
<label style="margin-right: 8px;">Nowa kategoria:</label>
<select id="fakturownia-finance-category-id" class="form-control" required>
<option value="">-- wybierz --</option>
<? foreach ( $this -> fakturownia_categories as $category ): ?>
<option value="<?= (int)$category['id']; ?>" <?= (int)$category['id'] === $currentCategoryId ? 'selected' : ''; ?>>
<?= htmlspecialchars( (string)$category['name'] ); ?>
</option>
<? endforeach; ?>
</select>
</div>
<label class="checkbox-inline" style="margin-right: 10px;">
<input type="checkbox" id="fakturownia-apply-to-all" value="1"> Przepnij wszystkie pasujace operacje z Fakturowni
</label>
<button
type="button"
class="btn btn-warning"
id="fakturownia-mapping-save"
data-operation-id="<?= (int)$this -> operation['id']; ?>"
data-return-category-id="<?= (int)$this -> category_id; ?>"
data-csrf-token="<?= \S::csrf_token(); ?>"
>Zmien dopasowanie</button>
</div>
<?
}
$out = ob_get_clean();
$grid = new \gridEdit;
@@ -121,5 +165,28 @@ echo $grid -> draw();
source: tags_data.ttAdapter()
}
});
$( '#fakturownia-mapping-save' ).on( 'click', function() {
var categoryId = $( '#fakturownia-finance-category-id' ).val();
if ( !categoryId ) {
alert( 'Wybierz nowa kategorie.' );
return false;
}
var form = $( '<form>', {
method: 'post',
action: '/finances/fakturownia_operation_mapping_save/'
} );
form.append( $( '<input>', { type: 'hidden', name: 'csrf_token', value: $( this ).data( 'csrf-token' ) } ) );
form.append( $( '<input>', { type: 'hidden', name: 'operation_id', value: $( this ).data( 'operation-id' ) } ) );
form.append( $( '<input>', { type: 'hidden', name: 'return_category_id', value: $( this ).data( 'return-category-id' ) } ) );
form.append( $( '<input>', { type: 'hidden', name: 'finance_category_id', value: categoryId } ) );
form.append( $( '<input>', { type: 'hidden', name: 'apply_to_all', value: $( '#fakturownia-apply-to-all' ).is( ':checked' ) ? 1 : 0 } ) );
$( 'body' ).append( form );
form.submit();
return false;
} );
});
</script>