Płatności nierozliczone
Zestawienie wszystkich nieopłaconych pozycji w wybranym okresie, pogrupowane po uczestniku — odpowiednik zakładki „Nierozliczone" znanej z panelu kluby.org.
👤 Instrukcja dla pracownika (Recepcja / Administracja)
Ścieżka: Dashboard ➔ Płatności ➔ Nierozliczone
Zakładka odpowiada na jedno pytanie: kto i ile jest nam winien w danym miesiącu. Domyślnie pokazuje bieżący miesiąc i lokalizację wybraną w górnym pasku (miasto + ulica).
1. Lista uczestników
| Kolumna | Znaczenie |
|---|---|
| Uczestnik | Osoba, której dotyczą zajęcia / rezerwacje |
| Opiekun / konto | Konto klienta, na którym wisi płatność (rodzic, opiekun prawny) |
| Pozycje | Liczba nieopłaconych pozycji w zakresie dat |
| Do zapłaty | Suma tych pozycji; kliknięcie otwiera szczegóły |
Na dole listy widoczna jest suma Łącznie dla całego zestawienia.
Wiersz to para uczestnik + konto. Jeśli płatności jednego uczestnika wiszą na dwóch adresach (tak zostaje po zmianie e-maila konta), zobaczysz dwa wiersze — każdy rozliczasz osobno, z portfelem i limitem zaległości tego właśnie konta.
Przejście do szczegółów zabiera ze sobą wszystkie aktywne filtry (zakres dat, typ produktu, lokalizacja, archiwalne, konto), więc kwota w szczegółach zgadza się z kwotą w wierszu, a „Rozlicz" nie sięga po pozycje, których lista nie liczyła.
2. Wyszukiwarka (filtry)
Przycisk Wyszukiwarka rozwija panel filtrów:
- Od / Do — zakres dat rozliczeniowych. Datą rozliczeniową jest termin zajęć, a gdy go nie ma — termin płatności, a w ostateczności data utworzenia pozycji.
- Szukaj — imię, nazwisko uczestnika lub e-mail konta.
- Typ produktu — Rezerwacje, Szkółka, Zajęcia indywidualne, Zajęcia próbne, Inne zajęcia, Półkolonie, Weekend z tenisem. Domyślnie zaznaczone wszystkie. Doładowań portfela tu nie ma: ich rozliczenie musi jednocześnie zasilić portfel, co robi wyłącznie okno doładowania przy ladzie.
- Uwzględnij ➔ Archiwalne — dokłada pozycje z profili archiwalnych.
Miasto i ulica pochodzą z globalnego selektora lokalizacji na górze ekranu, tak samo jak na pozostałych stronach finansowych.
3. Rozliczenie zbiorcze
Zaznacz uczestników i naciśnij przycisk pod listą. To, co dostajesz, zależy od tego, czy zaznaczenie mieści się w jednym koncie opiekuna.
Jedno konto → przycisk Rozlicz otwiera to samo okno rozliczenia, co reszta aplikacji (PaymentMethodDialog): karty benefitowe, zapłata z portfela z widocznym saldem, a dalej Gotówka / Karta / Płatność mieszana / Zaległość, komentarz i „Drukuj paragon". Jeśli któryś z zaznaczonych klientów prosi o fakturę, okno o tym ostrzega, zanim wydrukujesz paragon.
Kilka kont → zostaje Rozlicz jako zaległość z potwierdzeniem, a pod listą pojawia się wyjaśnienie. Gotówka, karta i płatność mieszana to jedna transakcja przy kasie: jedna osoba podaje jedną kwotę, a płatność mieszana rozdziela część gotówkową i kartową po wszystkich zaznaczonych pozycjach — w poprzek rodzin nie miałoby to pokrycia w tym, kto ile realnie zapłacił. Zaległość nie dotyka kasy ani dokumentów, więc działa dla wielu kont naraz.
Kiedy widać „Zaległość"
Zaległość pojawia się tylko dla klientów, którzy mogą ją mieć — czyli z client_settings.allow_arrears = 1. Reguły:
| Sytuacja | Co widzi pracownik |
|---|---|
| Jedno konto, ma zgodę | Przycisk Zaległość z podpisem „Pozostały limit zaległości: X zł" |
| Jedno konto, ma zgodę bez limitu | Przycisk Zaległość z podpisem „Bez limitu zaległości" |
| Jedno konto, kwota przekracza pozostały limit | Przycisk widoczny, ale nieaktywny, z informacją o przekroczeniu |
| Jedno konto, brak zgody | Przycisku nie ma w oknie w ogóle |
| Kilka kont, część ma zgodę | „N z M kont nie ma zgody na zaległości — te pozycje zostaną pominięte"; rozliczane są wyłącznie konta ze zgodą |
| Kilka kont, żadne nie ma zgody | Przycisk nieaktywny + „Żadne z zaznaczonych kont nie ma zgody na zaległości" |
Limit liczony jest tak samo jak w oknie rozliczenia rezerwacji: available = saldo portfela + limit, gdzie limit to client_settings.max_arrears_amount, a gdy go nie ma — max_arrears_amount_default z ustawień ograniczeń klienta dla miasta konta.
Komentarz wpisany w oknie zapisuje się w payment_comment na każdej rozliczanej pozycji — także przy rozliczeniu wielu pozycji naraz i przy zaległości.
W obu przypadkach system zbiera wszystkie nierozliczone pozycje zaznaczonych uczestników z aktualnie ustawionego zakresu i filtrów, a po zakończeniu pokazuje podsumowanie — ile pozycji rozliczono, a ile odrzucono (np. gdy klient nie ma zgody na zaległości albo przekroczyłby limit).
Dokumenty przy rozliczeniu zbiorczym
Zaznaczenie kilku uczestników nie tworzy jednego wspólnego paragonu. processPaymentStatusChange grupuje pozycje kluczem player-{player_id}-{payment_type} i wystawia dokument osobno dla każdego uczestnika i typu produktu. Zaległość nie generuje dokumentu w ogóle — kończy się statusem paid_wallet, a generator dokumentów bierze pod uwagę tylko paid, paid_cash, paid_card, paid_online i paid_mixed.
4. Szczegóły uczestnika
Kliknięcie nazwiska lub kwoty otwiera widok pojedynczego uczestnika:
- Lista pozycji pogrupowana po typie produktu, każda z własnym checkboxem (domyślnie wszystkie zaznaczone).
- Podsumowanie — kwoty w rozbiciu na typy produktu i łączna suma.
- Formularz opłaty — kwota do zapłaty za zaznaczone pozycje i przycisk Rozlicz.
Ponieważ tu jest jedno konto i jeden portfel, okno rozliczenia startuje od kroku „Rabaty i kwota":
- Karty benefitowe (Multisport, Medicover, PZU Sport, Fit Profit) i Zapłać z portfela z widocznym saldem klienta.
- Metoda: Gotówka, Karta, Płatność mieszana albo Zaległość, plus komentarz i „Drukuj paragon".
Po rozliczeniu następuje powrót do listy.
🛠️ Dokumentacja techniczna
Trasy
| Ścieżka | Warianty | Role |
|---|---|---|
/dashboard/unsettled-payments | admin | ADMIN, BACKOFFICE |
/dashboard/unsettled-payments/[playerId] | admin | ADMIN, BACKOFFICE |
Wpisy w route_access dodaje migracja 0266_add_unsettled_payments_route.sql; bez nich pozycja menu byłaby widoczna wyłącznie dla ADMIN-a (strony bez wiersza w route_access widzi w menu tylko ADMIN).
Pliki
| Plik | Rola |
|---|---|
lib/unsettled-payments.ts | Typy, lista typów produktu, parseProductTypes |
lib/actions/unsettled-payments.ts | Zapytania i akcja rozliczenia (server actions) |
app/(dashboard)/dashboard/unsettled-payments/page.tsx | Lista uczestników |
app/(dashboard)/dashboard/unsettled-payments/[playerId]/page.tsx | Szczegóły uczestnika |
components/tables/unsettled-payments-table/* | Filtry, tabela, widok szczegółów |
components/tables/unsettled-payments-table/settle-selected.ts | Wspólna ścieżka rozliczenia dla listy i szczegółów |
lib/invoice-notice.ts | Pobranie preferencji fakturowych dla ostrzeżenia o paragonie |
getOwnersArrearsAllowance w lib/actions/unsettled-payments.ts odpowiada na pytanie „które z tych kont mogą mieć zaległość" jednym zapytaniem na tabelę (client_settings, wallet, user.preferredCity) zamiast pary zapytań na klienta — inaczej „Zaznacz wszystkie" na pełnej liście odpalałoby kilkadziesiąt zapytań naraz. Miasto do domyślnego limitu czytane jest z konta (tak samo jak getUserCity, które sprawdza limit przy samym obciążeniu portfela), a nie z player.city — inaczej podpowiedź w oknie mówiłaby o innym limicie niż ten realnie egzekwowany.
Definicja „nierozliczonej" pozycji
p.status IN ('pending', 'overdue')
AND p.archived = 0 -- chyba że zaznaczono "Archiwalne"
AND NOT (wygasła rezerwacja online) -- payment_expires_at w przeszłości
AND NOT EXISTS (zajęcia odwołane lub zarchiwizowane)
AND date(<data rozliczeniowa PL>) BETWEEN :dateFrom AND :dateTo
AND <typ produktu> IN (:productTypes)
Data rozliczeniowa to COALESCE(g.start_time, p.due_date, p.created_at) przeliczone na czas Polski przez sqlPolandLocal — ta sama reguła, według której miesiąc wyznacza tabela płatności.
Typ produktu ≠ payment_type
Szkółka, zajęcia indywidualne i zajęcia „inne" mają ten sam payment_type = 'school' i różnią się dopiero typem rodzaju zajęć (activity_types.type). Filtr liczy więc PRODUCT_KIND_SQL:
| Typ produktu w filtrze | Warunek |
|---|---|
| Rezerwacje | payment_type = 'court_reservation' |
| Zajęcia próbne | payment_type = 'trial' |
| Półkolonie | payment_type = 'camp' |
| Weekend z tenisem | payment_type = 'tennis_course' |
| Zajęcia indywidualne | activity_types.type = 'individual' |
| Inne zajęcia | activity_types.type = 'other' |
| Szkółka | pozostałe (w tym school bez dopiętych zajęć) |
Płatność typu school, do której nie udało się dopiąć zajęć, trafia do „Szkółki" — inaczej zniknęłaby ze wszystkich filtrów naraz.
Doładowania portfela (payment_type = 'wallet_topup') są odcięte osobnym warunkiem
SQL, a nie tylko brakiem pozycji w filtrze. Zamknięcie doładowania musi też uznać
portfel — robi to jedynie settleWalletTopupAtDesk, które przy nieudanym uznaniu
cofa płatność do pending. Rozliczone stąd doładowanie zabrałoby pieniądze bez
podniesienia salda, a „Zaległość" obciążyłaby portfel kwotą, którą klient chciał go
zasilić.
Przepływ rozliczenia zbiorczego
Kolejność w settleSelectedPayments nie jest przypadkowa: portfel obciążamy przed oznaczeniem czegokolwiek jako opłacone i sprawdzamy, czy nie pokrył całej kwoty. Bez tego dopłata gotówką na wierzchu pobrałaby pieniądze drugi raz.
Rozliczenie idzie pozycja po pozycji, bo markPaymentAsArrears „zajmuje" płatność warunkowym UPDATE-em przed obciążeniem portfela — to właśnie ten warunek chroni przed podwójnym obciążeniem, gdy dwie osoby na recepcji klikną rozliczenie równocześnie.
Rabat, karty benefitowe i kwota z portfela wpisane w pierwszym kroku okna działają
tak samo dla „Zaległości", jak dla gotówki: wspólne applyPreApply obniża kwoty
płatności, zanim cokolwiek zostanie zamknięte, więc na konto klienta jako dług idzie
kwota po odliczeniach, a nie pierwotna.
Limit jednego rozliczenia
Jednorazowo rozliczanych jest maksymalnie 50 pozycji
(MAX_BULK_SETTLE_PAYMENTS). Każda zaległość to kilkanaście zapytań do D1, a cała
paczka wykonuje się w jednym wywołaniu Workera — większe zaznaczenie zostałoby
przerwane w połowie i nikt nie wiedziałby, które portfele zdążyły zostać obciążone.
Limit sprawdza zarówno przeglądarka (komunikat „Za dużo pozycji naraz"), jak i sama
akcja serwerowa, która w takim wypadku nie rozlicza niczego.
Lista, która zmieniła się w międzyczasie
Identyfikatory płatności są odczytywane dopiero przy zatwierdzeniu, razem z ich sumą. Jeśli suma różni się od tej, którą pokazywała strona (ktoś opłacił pozycję online, druga recepcja rozliczyła tego samego klienta), rozliczenie jest przerywane, lista się odświeża, a recepcja zaznacza pozycje jeszcze raz — inaczej podział na gotówkę i kartę wpisany dla starej kwoty rozłożyłby się na mniejszy zbiór pozycji i kasa pokazałaby więcej, niż wynosiły płatności.
Limity D1
Zapytania po liście identyfikatorów (uczestnicy, płatności) są dzielone na paczki poniżej limitu 100 parametrów D1, tak samo jak w lib/actions/payment.ts.