[18]
TRACKER GODZIN I GENERATOR FAKTUR DLA KANCELARII
▒ TIER C · ROK 2025 · STATUS: DZIAŁA · JĘZYKI: TYPESCRIPT
B2B SaaS domykający ścieżkę czas → stawka → faktura → raport
[FIG. 1] MISJA
Domykam pełną ścieżkę biznesową czas → stawka → faktura → raport w jednym systemie, zamiast rozwiązywać jej wycinek. Kancelaria, dla której to zbudowałem, rozliczała czas prawników arkuszami i przepisywała wyniki do systemu fakturowego ręcznie — godziny pracy i błędy wliczone w proces. W moim B2B SaaS prawnicy rejestrują czas per klient lub firma, a aplikacja mnoży go przez macierz stawek: każda para pracownik × klient ma własną stawkę godzinową, bo pojedyncza cena godziny nie opisuje takiej kancelarii. Z tego powstają faktury synchronizowane w obie strony z Fakturownią, czyli lokalnym systemem fakturowym, oraz raporty DOCX w kilku przekrojach — prawnicze, masterowe i per klient — jako załącznik do faktury. Wymagania odbierałem od klienta nietechnicznego i sam przekładałem je na model danych, łącznie z rolami: trzy role (admin / pracownik / klient) izoluje RLS w Supabase, więc klient widzi wyłącznie swoje rozliczenia, pracownik swoje wpisy, a admin całość. Vite + React + shadcn na froncie, Supabase (auth + Postgres) na backendzie, deploy na Vercelu.
[FIG. 2] ARCHITEKTURA
najedź na blok, by zobaczyć opis
[FIG. 3] WYZWANIA
[+][CH-01]
Serce rozliczeń kancelarii to macierz stawek: ten sam prawnik ma inną stawkę u każdego klienta, a klienci dzielą się na indywidualnych i firmy — każda para pracownik × klient i pracownik × firma trzyma własną stawkę godzinową. Najtrudniejsze nie było mnożenie, tylko gwarancja, że do każdego wpisu czasu przy fakturowaniu dobierze się właściwa stawka z właściwego wymiaru — pomyłka tutaj to nie bug, tylko błędna kwota na fakturze dla realnej kancelarii. Zamknąłem to w jednym przepływie: wpisy czasu × macierz stawek agregują się w pozycje faktury per klient, bez ręcznego etapu pośredniego.
[+][CH-02]
Fakturownia ma własny model świata — i brudne dane. Synchronizacja klientów musi sama rozstrzygnąć, kto jest firmą, a kto osobą fizyczną (trzystopniowa heurystyka: pole company z API → PESEL albo brak NIP-u przy imieniu i nazwisku → wzorce w nazwie), a identyfikatory podatkowe przychodzą w każdym formacie: NIP ze spacjami i myślnikami, PESEL o 11 cyfrach, zagraniczny VAT typu DE123456789. Naiwne czyszczenie do samych cyfr gubiło kod kraju, więc normalizacja ma osobną gałąź na wzorzec dwóch liter i 5–12 cyfr. Samo wystawienie faktury też okazało się serią zasadzek: CORS w edge functions wymagał nagłówków klienta Supabase, sporadyczne timeouty Fakturowni dostały pętlę retry, a kwota liczyła się przez stawkę efektywną — per-klient bije globalną — z pozycjami grupowanymi per prawnik i blokadą podwójnego zafakturowania tego samego miesiąca na unikacie bazy.
[+][CH-03]
Trzy role o sprzecznych interesach w jednej bazie rozstrzyga RLS — a RLS ma swoje pułapki. Polityki sprawdzające rolę czytają tabelę profiles, ale polityka na profiles też czyta profiles: rekurencja. Rozwiązałem ją funkcją SECURITY DEFINER get_current_user_role() z przypiętym search_path (bez tego to wektor ataku). Najlepsza lekcja przyszła jednak przy twardnieniu uprawnień: odebranie EXECUTE tej funkcji najpierw rolom anon i public było słuszne — ale chwilę później odebrałem je także authenticated, czym złamałem wszystkie polityki RLS dla zalogowanych użytkowników naraz. GRANT wrócił cztery minuty później, a cały footgun jest udokumentowany w historii zamiast zamieciony — bo błąd bezpieczeństwa, którego się nie ukrywa, jest wart więcej niż udawana nieomylność.
[+][CH-04]
Najciekawszy bug w projekcie był niewidzialny: lista firm do zafakturowania za bieżący miesiąc świeciła pustką, choć wpisy czasu istniały. Przyczyną okazał się domyślny limit 1000 wierszy PostgREST — kreator faktur ładował wszystkie wpisy posortowane rosnąco po dacie, więc przy ~1230 rekordach obcinane były właśnie te najnowsze. Diagnozę spisałem w planie, zanim powstała poprawka: przyczyna, rozwiązanie, zakres plików i reguła zapobiegawcza. Naprawa zmienia strategię pobierania — wpisy ładują się wyłącznie z wybranego okresu (zakres dat w zapytaniu) z defensywnym limitem — a reguła „nie pobieraj tabel transakcyjnych na zapas” została w projekcie na stałe. Niejawne limity platformy to dokładnie ten rodzaj wiedzy, którego nie ma w tutorialach.
[FIG. 4] WARSTWA AI
W runtime nie działa żaden model — klasyfikacja klientów firma/osoba to heurystyka regułowa, nie LLM. Sam projekt budowałem AI-native na platformie Lovable, z punktowymi interwencjami przez Codex i ręcznymi poprawkami edge functions — a najlepsze wzorce tej pracy widać w artefaktach: diagnoza limitu 1000 wierszy została spisana w planie zanim padła pierwsza linijka poprawki, razem z regułą na przyszłość „nie pobieraj tabel transakcyjnych na zapas bez zakresu daty”.
[FIG. 2A] MACIERZ STAWEK
| ACME SP. Z O.O. | BETA S.A. | GAMMA SP.K. | |
|---|---|---|---|
| MEC. K. | |||
| APL. S. | |||
| PRAW. T. |
NAJEDŹ NA KOMÓRKĘ — WYLICZENIE SPŁYWA DO FAKTURY
[FIG. 5] GALERIA



