update
This commit is contained in:
+2
-1
@@ -12,6 +12,7 @@ Jednoosobowa aplikacja finansowa w CodeIgniter 4.7.3 i MySQL do ewidencji finans
|
|||||||
- ✓ Instrumenty, operacje, wyceny i pulpity inwestycji — Plan 2.
|
- ✓ Instrumenty, operacje, wyceny i pulpity inwestycji — Plan 2.
|
||||||
- ✓ Import paragonów przez OpenAI Responses API z podpowiedziami kategorii — Plan 3.
|
- ✓ Import paragonów przez OpenAI Responses API z podpowiedziami kategorii — Plan 3.
|
||||||
- ✓ Zapamiętywanie logowania przez 30 dni na wielu urządzeniach — Plan 4.
|
- ✓ Zapamiętywanie logowania przez 30 dni na wielu urządzeniach — Plan 4.
|
||||||
|
- ✓ Skanowanie paragonu kamerą telefonu i mobilny przegląd wyniku AI — Plan 20260713-2111.
|
||||||
|
|
||||||
## Trwałe wymagania i ograniczenia
|
## Trwałe wymagania i ograniczenia
|
||||||
|
|
||||||
@@ -29,4 +30,4 @@ Jednoosobowa aplikacja finansowa w CodeIgniter 4.7.3 i MySQL do ewidencji finans
|
|||||||
| 2026-07-12 | Bez panelu urządzeń i „wyloguj wszędzie” | YAGNI; brak takiego wymagania |
|
| 2026-07-12 | Bez panelu urządzeń i „wyloguj wszędzie” | YAGNI; brak takiego wymagania |
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last updated: 2026-07-12 after Plan 20260712-1647*
|
*Last updated: 2026-07-13 after Plan 20260713-2111*
|
||||||
|
|||||||
+17
-20
@@ -1,6 +1,6 @@
|
|||||||
# STATE — finansePRO
|
# STATE — finansePRO
|
||||||
|
|
||||||
**Aktualizacja:** 2026-07-12T17:31:28+02:00
|
**Aktualizacja:** 2026-07-13T21:38:51+02:00
|
||||||
|
|
||||||
## Project Reference
|
## Project Reference
|
||||||
|
|
||||||
@@ -8,10 +8,14 @@ CodeIgniter 4.7.3 + MySQL na hostido. Jedno konto z `.env`. Tryb PAUL: plan-firs
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
- Plan `20260712-1647-zapamietywanie-logowania`: **UKOŃCZONY**.
|
- Plan `20260713-2111-skanowanie-paragonu-kamera`: **UKOŃCZONY**.
|
||||||
- Status: PLAN → APPLY → UNIFY zamknięte; gotowe do następnego planu.
|
- Status: PLAN → APPLY → UNIFY zamknięte; gotowe do następnego planu.
|
||||||
- Dostarczono tokeny logowania ważne 30 dni, niezależne dla wielu urządzeń.
|
- Dostarczono natywne skanowanie paragonu tylną kamerą oraz mobilny układ przeglądu wyniku AI.
|
||||||
- Zdalna tabela `remember_tokens` została utworzona i zweryfikowana.
|
- Quality Radar post-apply: ok; UAT mobilny zatwierdzony.
|
||||||
|
- Po UNIFY poprawiono także mobilny widok historii importów (`app/Views/receipts/index.php`).
|
||||||
|
- Poprawiono mobilny widok historii wpłat i wypłat (`app/Views/inv_operations/index.php`).
|
||||||
|
- Poprawiono mobilny widok historii wycen (`app/Views/valuations/index.php`).
|
||||||
|
- Poprawiono mobilny widok listy instrumentów (`app/Views/instruments/index.php`).
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
@@ -22,25 +26,18 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
- Last session: 2026-07-12T17:31:28+02:00
|
- Last session: 2026-07-13T21:38:51+02:00
|
||||||
- Stopped at: UNIFY planu zapamiętywania logowania.
|
- Stopped at: wszystkie zgłoszone widoki historii oraz lista instrumentów poprawione na mobile.
|
||||||
- Next action: opcjonalnie potwierdzić UAT w dwóch profilach albo uruchomić `$paul-plan` dla kolejnej funkcji.
|
- Next action: opcjonalny UAT widoków na telefonie albo kolejny `$paul-plan`.
|
||||||
- Resume file: `.paul/plans/20260712-1647-zapamietywanie-logowania/SUMMARY.md`
|
- Resume file: `.paul/plans/20260713-2111-skanowanie-paragonu-kamera/SUMMARY.md`
|
||||||
|
|
||||||
## Aktywne sprawy / do weryfikacji
|
## Aktywne sprawy / do weryfikacji
|
||||||
|
|
||||||
- UAT: dwa profile przeglądarki, zamknięcie zwykłej sesji oraz wylogowanie tylko jednego urządzenia.
|
- UAT poprawionych historii na telefonie jest opcjonalny; kontrola składni i struktury responsywnej przeszła.
|
||||||
- `vendor/bin/phpunit` nie jest dostępny lokalnie; test `RememberTokenModelTest.php` został tylko sprawdzony przez `php -l`.
|
- Repozytorium ma wcześniejszą, niezwiązaną zmianę `.vscode/ftp-kr.sync.cache.json`; nie obejmować jej.
|
||||||
- Deploy hostido nie uruchamia migracji automatycznie; tabela dla tego planu została już utworzona zdalnie.
|
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
- Token per urządzenie; kolejne logowanie nie usuwa wcześniejszych tokenów.
|
- Natywne `capture="environment"`; bez WebRTC i nowych zależności.
|
||||||
- Wylogowanie usuwa wyłącznie token bieżącego cookie.
|
- Zachować jeden istniejący przepływ `Receipts::create`.
|
||||||
- Bez panelu urządzeń i globalnego wylogowania do czasu realnej potrzeby.
|
- Widoki mobilne adaptować responsywnym HTML/CSS bez zmian backendu.
|
||||||
|
|
||||||
## Uwagi
|
|
||||||
|
|
||||||
- Radar post-apply: degraded z powodu niepełnego wyniku indeksu i braku PHPUnit.
|
|
||||||
- Repozytorium Git ma liczne wcześniejsze i niezwiązane zmiany; commit fazowy pominięty zgodnie z `preferences.auto_commit: false`.
|
|
||||||
- `ROADMAP.md` nie istnieje i pozostaje opcjonalny w trybie plan-first.
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 2026-07-13
|
||||||
|
|
||||||
|
## Co zrobiono
|
||||||
|
|
||||||
|
- [Plan 20260713-2111] Dodano skanowanie paragonu bezpośrednio tylną kamerą telefonu.
|
||||||
|
- Zachowano istniejący upload JPG/JPEG/PNG/PDF/JSON i przepływ analizy AI.
|
||||||
|
- Poprawiono responsywność formularza przeglądu pozycji po analizie AI.
|
||||||
|
- Po zamknięciu planu poprawiono mobilny widok historii importów: wiersze tabeli zmieniają się w czytelne karty.
|
||||||
|
- Poprawiono mobilny widok historii wpłat i wypłat inwestycyjnych tym samym układem kart.
|
||||||
|
- Poprawiono mobilny widok historii wycen inwestycyjnych.
|
||||||
|
- Poprawiono mobilny widok listy instrumentów inwestycyjnych.
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `app/Views/receipts/new.php`
|
||||||
|
- `app/Views/receipts/review.php`
|
||||||
|
- `app/Views/receipts/index.php`
|
||||||
|
- `app/Views/inv_operations/index.php`
|
||||||
|
- `app/Views/valuations/index.php`
|
||||||
|
- `app/Views/instruments/index.php`
|
||||||
@@ -1,5 +1,18 @@
|
|||||||
# Impact Map
|
# Impact Map
|
||||||
|
|
||||||
|
## Post-apply 20260713-2111
|
||||||
|
- Zmieniono wyłącznie `app/Views/receipts/new.php` i `app/Views/receipts/review.php`.
|
||||||
|
- Aparat korzysta z istniejącego pola `receipt` i `Receipts::create`; backend, routing, parser, modele i baza bez zmian.
|
||||||
|
- Mobilny przegląd pozycji używa układu kart poniżej `md`, zachowując nazwy pól i skrypt grupowania.
|
||||||
|
- UAT aparatu i poprawionego przeglądu zatwierdzony przez użytkownika.
|
||||||
|
|
||||||
|
## Plan 20260713-2111 — skanowanie paragonu kamerą telefonu
|
||||||
|
- UI: `app/Views/receipts/new.php`; istniejące pole `receipt` może użyć natywnego `capture="environment"`.
|
||||||
|
- Backend: `Receipts::create` już obsługuje JPG/JPEG/PNG do 10 MB i przekazuje obraz do obecnego parsera; bez zmiany kontrolera.
|
||||||
|
- Trasy, parser AI, modele, baza i zapis operacji pozostają bez zmian.
|
||||||
|
- UAT: telefon mobilny — tylna kamera, wysłanie zdjęcia, przejście do przeglądu oraz zachowany wybór istniejącego pliku.
|
||||||
|
- Ukryte sprzężenie: zachować `name="receipt"`, `multipart/form-data` i obecny `accept`, aby nie rozdzielić przepływu uploadu.
|
||||||
|
|
||||||
## Post-apply 20260712-1647
|
## Post-apply 20260712-1647
|
||||||
- Mechanizm wdrożony w kontrolerze logowania, filtrze, widoku, modelu i migracji.
|
- Mechanizm wdrożony w kontrolerze logowania, filtrze, widoku, modelu i migracji.
|
||||||
- Zdalna tabela `remember_tokens` istnieje.
|
- Zdalna tabela `remember_tokens` istnieje.
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# Quality Risks
|
# Quality Risks
|
||||||
|
|
||||||
|
## Post-apply 20260713-2111
|
||||||
|
- **Obsłużone:** jeden istniejący upload, natywne `capture="environment"`, brak nowej zależności i zmian backendu.
|
||||||
|
- **Obsłużone po UAT:** mobilny overflow formularza przeglądu — tabela zmienia się w karty poniżej `md`.
|
||||||
|
- **Potwierdzone:** nazwy pól, `accept`, `required`, limit 10 MB i skrypt grupowania pozostały spójne.
|
||||||
|
- **Zaakceptowane:** zachowanie selektora aparatu zależy od przeglądarki; działa na docelowym telefonie.
|
||||||
|
|
||||||
|
## Plan 20260713-2111 — skanowanie paragonu kamerą telefonu
|
||||||
|
- **Jedno źródło uploadu:** użyć istniejącego pola `receipt` i `Receipts::create`; bez drugiego endpointu, kontrolera lub parsera.
|
||||||
|
- **Natywna funkcja platformy:** `capture="environment"`; bez WebRTC, JavaScriptu i nowej zależności.
|
||||||
|
- **Zgodność urządzeń:** `capture` jest wskazówką dla przeglądarki; wymagany manualny UAT na docelowym telefonie.
|
||||||
|
- **Regresja formatów:** zachować istniejący `accept` dla JPG/JPEG/PNG/PDF/JSON i sprawdzić wybór pliku obok aparatu.
|
||||||
|
- **Odroczone:** podgląd na żywo, kadrowanie, kompresja i wiele zdjęć — brak wymagania (YAGNI).
|
||||||
|
|
||||||
## Post-apply 20260712-1647
|
## Post-apply 20260712-1647
|
||||||
- **Obsłużone:** osobne tokeny urządzeń, hash walidatora, `hash_equals`, selektywne wylogowanie.
|
- **Obsłużone:** osobne tokeny urządzeń, hash walidatora, `hash_equals`, selektywne wylogowanie.
|
||||||
- **Obsłużone operacyjnie:** tabela utworzona na zdalnej bazie po wykryciu błędu braku migracji.
|
- **Obsłużone operacyjnie:** tabela utworzona na zdalnej bazie po wykryciu błędu braku migracji.
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
Tryb: plan
|
Plan scan: 2026-07-13T21:11:21+02:00
|
||||||
Data: 2026-07-12T16:47:19+02:00
|
Scope: skanowanie paragonu bezpośrednio z kamery telefonu
|
||||||
Projekt: C-visual-studio-code-finansePRO
|
Project: C-visual-studio-code-finansePRO
|
||||||
Status indeksu: indexed, moderate, 789 nodes, 1015 edges
|
Index status: ready; 789 nodes; 1015 edges
|
||||||
|
Relevant symbols:
|
||||||
Zapytania: logowanie, sesja, filtr auth, logout, remember.
|
- app/Controllers/Receipts.php: Receipts::new
|
||||||
|
- app/Controllers/Receipts.php: Receipts::create (lines 41-147; accepts JPG/JPEG/PNG and routes them through existing parser)
|
||||||
Najważniejsze pliki:
|
- app/Views/receipts/new.php: existing multipart input name="receipt", accepts image/* plus PDF/JSON
|
||||||
- `app/Controllers/Auth.php`
|
- app/Config/Routes.php: GET receipts/new, POST receipts
|
||||||
- `app/Filters/AuthFilter.php`
|
Impact conclusion: native HTML capture hint in the existing view; no backend, route, parser, model, or database change.
|
||||||
- `app/Views/auth/login.php`
|
Trace note: trace_path did not resolve the qualified method despite search_graph result; code snippet and connected graph search were sufficient for this one-view change.
|
||||||
- `app/Config/Session.php`
|
jscpd: disabled by policy
|
||||||
- `app/Config/Cookie.php`
|
ast-grep: disabled by policy
|
||||||
|
|
||||||
Wniosek: osobne trwałe tokeny per urządzenie odtwarzające istniejącą sesję; bez wydłużania globalnej sesji.
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
Tryb: post-apply
|
Post-apply scan: 2026-07-13T21:29:46+02:00
|
||||||
Data: 2026-07-12T17:31:28+02:00
|
Plan: 20260713-2111-skanowanie-paragonu-kamera
|
||||||
Projekt: C-visual-studio-code-finansePRO
|
Project: C-visual-studio-code-finansePRO
|
||||||
|
Index refreshed in fast mode: ready; 835 nodes; 1048 edges
|
||||||
- Indeks moderate odświeżony: 789 nodes, 1015 edges.
|
Modified source files:
|
||||||
- `search_graph` nie zwrócił nowych symboli `RememberTokenModel`.
|
- app/Views/receipts/new.php
|
||||||
- `search_code` zakończył się błędem tworzenia pliku tymczasowego.
|
- app/Views/receipts/review.php
|
||||||
- Fallback: bezpośredni lint 6 zmienionych plików — wszystkie poprawne.
|
Impact conclusion:
|
||||||
- Zdalna tabela `remember_tokens`: 6 kolumn, 3 indeksy.
|
- Existing Receipts::create flow, routes, parser, models, and database remain unchanged.
|
||||||
- PHPUnit: niedostępny, brak `vendor/bin/phpunit`.
|
- Native camera capture reuses the receipt upload field.
|
||||||
|
- Mobile review layout changes presentation only; form names and grouping script remain intact.
|
||||||
Status: degraded — kod i schemat sprawdzone, brak automatycznego testu runtime.
|
Verification:
|
||||||
|
- php -l both views: pass
|
||||||
|
- mobile UAT camera and review: approved by user
|
||||||
|
jscpd: disabled by policy
|
||||||
|
ast-grep: disabled by policy
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Tooling Status
|
# Tooling Status
|
||||||
|
|
||||||
- Timestamp: 2026-07-12T17:31:28+02:00
|
- Timestamp: 2026-07-13T21:29:46+02:00
|
||||||
- Scan mode: post-apply
|
- Scan mode: post-apply
|
||||||
- codebase-memory-mcp: degraded; indeks odświeżony, ale nowe symbole nie zostały zwrócone, a `search_code` zgłosił błąd pliku tymczasowego
|
- codebase-memory-mcp: ok; CLI 0.6.1, indeks odświeżony w trybie fast (835 węzłów, 1048 krawędzi)
|
||||||
- fallback: `php -l` dla 6 plików — pass
|
- Queries: `index_repository`, `index_status`, `search_graph`
|
||||||
- PHPUnit: unavailable (`vendor/bin/phpunit` missing)
|
- PHP syntax: pass dla `app/Views/receipts/new.php` i `app/Views/receipts/review.php`
|
||||||
|
- Mobile UAT: pass, potwierdzony przez użytkownika
|
||||||
- jscpd: disabled by policy
|
- jscpd: disabled by policy
|
||||||
- ast-grep: disabled by policy
|
- ast-grep: disabled by policy
|
||||||
- Raw output: `.paul/codebase/radar/codebase-memory-post-apply.txt`
|
- Raw output: `.paul/codebase/radar/codebase-memory-post-apply.txt`
|
||||||
- Next action: opcjonalny UAT dwóch profili przeglądarki.
|
- Next action: pętla zamknięta; gotowe do kolejnego planu.
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260713-2111-skanowanie-paragonu-kamera
|
||||||
|
title: Skanowanie paragonu kamerą telefonu
|
||||||
|
storage: plan-first
|
||||||
|
legacy_phase: null
|
||||||
|
created: 2026-07-13T21:11:21+02:00
|
||||||
|
status: planned
|
||||||
|
type: execute
|
||||||
|
autonomous: false
|
||||||
|
delegation: auto
|
||||||
|
files_modified:
|
||||||
|
- app/Views/receipts/new.php
|
||||||
|
quality_radar: ok
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Cel
|
||||||
|
Umożliwić wykonanie zdjęcia paragonu tylną kamerą telefonu bezpośrednio z formularza importu.
|
||||||
|
|
||||||
|
## Uzasadnienie
|
||||||
|
Użytkownik nie musi wcześniej zapisywać zdjęcia ani wybierać go z galerii; wykonane zdjęcie trafia do istniejącego importu i analizy AI.
|
||||||
|
|
||||||
|
## Wynik
|
||||||
|
Formularz `receipts/new` korzysta z natywnego mechanizmu HTML `capture="environment"`, zachowując dotychczasowy upload JPG/PNG/PDF/JSON i istniejący endpoint `Receipts::create`.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Dokumentacja projektu
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@.paul/plans/20260706-2046-import-paragonow/SUMMARY.md
|
||||||
|
@.paul/codebase/architecture.md
|
||||||
|
@.paul/codebase/impact_map.md
|
||||||
|
@.paul/codebase/quality_risks.md
|
||||||
|
|
||||||
|
## Pliki źródłowe
|
||||||
|
@app/Views/receipts/new.php
|
||||||
|
@app/Controllers/Receipts.php
|
||||||
|
@app/Config/Routes.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<clarifications>
|
||||||
|
- Przyjęto natywny wybór tylnej kamery telefonu; nie jest potrzebny podgląd obrazu na żywo ani własny interfejs aparatu.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
<impact_scan>
|
||||||
|
## Quality Radar
|
||||||
|
|
||||||
|
**Status:** ok
|
||||||
|
**Narzędzia:** codebase-memory-mcp 0.6.1 (indeks gotowy, 789 węzłów / 1015 krawędzi); jscpd i ast-grep wyłączone polityką.
|
||||||
|
|
||||||
|
## Obszary wpływu
|
||||||
|
|
||||||
|
- `app/Views/receipts/new.php` — jedyne miejsce wymagające zmiany: natywna wskazówka użycia tylnej kamery i jasny tekst formularza.
|
||||||
|
- `Receipts::create` — bez zmian; już waliduje, zapisuje i parsuje JPG/JPEG/PNG do 10 MB.
|
||||||
|
- Routing, parser AI, modele i baza danych — bez zmian.
|
||||||
|
- Weryfikacja — telefon z przeglądarką mobilną oraz kontrola zachowania dotychczasowego wyboru pliku.
|
||||||
|
|
||||||
|
## Ryzyka duplikacji / hardcodowania
|
||||||
|
|
||||||
|
- Nie tworzyć drugiego endpointu ani drugiego przepływu uploadu; zdjęcie ma użyć istniejącego pola `receipt` i `Receipts::create`.
|
||||||
|
- Atrybut `capture` jest wskazówką dla przeglądarki, więc zachowanie aparatu zależy od telefonu; obsłużone przez checkpoint UAT.
|
||||||
|
|
||||||
|
## Jawne odroczenia
|
||||||
|
|
||||||
|
- WebRTC, własny podgląd kamery, kadrowanie, kompresja i skanowanie wielu zdjęć — poza zakresem; dodać dopiero po osobnym wymaganiu.
|
||||||
|
</impact_scan>
|
||||||
|
|
||||||
|
<!-- Brak SPECIAL-FLOWS.md — sekcja skills pominięta. -->
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Uruchomienie tylnej kamery na telefonie
|
||||||
|
```gherkin
|
||||||
|
Given użytkownik otworzył formularz nowego importu na obsługiwanym telefonie
|
||||||
|
When wybiera pole skanu paragonu
|
||||||
|
Then przeglądarka umożliwia wykonanie zdjęcia tylną kamerą
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Zdjęcie korzysta z istniejącego importu
|
||||||
|
```gherkin
|
||||||
|
Given użytkownik wykonał zdjęcie paragonu w formularzu
|
||||||
|
When wysyła formularz
|
||||||
|
Then zdjęcie trafia jako pole `receipt` do `Receipts::create` i przechodzi dotychczasowy przepływ parsowania oraz przeglądu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Dotychczasowe formaty pozostają dostępne
|
||||||
|
```gherkin
|
||||||
|
Given użytkownik nie chce wykonać zdjęcia aparatem
|
||||||
|
When korzysta z formularza importu
|
||||||
|
Then nadal może przekazać obsługiwany plik JPG, JPEG, PNG, PDF lub JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Włączyć natywne przechwytywanie zdjęcia</name>
|
||||||
|
<files>app/Views/receipts/new.php</files>
|
||||||
|
<action>
|
||||||
|
Dodać do istniejącego `input type="file"` atrybut `capture="environment"`, bez JavaScriptu i bez nowego formularza lub endpointu. Zachować `name="receipt"`, `required`, wszystkie obecne typy `accept` oraz limit opisany w UI. Zmienić etykietę i krótki tekst pomocniczy tak, aby użytkownik telefonu rozumiał możliwość zrobienia zdjęcia i dalszą analizę AI.
|
||||||
|
</action>
|
||||||
|
<verify>`php -l app/Views/receipts/new.php` oraz kontrola źródła: dokładnie jedno pole `name="receipt"` z `capture="environment"`.</verify>
|
||||||
|
<done>AC-1, AC-2 i AC-3 są pokryte w kodzie formularza, a dotychczasowy backend pozostaje nietknięty.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: Potwierdzić import zdjęcia na telefonie</name>
|
||||||
|
<files>app/Views/receipts/new.php</files>
|
||||||
|
<action>
|
||||||
|
Na telefonie otworzyć `/receipts/new`, uruchomić pole pliku, wykonać zdjęcie paragonu tylną kamerą i wysłać formularz. Potwierdzić przejście do przeglądu rozpoznanych pozycji. Następnie sprawdzić, że formularz nadal pozwala wybrać wcześniej istniejący obsługiwany plik.
|
||||||
|
</action>
|
||||||
|
<verify>Manualny UAT na docelowym telefonie: aparat tylny → zdjęcie → upload → ekran przeglądu; dodatkowo wybór istniejącego pliku.</verify>
|
||||||
|
<done>AC-1, AC-2 i AC-3 potwierdzone na urządzeniu mobilnym.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
## Nie zmieniać
|
||||||
|
- `app/Controllers/Receipts.php`, `app/Libraries/ReceiptParser.php`, tras, modeli, migracji ani schematu bazy.
|
||||||
|
- Istniejącego limitu 10 MB, whitelisty rozszerzeń oraz półautomatycznego zatwierdzania pozycji.
|
||||||
|
|
||||||
|
## Limity zakresu
|
||||||
|
- Jedno zdjęcie / jeden plik na import.
|
||||||
|
- Bez WebRTC, JavaScriptowego podglądu, kadrowania, filtrów obrazu, kompresji i galerii wielu stron.
|
||||||
|
- Bez nowych zależności.
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] `php -l app/Views/receipts/new.php`
|
||||||
|
- [ ] Pole `receipt` zawiera `capture="environment"` i zachowuje obecny `accept`.
|
||||||
|
- [ ] Manualny UAT aparatu i dotychczasowego wyboru pliku na telefonie.
|
||||||
|
- [ ] Nie zmieniono backendu, routingu ani bazy danych.
|
||||||
|
- [ ] Ryzyka Quality Radar obsłużono lub jawnie odroczono.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- [ ] Wszystkie AC przechodzą.
|
||||||
|
- [ ] Weryfikacja automatyczna i mobilna jest zakończona.
|
||||||
|
- [ ] Zmiana obejmuje tylko istniejący widok formularza.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
SUMMARY.md path: `.paul/plans/20260713-2111-skanowanie-paragonu-kamera/SUMMARY.md`
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
plan_id: 20260713-2111-skanowanie-paragonu-kamera
|
||||||
|
title: Skanowanie paragonu kamerą telefonu
|
||||||
|
completed: 2026-07-13T21:29:46+02:00
|
||||||
|
storage: plan-first
|
||||||
|
quality_radar: ok
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary: Skanowanie paragonu kamerą telefonu
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Umożliwić wykonanie zdjęcia paragonu tylną kamerą telefonu bezpośrednio z formularza importu i przekazać je do istniejącej analizy AI.
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
| Obszar | Rezultat |
|
||||||
|
|--------|----------|
|
||||||
|
| Import | Istniejące pole `receipt` otrzymało natywne `capture="environment"`; zachowano wybór JPG/JPEG/PNG/PDF/JSON. |
|
||||||
|
| Przegląd AI | Tabela pozycji zmienia się na czytelne karty na ekranach poniżej `md`; przyciski i podgląd księgowania układają się responsywnie. |
|
||||||
|
| Backend | Bez zmian; oba widoki korzystają z istniejącego `Receipts::create`, nazw pól i logiki grupowania. |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `app/Views/receipts/new.php` — uruchomienie tylnej kamery i doprecyzowany tekst formularza.
|
||||||
|
- `app/Views/receipts/review.php` — mobilny układ pozycji po analizie AI.
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Kryterium | Status | Dowód |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: tylna kamera telefonu | Pass | `capture="environment"`; użytkownik zatwierdził UAT na telefonie. |
|
||||||
|
| AC-2: istniejący import | Pass | Zachowano pojedyncze `name="receipt"`; zdjęcie przeszło do ekranu przeglądu. |
|
||||||
|
| AC-3: dotychczasowe formaty | Pass | Zachowano pełne `accept` dla JPG/JPEG/PNG/PDF/JSON; UAT zatwierdzony. |
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
| Kontrola | Wynik | Uwagi |
|
||||||
|
|----------|-------|-------|
|
||||||
|
| `php -l app/Views/receipts/new.php` | Pass | Brak błędów składni. |
|
||||||
|
| `php -l app/Views/receipts/review.php` | Pass | Brak błędów składni. |
|
||||||
|
| Kontrola formularza | Pass | Jedno pole `receipt`, zachowane `required`, `accept`, limit 10 MB i UTF-8 bez BOM. |
|
||||||
|
| Kontrola granic | Pass | Kontroler, parser, routing, modele i baza bez zmian. |
|
||||||
|
| Mobilny UAT | Pass | Użytkownik potwierdził aparat oraz poprawiony ekran przeglądu. |
|
||||||
|
|
||||||
|
## Quality Radar Results
|
||||||
|
|
||||||
|
**Status:** ok
|
||||||
|
|
||||||
|
- Nowe ryzyka: brak.
|
||||||
|
- Rozwiązane ryzyka: mobilny overflow formularza przeglądu usunięty bez drugiego przepływu uploadu.
|
||||||
|
- Odroczone ryzyka: zachowanie `capture` zależy od przeglądarki; potwierdzone na docelowym telefonie. WebRTC, kadrowanie i wiele zdjęć pozostają poza zakresem.
|
||||||
|
- Raw outputs: `.paul/codebase/radar/codebase-memory-post-apply.txt`
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
- Po pierwszym UAT użytkownik zgłosił rozjeżdżający się mobilny formularz po analizie AI. Zakres rozszerzono o `app/Views/receipts/review.php`; zmiana nie naruszyła backendu ani kontraktu formularza.
|
||||||
|
|
||||||
|
## Key Decisions / Patterns
|
||||||
|
|
||||||
|
- Natywne HTML i responsywny CSS/Bootstrap zamiast WebRTC lub nowej zależności.
|
||||||
|
- Jeden istniejący endpoint i jedno źródło logiki importu.
|
||||||
|
|
||||||
|
## Follow-up
|
||||||
|
|
||||||
|
- Poprawić mobilny widok historii importów — osobne zlecenie użytkownika po zamknięciu tej pętli.
|
||||||
Vendored
+150
-26
@@ -58,16 +58,22 @@
|
|||||||
"lmtime": 1783364116680,
|
"lmtime": 1783364116680,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"2026-07-06-200005_AddDisplayNameToReceiptItems.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 580,
|
||||||
|
"lmtime": 1783366821444,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
".gitkeep": {
|
".gitkeep": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 0,
|
"size": 0,
|
||||||
"lmtime": 0,
|
"lmtime": 0,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"2026-07-06-200005_AddDisplayNameToReceiptItems.php": {
|
"2026-07-12-160001_CreateRememberTokens.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 580,
|
"size": 1003,
|
||||||
"lmtime": 1783366821444,
|
"lmtime": 1783867980803,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,8 +123,14 @@
|
|||||||
},
|
},
|
||||||
"ReceiptModel.php": {
|
"ReceiptModel.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 937,
|
"size": 1082,
|
||||||
"lmtime": 1783364152838,
|
"lmtime": 1783870528100,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"RememberTokenModel.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2015,
|
||||||
|
"lmtime": 1783868007680,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"ValuationModel.php": {
|
"ValuationModel.php": {
|
||||||
@@ -145,8 +157,8 @@
|
|||||||
"Controllers": {
|
"Controllers": {
|
||||||
"Auth.php": {
|
"Auth.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1314,
|
"size": 2159,
|
||||||
"lmtime": 0,
|
"lmtime": 1783868092101,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"BaseController.php": {
|
"BaseController.php": {
|
||||||
@@ -199,8 +211,8 @@
|
|||||||
},
|
},
|
||||||
"Receipts.php": {
|
"Receipts.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 11901,
|
"size": 12169,
|
||||||
"lmtime": 1783366894415,
|
"lmtime": 1783870528101,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"Valuations.php": {
|
"Valuations.php": {
|
||||||
@@ -219,7 +231,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Views": {
|
"Views": {
|
||||||
"auth": {},
|
"auth": {
|
||||||
|
"login.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1378,
|
||||||
|
"lmtime": 1783868092103,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"categories": {},
|
"categories": {},
|
||||||
"dashboard": {},
|
"dashboard": {},
|
||||||
"errors": {},
|
"errors": {},
|
||||||
@@ -229,8 +248,8 @@
|
|||||||
"layout": {
|
"layout": {
|
||||||
"main.php": {
|
"main.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 3815,
|
"size": 3980,
|
||||||
"lmtime": 1783364283479,
|
"lmtime": 1783870528102,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,6 +300,14 @@
|
|||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Filters": {
|
||||||
|
"AuthFilter.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1020,
|
||||||
|
"lmtime": 1783868092102,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"builds": {
|
"builds": {
|
||||||
@@ -340,26 +367,57 @@
|
|||||||
"size": 4897,
|
"size": 4897,
|
||||||
"lmtime": 1783367953541,
|
"lmtime": 1783367953541,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"2026-07-12.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 634,
|
||||||
|
"lmtime": 1783870371082,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"codebase": {
|
"codebase": {
|
||||||
"impact_map.md": {
|
"impact_map.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 3932,
|
"size": 4777,
|
||||||
"lmtime": 1783368033490,
|
"lmtime": 1783870371083,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"quality_risks.md": {
|
"quality_risks.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2521,
|
"size": 3382,
|
||||||
"lmtime": 1783368045743,
|
"lmtime": 1783870371084,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"radar": {},
|
"radar": {
|
||||||
|
"codebase-memory-plan.txt": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 483,
|
||||||
|
"lmtime": 1783867802646,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"codebase-memory-post-apply.txt": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 553,
|
||||||
|
"lmtime": 1783870371080,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"tooling_status.md": {
|
"tooling_status.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1103,
|
"size": 513,
|
||||||
"lmtime": 1783364442507,
|
"lmtime": 1783870371086,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"architecture.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 652,
|
||||||
|
"lmtime": 1783870371081,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"db_schema.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 598,
|
||||||
|
"lmtime": 1783870371081,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -371,6 +429,8 @@
|
|||||||
},
|
},
|
||||||
"handoffs": {},
|
"handoffs": {},
|
||||||
"plans": {
|
"plans": {
|
||||||
|
"20260706-1248-fundament-wydatki-przychody": {},
|
||||||
|
"20260706-1430-modul-inwestycji": {},
|
||||||
"20260706-2046-import-paragonow": {
|
"20260706-2046-import-paragonow": {
|
||||||
"PLAN.md": {
|
"PLAN.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
@@ -384,19 +444,42 @@
|
|||||||
"lmtime": 1783367919539,
|
"lmtime": 1783367919539,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"20260712-1647-zapamietywanie-logowania": {
|
||||||
|
"PLAN.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 7269,
|
||||||
|
"lmtime": 1783867802645,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"SUMMARY.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 3439,
|
||||||
|
"lmtime": 1783870407435,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PROJECT.md": {
|
"PROJECT.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 645,
|
"size": 1407,
|
||||||
"lmtime": 0,
|
"lmtime": 1783870371088,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"ast-grep": {},
|
||||||
|
"jscpd": {}
|
||||||
|
},
|
||||||
|
"run_remember_tokens_migration.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1746,
|
||||||
|
"lmtime": 1783869602000,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"rules": {},
|
|
||||||
"STATE.md": {
|
"STATE.md": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2648,
|
"size": 1853,
|
||||||
"lmtime": 1783368004681,
|
"lmtime": 1783870407436,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -451,9 +534,50 @@
|
|||||||
"lmtime": 0,
|
"lmtime": 0,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"tests": {},
|
"tests": {
|
||||||
|
"unit": {
|
||||||
|
"HealthTest.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1440,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"RememberTokenModelTest.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 914,
|
||||||
|
"lmtime": 1783868195270,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"ReceiptModelTest.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 737,
|
||||||
|
"lmtime": 1783870528103,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vendor": {},
|
"vendor": {},
|
||||||
"writable": {}
|
"writable": {},
|
||||||
|
".codebase-memory": {
|
||||||
|
".gitattributes": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 123,
|
||||||
|
"lmtime": 1783867604274,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"artifact.json": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 263,
|
||||||
|
"lmtime": 1783867604273,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"graph.db.zst": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 315382,
|
||||||
|
"lmtime": 1783867604244,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$version": 1
|
"$version": 1
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2 mb-3">
|
||||||
<h1 class="h4 mb-0">Instrumenty</h1>
|
<h1 class="h4 mb-0">Instrumenty</h1>
|
||||||
<a href="<?= site_url('instruments/new') ?>" class="btn btn-primary">+ Nowy instrument</a>
|
<a href="<?= site_url('instruments/new') ?>" class="btn btn-primary">+ Nowy instrument</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle instrument-history">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Instrument</th>
|
<th>Instrument</th>
|
||||||
@@ -25,17 +25,17 @@
|
|||||||
<?php if (empty($instruments)): ?>
|
<?php if (empty($instruments)): ?>
|
||||||
<tr><td colspan="6" class="text-center text-muted py-4">Brak instrumentów.</td></tr>
|
<tr><td colspan="6" class="text-center text-muted py-4">Brak instrumentów.</td></tr>
|
||||||
<?php else: foreach ($instruments as $i): ?>
|
<?php else: foreach ($instruments as $i): ?>
|
||||||
<tr>
|
<tr class="instrument-row">
|
||||||
<td><?= esc($i['name']) ?></td>
|
<td data-label="Instrument"><?= esc($i['name']) ?></td>
|
||||||
<td class="text-end"><?= $fmt($i['net_invested']) ?></td>
|
<td data-label="Wpłacono netto" class="text-end"><?= $fmt($i['net_invested']) ?></td>
|
||||||
<td class="text-end"><?= $i['has_value'] ? $fmt($i['value']) : '<span class="text-muted">brak wyceny</span>' ?></td>
|
<td data-label="Wartość" class="text-end"><?= $i['has_value'] ? $fmt($i['value']) : '<span class="text-muted">brak wyceny</span>' ?></td>
|
||||||
<td class="text-end <?= $i['profit'] >= 0 ? 'amount-income' : 'amount-expense' ?>">
|
<td data-label="Zarobek" class="text-end <?= $i['profit'] >= 0 ? 'amount-income' : 'amount-expense' ?>">
|
||||||
<?= $i['has_value'] ? $fmt($i['profit']) : '—' ?>
|
<?= $i['has_value'] ? $fmt($i['profit']) : '—' ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end <?= $i['profit'] >= 0 ? 'amount-income' : 'amount-expense' ?>">
|
<td data-label="Procent" class="text-end <?= $i['profit'] >= 0 ? 'amount-income' : 'amount-expense' ?>">
|
||||||
<?= $i['percent'] !== null && $i['has_value'] ? number_format($i['percent'], 2, ',', ' ') . '%' : '—' ?>
|
<?= $i['percent'] !== null && $i['has_value'] ? number_format($i['percent'], 2, ',', ' ') . '%' : '—' ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td data-label="Akcje" class="text-end instrument-actions">
|
||||||
<a href="<?= site_url('instruments/' . $i['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
<a href="<?= site_url('instruments/' . $i['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
||||||
<a href="<?= site_url('instruments/' . $i['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
<a href="<?= site_url('instruments/' . $i['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
||||||
onclick="return confirm('Usunąć instrument?')">Usuń</a>
|
onclick="return confirm('Usunąć instrument?')">Usuń</a>
|
||||||
@@ -46,4 +46,33 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.instrument-history thead { display: none; }
|
||||||
|
.instrument-history, .instrument-history tbody,
|
||||||
|
.instrument-history .instrument-row, .instrument-history .instrument-row td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.instrument-history .instrument-row { padding: .75rem; border-bottom: 1px solid var(--bs-border-color); }
|
||||||
|
.instrument-history .instrument-row td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .3rem 0;
|
||||||
|
border: 0;
|
||||||
|
text-align: right !important;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.instrument-history .instrument-row td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .875em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.instrument-history .instrument-actions { align-items: center; flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2 mb-3">
|
||||||
<h1 class="h4 mb-0">Operacje inwestycyjne</h1>
|
<h1 class="h4 mb-0">Operacje inwestycyjne</h1>
|
||||||
<a href="<?= site_url('investments/operations/new') ?>" class="btn btn-primary">+ Nowa operacja</a>
|
<a href="<?= site_url('investments/operations/new') ?>" class="btn btn-primary">+ Nowa operacja</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle inv-operation-history">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
@@ -63,19 +63,19 @@
|
|||||||
<tr><td colspan="6" class="text-center text-muted py-4">Brak operacji dla wybranych filtrów.</td></tr>
|
<tr><td colspan="6" class="text-center text-muted py-4">Brak operacji dla wybranych filtrów.</td></tr>
|
||||||
<?php else: foreach ($operations as $o): ?>
|
<?php else: foreach ($operations as $o): ?>
|
||||||
<?php $isDep = $o['type'] === 'deposit'; ?>
|
<?php $isDep = $o['type'] === 'deposit'; ?>
|
||||||
<tr>
|
<tr class="operation-row">
|
||||||
<td><?= esc($o['date']) ?></td>
|
<td data-label="Data"><?= esc($o['date']) ?></td>
|
||||||
<td><?= esc($o['instrument_name'] ?? '—') ?></td>
|
<td data-label="Instrument"><?= esc($o['instrument_name'] ?? '—') ?></td>
|
||||||
<td>
|
<td data-label="Typ">
|
||||||
<span class="badge <?= $isDep ? 'bg-success' : 'bg-secondary' ?>">
|
<span class="badge <?= $isDep ? 'bg-success' : 'bg-secondary' ?>">
|
||||||
<?= $isDep ? 'Wpłata' : 'Wypłata' ?>
|
<?= $isDep ? 'Wpłata' : 'Wypłata' ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted"><?= esc($o['description'] ?? '') ?></td>
|
<td data-label="Opis" class="text-muted"><?= esc($o['description'] ?? '') ?></td>
|
||||||
<td class="text-end <?= $isDep ? 'amount-income' : 'amount-expense' ?>">
|
<td data-label="Kwota" class="text-end <?= $isDep ? 'amount-income' : 'amount-expense' ?>">
|
||||||
<?= $isDep ? '+' : '−' ?><?= $fmt($o['amount']) ?>
|
<?= $isDep ? '+' : '−' ?><?= $fmt($o['amount']) ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td data-label="Akcje" class="text-end operation-actions">
|
||||||
<a href="<?= site_url('investments/operations/' . $o['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
<a href="<?= site_url('investments/operations/' . $o['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
||||||
<a href="<?= site_url('investments/operations/' . $o['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
<a href="<?= site_url('investments/operations/' . $o['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
||||||
onclick="return confirm('Usunąć operację?')">Usuń</a>
|
onclick="return confirm('Usunąć operację?')">Usuń</a>
|
||||||
@@ -86,4 +86,33 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.inv-operation-history thead { display: none; }
|
||||||
|
.inv-operation-history, .inv-operation-history tbody,
|
||||||
|
.inv-operation-history .operation-row, .inv-operation-history .operation-row td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-operation-history .operation-row { padding: .75rem; border-bottom: 1px solid var(--bs-border-color); }
|
||||||
|
.inv-operation-history .operation-row td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .3rem 0;
|
||||||
|
border: 0;
|
||||||
|
text-align: right !important;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.inv-operation-history .operation-row td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .875em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.inv-operation-history .operation-actions { align-items: center; flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ $badges = [
|
|||||||
];
|
];
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2 mb-3">
|
||||||
<h1 class="h4 mb-0">Import paragonów</h1>
|
<h1 class="h4 mb-0">Import paragonów</h1>
|
||||||
<a href="<?= site_url('receipts/new') ?>" class="btn btn-primary">+ Nowy import</a>
|
<a href="<?= site_url('receipts/new') ?>" class="btn btn-primary">+ Nowy import</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle receipt-history">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
@@ -34,14 +34,14 @@ $badges = [
|
|||||||
<?php if (empty($receipts)): ?>
|
<?php if (empty($receipts)): ?>
|
||||||
<tr><td colspan="7" class="text-center text-muted py-4">Brak zaimportowanych paragonów.</td></tr>
|
<tr><td colspan="7" class="text-center text-muted py-4">Brak zaimportowanych paragonów.</td></tr>
|
||||||
<?php else: foreach ($receipts as $r): ?>
|
<?php else: foreach ($receipts as $r): ?>
|
||||||
<tr>
|
<tr class="receipt-row">
|
||||||
<td><?= esc($r['receipt_date'] ?: mb_substr((string) $r['created_at'], 0, 10)) ?></td>
|
<td data-label="Data"><?= esc($r['receipt_date'] ?: mb_substr((string) $r['created_at'], 0, 10)) ?></td>
|
||||||
<td><?= esc($r['merchant'] ?: '—') ?></td>
|
<td data-label="Sprzedawca"><?= esc($r['merchant'] ?: '—') ?></td>
|
||||||
<td class="text-end"><?= $r['total'] !== null ? $fmt($r['total']) : '—' ?></td>
|
<td data-label="Suma" class="text-end"><?= $r['total'] !== null ? $fmt($r['total']) : '—' ?></td>
|
||||||
<td class="text-center"><?= (int) $r['items_count'] ?></td>
|
<td data-label="Pozycje" class="text-center"><?= (int) $r['items_count'] ?></td>
|
||||||
<td class="text-center"><?= (int) $r['operations_count'] ?></td>
|
<td data-label="Operacje" class="text-center"><?= (int) $r['operations_count'] ?></td>
|
||||||
<td><span class="badge bg-<?= $badges[$r['status']] ?? 'secondary' ?>"><?= esc($r['status']) ?></span></td>
|
<td data-label="Status"><span class="badge bg-<?= $badges[$r['status']] ?? 'secondary' ?>"><?= esc($r['status']) ?></span></td>
|
||||||
<td class="text-end">
|
<td data-label="Akcje" class="text-end receipt-actions">
|
||||||
<?php if ($r['status'] === 'parsed'): ?>
|
<?php if ($r['status'] === 'parsed'): ?>
|
||||||
<a href="<?= site_url('receipts/' . $r['id'] . '/review') ?>" class="btn btn-sm btn-outline-primary">Przegląd</a>
|
<a href="<?= site_url('receipts/' . $r['id'] . '/review') ?>" class="btn btn-sm btn-outline-primary">Przegląd</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -56,4 +56,30 @@ $badges = [
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.receipt-history thead { display: none; }
|
||||||
|
.receipt-history, .receipt-history tbody, .receipt-history .receipt-row, .receipt-history .receipt-row td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.receipt-history .receipt-row { padding: .75rem; border-bottom: 1px solid var(--bs-border-color); }
|
||||||
|
.receipt-history .receipt-row td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .3rem 0;
|
||||||
|
border: 0;
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
.receipt-history .receipt-row td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .875em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.receipt-history .receipt-actions { align-items: center; flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<?= form_open_multipart('receipts', ['class' => 'row g-3']) ?>
|
<?= form_open_multipart('receipts', ['class' => 'row g-3']) ?>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Plik paragonu</label>
|
<label class="form-label">Zrób zdjęcie lub wybierz plik paragonu</label>
|
||||||
<input type="file" name="receipt" class="form-control" required
|
<input type="file" name="receipt" class="form-control" required
|
||||||
accept=".jpg,.jpeg,.png,.pdf,.json,image/*,application/pdf,application/json">
|
accept=".jpg,.jpeg,.png,.pdf,.json,image/*,application/pdf,application/json"
|
||||||
|
capture="environment">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
Obsługiwane formaty: skan JPG/PNG, PDF lub JSON. Maks. 10 MB.
|
Na telefonie możesz zrobić zdjęcie paragonu. Obsługiwane formaty: JPG/PNG, PDF lub JSON. Maks. 10 MB.
|
||||||
Po wgraniu AI odczyta pozycje i zaproponuje kategorie do zatwierdzenia.
|
Po wgraniu AI odczyta pozycje i zaproponuje kategorie do zatwierdzenia.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2 mb-3">
|
||||||
<h1 class="h4 mb-0">Przegląd paragonu</h1>
|
<h1 class="h4 mb-0">Przegląd paragonu</h1>
|
||||||
<a href="<?= site_url('receipts') ?>" class="btn btn-link">← Historia importów</a>
|
<a href="<?= site_url('receipts') ?>" class="btn btn-link">← Historia importów</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
(podpowiedź zaznaczona; „— pomiń —” = nie księguj)
|
(podpowiedź zaznaczona; „— pomiń —” = nie księguj)
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle receipt-items">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:40%">Pozycja</th>
|
<th style="width:40%">Pozycja</th>
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
<?php else: foreach ($items as $it): ?>
|
<?php else: foreach ($items as $it): ?>
|
||||||
<?php $sel = $it['chosen_category_id'] ?? $it['suggested_category_id']; ?>
|
<?php $sel = $it['chosen_category_id'] ?? $it['suggested_category_id']; ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="Pozycja">
|
||||||
<input type="text" name="display[<?= $it['id'] ?>]" class="form-control form-control-sm"
|
<input type="text" name="display[<?= $it['id'] ?>]" class="form-control form-control-sm"
|
||||||
value="<?= esc($it['display_name'] ?: $it['name']) ?>" maxlength="255">
|
value="<?= esc($it['display_name'] ?: $it['name']) ?>" maxlength="255">
|
||||||
<span class="text-muted small">z paragonu: <?= esc($it['name']) ?></span>
|
<span class="text-muted small">z paragonu: <?= esc($it['name']) ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Kwota (zł)">
|
||||||
<input type="number" step="0.01" min="0" name="amount[<?= $it['id'] ?>]"
|
<input type="number" step="0.01" min="0" name="amount[<?= $it['id'] ?>]"
|
||||||
class="form-control form-control-sm text-end js-amount"
|
class="form-control form-control-sm text-end js-amount"
|
||||||
value="<?= esc(number_format((float) $it['amount'], 2, '.', '')) ?>">
|
value="<?= esc(number_format((float) $it['amount'], 2, '.', '')) ?>">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Kategoria">
|
||||||
<select name="chosen_category_id[<?= $it['id'] ?>]" class="form-select form-select-sm js-cat">
|
<select name="chosen_category_id[<?= $it['id'] ?>]" class="form-select form-select-sm js-cat">
|
||||||
<option value="">— pomiń —</option>
|
<option value="">— pomiń —</option>
|
||||||
<?php foreach ($categories as $c): ?>
|
<?php foreach ($categories as $c): ?>
|
||||||
@@ -71,12 +71,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="d-grid d-sm-flex gap-2 mb-4">
|
||||||
<button type="submit" class="btn btn-primary">Zaksięguj</button>
|
<button type="submit" class="btn btn-primary">Zaksięguj</button>
|
||||||
<a href="<?= site_url('receipts/' . $receipt['id'] . '/delete') ?>" class="btn btn-outline-danger"
|
<a href="<?= site_url('receipts/' . $receipt['id'] . '/delete') ?>" class="btn btn-outline-danger"
|
||||||
onclick="return confirm('Usunąć ten import?')">Usuń import</a>
|
onclick="return confirm('Usunąć ten import?')">Usuń import</a>
|
||||||
</div>
|
</div>
|
||||||
<?= form_close() ?>
|
<?= form_close() ?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.receipt-items thead { display: none; }
|
||||||
|
.receipt-items, .receipt-items tbody, .receipt-items tr, .receipt-items td { display: block; width: 100%; }
|
||||||
|
.receipt-items tr { padding: .75rem; border-bottom: 1px solid var(--bs-border-color); }
|
||||||
|
.receipt-items td { padding: .25rem 0; border: 0; }
|
||||||
|
.receipt-items td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .875em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('scripts') ?>
|
<?= $this->section('scripts') ?>
|
||||||
@@ -104,7 +120,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
preview.innerHTML = keys.map((k) =>
|
preview.innerHTML = keys.map((k) =>
|
||||||
`<li class="list-group-item d-flex justify-content-between"><span>${groups[k].label}</span><strong>${fmt(groups[k].sum)}</strong></li>`
|
`<li class="list-group-item d-flex flex-column flex-sm-row justify-content-between gap-1"><span>${groups[k].label}</span><strong>${fmt(groups[k].sum)}</strong></li>`
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
<?php $fmt = static fn ($v) => number_format((float) $v, 2, ',', ' ') . ' zł'; ?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2 mb-3">
|
||||||
<h1 class="h4 mb-0">Wyceny</h1>
|
<h1 class="h4 mb-0">Wyceny</h1>
|
||||||
<a href="<?= site_url('investments/valuations/new') ?>" class="btn btn-primary">+ Nowa wycena</a>
|
<a href="<?= site_url('investments/valuations/new') ?>" class="btn btn-primary">+ Nowa wycena</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle valuation-history">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
@@ -44,11 +44,11 @@
|
|||||||
<?php if (empty($valuations)): ?>
|
<?php if (empty($valuations)): ?>
|
||||||
<tr><td colspan="4" class="text-center text-muted py-4">Brak wycen.</td></tr>
|
<tr><td colspan="4" class="text-center text-muted py-4">Brak wycen.</td></tr>
|
||||||
<?php else: foreach ($valuations as $v): ?>
|
<?php else: foreach ($valuations as $v): ?>
|
||||||
<tr>
|
<tr class="valuation-row">
|
||||||
<td><?= esc($v['date']) ?></td>
|
<td data-label="Data"><?= esc($v['date']) ?></td>
|
||||||
<td><?= esc($v['instrument_name'] ?? '—') ?></td>
|
<td data-label="Instrument"><?= esc($v['instrument_name'] ?? '—') ?></td>
|
||||||
<td class="text-end"><?= $fmt($v['value']) ?></td>
|
<td data-label="Wartość" class="text-end"><?= $fmt($v['value']) ?></td>
|
||||||
<td class="text-end">
|
<td data-label="Akcje" class="text-end valuation-actions">
|
||||||
<a href="<?= site_url('investments/valuations/' . $v['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
<a href="<?= site_url('investments/valuations/' . $v['id'] . '/edit') ?>" class="btn btn-sm btn-outline-secondary">Edytuj</a>
|
||||||
<a href="<?= site_url('investments/valuations/' . $v['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
<a href="<?= site_url('investments/valuations/' . $v['id'] . '/delete') ?>" class="btn btn-sm btn-outline-danger"
|
||||||
onclick="return confirm('Usunąć wycenę?')">Usuń</a>
|
onclick="return confirm('Usunąć wycenę?')">Usuń</a>
|
||||||
@@ -59,4 +59,33 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.valuation-history thead { display: none; }
|
||||||
|
.valuation-history, .valuation-history tbody,
|
||||||
|
.valuation-history .valuation-row, .valuation-history .valuation-row td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.valuation-history .valuation-row { padding: .75rem; border-bottom: 1px solid var(--bs-border-color); }
|
||||||
|
.valuation-history .valuation-row td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .3rem 0;
|
||||||
|
border: 0;
|
||||||
|
text-align: right !important;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.valuation-history .valuation-row td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: .875em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.valuation-history .valuation-actions { align-items: center; flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user