Etap 27 — Raporty: dashboard z wykresami, filtrami i rzutem bębna¶
W skrócie
Zakładka Raporty (Wydawanie → Raporty) to teraz dashboard z zakładkami i wykresami (recharts) zamiast surowych tabel. Wspólny pasek filtrów (zakres dat + maszyna) zasila wszystkie raporty na żywo (przez URL → re-render po stronie serwera). 9 raportów zgrupowanych w 5 kart.
Karty (taby)¶
| Karta | Raporty |
|---|---|
| Najpopularniejsze | #1 Top 10 produktów (słupki poziome) · #2 Top 10 użytkowników (słupki poziome) |
| Trendy | #3 Wydania miesięcznie (wykres warstwowy) · #4 Przyjęcia vs wydania (słupki grupowane) · #5 Trend roczny — wydania + przyjęcia (linie) · #9 Wypożyczenia vs zwroty (słupki grupowane) |
| Maszyny | #6 Stan maszyn — pojemność / stan / puste (słupki + tabela) · #7 Rzut bębna — mapa skrytek, puste na czerwono |
| Alerty stanów | #8 Produkty bez stanu / poniżej progu (donut OUT vs LOW + tabela) |
| Audyt | Transakcje magazynowe (IN/OUT) · dziennik decyzji (ALLOW/BLOCK) · nadpisania · akceptacje regulaminów |
Filtry¶
Pasek filtrów nad zakładkami:
- Zakres dat — presety
7 / 30 / 90 dni,12 miesięcylub własnyOd/Do(poladate). Brak zakresu → backend bierze okno domyślne (30 dni dla zużycia, 12 mies. dla trendu). - Maszyna — wszystkie albo jedna. Filtr maszyny dotyczy wszystkich raportów; rzut bębna (#7) wymaga wybrania konkretnej maszyny.
Filtry zapisują się w query-stringu (?from=&to=&machineId=&decision=) — strona to server component, więc zmiana filtra przeładowuje dane po stronie serwera. Zakładki przełączają się po stronie klienta (bez re-fetcha).
Backend — końcówki¶
Wszystko pod GET /dispensing/reports/* (rola OPERATOR/ADMIN), w dispensing/reports.service.ts:
| Końcówka | Zasila | Co liczy |
|---|---|---|
usage?groupBy=item\|user (istniejąca) |
#1, #2 | suma Movement.quantity (WITHDRAWAL, userId not null) pogrupowana, sort malejąco |
timeseries?granularity=month\|year |
#3, #4, #5, #9 | date_trunc(...) + suma per MovementType; kubełki ciągłe (luki = 0); pochodne in = restock + return, out = withdrawal |
machines-status |
#6 | per maszyna: liczba slotów, SUM(capacity), SUM(quantity), COUNT FILTER puste / częściowe / niskie |
drum/:machineId |
#7 | geometria (doorsPerColumn/columns) + każda komórka ze stanem; empty ⇔ skrytka DISPENSE z quantity ≤ 0 |
stock-alerts |
#8 | itemy (aktywne, ≥1 slot w zakresie) z SUM(quantity) ≤ 0 lub < lowStockThreshold; severity OUT/LOW |
Agregacje czasowo-/stanowe idą przez $queryRaw (date_trunc, FILTER (WHERE …), COALESCE progu slot→item). Sumy castowane ::int, by uniknąć BigInt w JSON.
Frontend¶
- Server:
app/[locale]/dashboard/wydawanie/raporty/page.tsx— guard ADMIN, równoległy fetch wszystkich raportów wg filtrów, defensywna normalizacja kształtów (część końcówek to gołe tablice, dziennik decyzji to{rows,…}). - Klient:
components/dispensing/reports-dashboard.tsx('use client') — zakładki, pasek filtrów (useRouter+ URL), wykresy recharts (Bar/Line/Area/Pie), tabela stanu maszyn, rzut bębna reużywającylib/drum-layout.ts(kolumny K×wiersze; LockerBox = płaska siatka). Paleta dobrana pod ciemny panel.
Uwagi / ograniczenia¶
- #9 Wypożyczenia i zwroty liczy ruchy
WITHDRAWALvsRETURN. Dedykowany flow wypożyczeń (KEY_RENTAL) nie jest wdrożony — do czasu jego dodania słupki zwrotów zwykle są zerowe (raport jest podpięty i uczciwy). - #8 Alerty pokazują tylko produkty, które mają już przydzielony stan (≥1
StockSlot) w zakresie — produkt bez żadnej alokacji nie jest „brakiem stanu", tylko nieskonfigurowanym.