Przejdź do treści

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ęcy lub własny Od/Do (pola date). 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ący lib/drum-layout.ts (kolumny K×wiersze; LockerBox = płaska siatka). Paleta dobrana pod ciemny panel.

Uwagi / ograniczenia

  • #9 Wypożyczenia i zwroty liczy ruchy WITHDRAWAL vs RETURN. 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.

Powiązane