Przejdź do głównej zawartości

Statystyki i analizy (Dashboard L1, Sprzedaż, LEADS CRM)

Zakładka Statystyki to centrum analityczne obiektu. Pokazuje kluczowe wskaźniki biznesowe (obrót, aktywni uczestnicy), lejek sprzedaży oraz bazę potencjalnych klientów (leadów). Statystyki są podzielone na podstrony (zakładki), a większość z nich można filtrować po okresie i lokalizacji.

👤 Instrukcja dla pracownika

Statystyki znajdziesz w menu Statystyki. Na górze każdej podstrony widoczne są zakładki przełączające widoki:

  • Uczestnicy – dotychczasowy panel z liczbą uczestników i grupami wiekowymi.
  • Dashboard L1 – najważniejsze liczby obiektu w jednym miejscu.
  • Dashboard Tygodniowy – przychód w trzech horyzontach, aktywni per lokalizacja i lejek leadów.
  • Sprzedaż – lejek leadów, źródła pozyskania i konwersje.
  • LEADS CRM – baza potencjalnych klientów z możliwością edycji i eksportu.
  • Retencja – rezygnacje (churn) i nieobecności aktywnych klientów.
  • Dosprzedaż – cross-sell, przychód per rodzina i sprzedaż eventów do obecnych klientów.
  • Obłożenie kortów – wykorzystanie kortów w szczycie, poza szczytem i w weekendy.
  • Jakość – satysfakcja klientów (CSAT), skargi i oceny trenerów.
  • Segmentacja klientów – segmenty wg rocznych wydatków, rozkład wartości i migracja segmentów.
  • Własne propozycje – RevPAR kortowy, sezonowość, polecenia i wypełnienie szkółek.

Lokalizacja (miasto) jest sterowana globalnym przełącznikiem miasta w nagłówku – wszystkie liczby przeliczają się dla wybranego obiektu. Wybór Wszystkie pokazuje dane zbiorcze.

1. Wybór okresu

Na podstronach Dashboard L1 i Sprzedaż w prawym górnym rogu znajduje się selektor okresu:

  • Tydzień – bieżący tydzień (od poniedziałku).
  • Miesiąc – bieżący miesiąc (domyślnie).
  • Kwartał – bieżący kwartał.
  • Od początku roku – od 1 stycznia do dziś.
  • Rok – bieżący rok.

Karty z deltą procentową (np. „+12,5%") porównują wybrany okres z analogicznym okresem rok wcześniej (rok do roku). Wyjątkiem jest „Tydzień", który porównuje się z poprzednim tygodniem. Jeśli w okresie porównawczym nie ma danych, delta się nie wyświetla.

2. Dashboard L1

Najważniejsze wskaźniki na poziomie zarządu:

  • Obrót (przychód całkowity) – suma wszystkich opłaconych płatności w okresie (korty, szkółki, obozy, kurs tenisa, zajęcia próbne). Pod spodem karta Przychód wg rodzaju usługi pokazuje, ile pochodzi z każdej kategorii i jaki ma udział procentowy.
  • Aktywne dzieci w szkółkach – liczba aktywnych uczestników poniżej 18 lat zapisanych do zajęć grupowych (szkółek).
  • Aktywni dorośli w szkółkach – to samo dla osób w wieku 18+.
  • Marża operacyjna(przychód − koszty operacyjne) / przychód. Koszty wpisuje się ręcznie ikoną ołówka na kafelku (okno z miesiącem i kwotami per kategoria: wynagrodzenia, najem, media, marketing, inne); sumowane są po miesiącach wybranego okresu.

3. Sprzedaż

Mierzy skuteczność pozyskiwania klientów na podstawie danych z LEADS CRM:

  • Nowe leady – liczba nowych zapytań pozyskanych w okresie (z porównaniem do poprzedniego okresu).
  • Czas pierwszego kontaktu – średni czas (w godzinach) od dodania leada do pierwszego kontaktu. Im krótszy, tym lepsza konwersja (benchmark: poniżej 1 godziny).
  • Konwersja lead → próbne – jaki procent leadów umówił się na zajęcia próbne.
  • Konwersja próbne → stały – jaki procent osób po zajęciach próbnych zapisał się na stałe.
  • Lejek leadów – wizualny lejek 5 etapów: Nowy → Skontaktowany → Umówione próbne → Odbyte próbne → Zapisany, z procentem przejścia między etapami. Pozwala od razu zobaczyć, gdzie tracimy klientów.
  • Źródła pozyskania – liczba i udział % leadów per kanał (Google Ads, Meta, Inne online, Offline, Polecenie).
  • Powody odmowy – ranking powodów, dla których leady zrezygnowały z zakupu (cena, brak terminu, lokalizacja, konkurencja, rezygnacja z decyzji, brak odpowiedzi, inne), z udziałem %. Zasilany powodem wybranym przy oznaczaniu leada jako „Odmowa".
  • ROI marketingowy per kanał – tabela zestawiająca ręcznie wpisany budżet kanału z efektami: liczba leadów, liczba pozyskanych klientów, przychód od tych klientów, CAC (koszt pozyskania klienta = budżet / klienci) i ROI (= (przychód − budżet) / budżet). Przycisk Edytuj budżety otwiera okno, w którym dla wybranego miesiąca wpisujesz kwotę budżetu dla każdego kanału.

⚠️ Te wskaźniki zadziałają tylko wtedy, gdy recepcja rzetelnie wprowadza leady w zakładce LEADS CRM i aktualizuje ich statusy (oraz wpisuje budżety marketingowe). System nie wygeneruje tych danych samodzielnie. Przychód per kanał jest liczony z płatności klientów powiązanych z leadem (pole „converted player" ustawiane przy konwersji leada na klienta).

4. LEADS CRM – obsługa leadów

Lead to potencjalny klient, który wykazał zainteresowanie (telefon, formularz, social media, polecenie), ale jeszcze nic nie kupił. Zakładka LEADS CRM to baza takich osób.

Dodawanie leada: kliknij Dodaj lead, uzupełnij dane (imię, telefon, e-mail), wybierz Źródło (skąd przyszło zapytanie) i opcjonalnie produkt zainteresowania oraz notatkę. Oznaczenie źródła jest kluczowe – to ono zasila statystyki źródeł i konwersji.

Prowadzenie leada przez lejek: w miarę kontaktów zmieniaj Status leada:

StatusZnaczenie
NowyŚwieże zapytanie, jeszcze nieobsłużone
SkontaktowanyOddzwoniliśmy / odpisaliśmy
Umówione próbneLead ma ustalony termin zajęć próbnych
Odbyte próbneLead pojawił się na zajęciach próbnych
ZapisanyZostał stałym klientem
OdmowaZrezygnował – wybierz powód odmowy z listy
NieaktywnyBrak reakcji / kontakt zawieszony

Po wybraniu statusu Odmowa pojawia się dodatkowe pole Powód odmowy (lista wyboru) – to ono zasila kartę „Powody odmowy" na podstronie Sprzedaż.

System sam zapisuje znaczniki czasu przy zmianie statusu (np. czas pierwszego kontaktu, umówienia próbnych, zapisu, odmowy), co napędza wskaźniki na podstronie Sprzedaż.

Filtrowanie i wyszukiwanie: użyj pola wyszukiwania (imię, telefon, e-mail) oraz filtrów statusu i źródła, aby szybko znaleźć potrzebne leady.

Eksport: przycisk Eksport CSV pobiera aktualnie przefiltrowaną listę leadów do pliku, który otworzysz w Excelu (raport „do druku").

Edycja: kliknij dowolny wiersz, aby otworzyć kartę leada i zaktualizować dane lub status.

5. Retencja

Mierzy, czy utrzymujemy klientów:

  • Rezygnacje miesięczne (Churn) – procent klientów, którzy stali się nieaktywni w wybranym okresie, w stosunku do aktywnych na początku okresu. Niski churn = stabilne przychody. Pod kafelkiem liczby pomocnicze: ilu zrezygnowało i ilu było aktywnych na początku.
  • Nieobecności w okresie – procent aktywnych klientów, którzy nie pojawili się na żadnych zajęciach w okresie. Wczesny sygnał ryzyka rezygnacji – warto skontaktować się proaktywnie.
  • Powody rezygnacji – ranking przyczyn rezygnacji w wybranym okresie (cena, przeprowadzka, brak czasu, jakość zajęć, kontuzja, inne). Dane wprowadza personel przyciskiem „Dodaj powód" – każdy wpis to jedna rezygnacja z wybraną przyczyną i opcjonalną notatką. Karta pokazuje liczbę wystąpień każdej przyczyny posortowaną malejąco.
  • Win-back (reaktywacja) – skuteczność akcji odzyskiwania klientów: odzyskani / wszystkie akcje. Personel rejestruje każdą próbę reaktywacji przyciskiem „Dodaj akcję" (kanał: telefon / SMS / e-mail; wynik: wrócił / odmówił / w toku). Współczynnik liczony jest tylko z akcji o wyniku „wrócił" względem wszystkich akcji w okresie.

👤 Jak używać: powody rezygnacji i akcje win-back są zasilane ręcznie. Aby dane były wiarygodne, rejestruj rezygnację od razu po jej zgłoszeniu, a każdą próbę reaktywacji – w momencie jej wykonania i ponownie po uzyskaniu odpowiedzi (zaktualizuj wynik z „w toku" na „wrócił"/„odmówił").

5a. Dosprzedaż

Mierzy, jak skutecznie zwiększamy wartość obecnych klientów (wszystkie wskaźniki liczone automatycznie z płatności i bazy graczy, bez ręcznego wprowadzania):

  • Cross-sell (2+ usługi) – odsetek aktywnych klientów, którzy w okresie zapłacili za co najmniej dwa różne typy usług (np. szkółka + wynajem kortu, szkółka + obóz). Liczony jako klienci z 2+ typami płatności / aktywni klienci. Im wyższy, tym wyższy LTV. Pod kafelkiem: liczba klientów z 2+ usługami oraz liczba aktywnych klientów (mianownik).
  • Przychód per rodzina – średni przychód przypadający na jedną rodzinę. Rodzina rozpoznawana jest po wspólnym e-mailu/telefonie opiekuna (guardian_emailguardian_phoneowner_email, a w ostateczności pojedynczy gracz). Liczony jako łączny przychód w okresie / liczba rodzin. Karta pokazuje też zmianę % względem poprzedniego okresu (YoY/poprzedni tydzień).
  • Eventy do obecnych klientów – odsetek aktywnych klientów, którzy w okresie kupili produkt eventowy (półkolonie / obozy / weekend z tenisem, tj. płatności typu camp lub tennis_course). Liczony jako klienci, którzy kupili event / aktywni klienci. Obecni klienci to najtańszy kanał sprzedaży – wskaźnik mierzy skuteczność dosprzedaży eventów.

6. Obłożenie kortów

Pokazuje, jak wykorzystywane są korty (w godzinach kortowych: zajęte / dostępne):

  • Szczyt (peak) – obłożenie w godzinach największego popytu: pn–pt 16:00–21:00, sob–ndz 8:00–20:00. Benchmark: powyżej ~85% sygnalizuje potrzebę rozbudowy lub cennika dynamicznego.
  • Poza szczytem – obłożenie w pozostałych godzinach otwarcia. Niskie wartości to potencjał na programy seniorskie/korporacyjne i rabaty.
  • Weekend – obłożenie w soboty i niedziele (wszystkie godziny otwarcia).

Pod każdym kafelkiem widać zajęte i dostępne godziny. Dostępne godziny liczone są z godzin otwarcia kortu (domyślnie 07:00–23:00, gdy kort ich nie ma), pomniejszonych o zamknięcia (closures). Okres liczony jest do dzisiaj (dni przyszłe nie zaniżają wyniku).

7. Jakość

Mierzy zadowolenie klientów i jakość obsługi:

  • Satysfakcja (CSAT) – liczona automatycznie z ankiet po obozach (camp_feedback, skala 1–5). Pokazuje procent satysfakcji (średnia / 5 × 100%), średnią ocenę i liczbę odpowiedzi w okresie.
  • Skargi i reklamacje – liczba zgłoszeń w okresie z podziałem na kategorie (trener, kort, recepcja, faktura, inne). Wprowadzane ręcznie przyciskiem Dodaj skargę.
  • Oceny trenerów – średnia ocena każdego trenera (skala 1–5) z liczbą ocen. Wprowadzane ręcznie przyciskiem Dodaj ocenę (wybór trenera + ocena 1–5 + komentarz).

ℹ️ CSAT zadziała, gdy spłyną ankiety po obozach. Skargi i oceny trenerów wymagają ręcznego wprowadzania przez personel.

8. Dashboard Tygodniowy

„Jeden raport prawdy" – najważniejsze liczby w jednym widoku, bez przełączania okresu:

  • Przychód – tydzień / miesiąc / od początku roku (YTD) – trzy horyzonty obok siebie. Tydzień porównywany jest do poprzedniego tygodnia, miesiąc i YTD do analogicznego okresu rok wcześniej (YoY). Respektuje globalny wybór miasta.
  • Aktywni per lokalizacja – aktywne dzieci i dorośli w szkółkach w rozbiciu na lokalizacje (wszystkie miasta), żeby szybko wykryć spadki w konkretnym obiekcie.
  • Lejek leadów – ten sam 5-etapowy lejek co na podstronie Sprzedaż (za bieżący miesiąc).

9. Segmentacja klientów

Dzieli klientów wg wartości (rocznych wydatków, rolling 12 miesięcy):

  • Segmenty wartości – udział klientów w 4 segmentach: Rzadki (< 1 000 zł), Okazjonalny (1–2 tys. zł), Regularny (2–5 tys. zł), VIP (> 5 000 zł). Pomaga ocenić, czy baza jest zdrowo skierowana ku wyższym segmentom.
  • Rozkład wartości – histogram liczby klientów w przedziałach wydatków (< 500, 500–1k, 1–2k, 2–3k, 3–5k, 5k+).
  • Migracja segmentów – ilu klientów awansowało do wyższego segmentu, ilu spadło, a ilu pozostało bez zmian (porównanie bieżących 12 miesięcy z poprzednimi 12). Mierzy skuteczność programów lojalnościowych i dosprzedaży.

10. Własne propozycje

Dodatkowe wskaźniki operacyjne:

  • RevPAR kortowy – średni przychód z kortów na dostępną godzinę kortową (przychód z kortów / dostępne godziny). Analogia do hotelowego RevPAR – łączy obłożenie z ceną, lepszy niż samo obłożenie %. Zależy od wybranego okresu.
  • Wskaźnik poleceń – odsetek leadów pozyskanych z polecenia (źródło „Polecenie"). Polecenia = zerowy koszt pozyskania i wysoka retencja. Zależy od wybranego okresu.
  • Wypełnienie szkółek – dla każdej grupy: zapisani vs pojemność (max_participants), z procentem wypełnienia. Stan bieżący (niezależny od okresu).
  • Sezonowość przychodów – przychód miesięczny z ostatnich 12 miesięcy z indeksem (100 = miesiąc średni). Pokazuje, które miesiące są powyżej/poniżej średniej – do planowania kampanii i zatrudnienia sezonowego.
  • Aktywność w aplikacji – odsetek rezerwacji złożonych online (booking_source='online') w okresie. Wysoki = odciążenie recepcji; niski = potencjał do onboardingu/edukacji.
  • CLV (wartość klienta) – szacunkowa łączna wartość klienta: średni miesięczny przychód × średnia długość relacji (w miesiącach). CLV powinno być wielokrotnie wyższe niż CAC.
  • Zapełnienie eventów – dla każdego obozu/kursu: zapisani (opłaceni) vs miejsca i przychód (SUM(final_price)). Pozwala decydować o kolejnych edycjach i optymalizować cennik.

ℹ️ Utylizacja trenerów (#40, wymaga rozwinięcia rekurencyjnych dostępności) oraz renewal (#44, wymaga modelu abonamentów z datą wygaśnięcia) są zaplanowane jako kolejny etap.


🛠️ Dokumentacja techniczna

Sekcja dla deweloperów: architektura, model danych i przepływ danych nowych widoków analitycznych.

Routing i nawigacja

Wszystkie widoki są podstronami pod app/(dashboard)/dashboard/statistics/:

ŚcieżkaPlikOpis
/dashboard/statisticspage.tsxUczestnicy (istniejący panel widgetów)
/dashboard/statistics/overviewoverview/page.tsxDashboard L1
/dashboard/statistics/salessales/page.tsxSprzedaż (lejek, źródła)
/dashboard/statistics/crmcrm/page.tsxLEADS CRM
/dashboard/statistics/retentionretention/page.tsxRetencja (churn, nieobecności)
/dashboard/statistics/cross-sellcross-sell/page.tsxDosprzedaż (cross-sell, per rodzina)
/dashboard/statistics/occupancyoccupancy/page.tsxObłożenie kortów
/dashboard/statistics/qualityquality/page.tsxJakość (CSAT, skargi, oceny trenerów)
/dashboard/statistics/weeklyweekly/page.tsxDashboard Tygodniowy
/dashboard/statistics/segmentssegments/page.tsxSegmentacja klientów
/dashboard/statistics/advancedadvanced/page.tsxWłasne propozycje

Nawigacja między zakładkami: components/statistics/StatisticsTabs.tsx (zachowuje parametry city i period w URL). Filtry przekazywane są przez searchParams (city, period). Zakładki filtrowane są przez useRouteAccess().checkRouteAccess(href) (jak menu boczne), więc zakładka, do której rola nie ma dostępu w route_access, jest ukrywana.

Warstwa analityczna i okresy

  • lib/analytics/periods.tsresolvePeriod(type) zwraca zakres bieżący i porównawczy ({ fromUtc, toUtc }). Granice liczone są w strefie Europe/Warsaw (date-fns + date-fns-tz) i zwracane jako UTC ISO. Porównanie: rok do roku dla miesiąca/kwartału/YTD/roku, poprzedni tydzień dla tygodnia (year = pełny rok kalendarzowy, ytd = od początku roku do dziś). calculateDeltaPct liczy zmianę %. getPeriodMonthRange(type) zwraca zakres miesięcy (startYm/endYm) okresu – używane do agregacji miesięcznych budżetów marketingowych.
  • lib/analytics/payments.ts – współdzielona lista statusów „opłacone" (PAID_PAYMENT_STATUSES) oraz wyrażenie daty płatności COALESCE(p.paid_at, p.updated_at, p.created_at).
  • lib/analytics/format.tsformatPLN, formatCount, formatDeltaPct.
  • Komponenty prezentacji: components/statistics/KpiCard.tsx (wartość + delta), PeriodSelector.tsx.

Server Actions (źródła danych)

Wszystkie filtrują po tenant_id (getTenant) i opcjonalnie po lokalizacji (locationFilter). Każda funkcja łapie błędy i zwraca pusty wynik, więc widoki działają nawet bez danych.

  • lib/actions/analytics/revenue.tsgetRevenueOverview(city, period) – suma payment.amount dla statusów opłaconych, pogrupowana po payment_type, z porównaniem okresów. (KPI #1)
  • lib/actions/analytics/members.tsgetActiveSchoolMembers(city)COUNT(DISTINCT player) przez player_activity_types + activity_types.type = 'group', podział wieku z player.date_of_birth (poniżej 18 / 18+), tylko aktywni (inactive_since IS NULL). (KPI #2, #3)
  • lib/actions/analytics/sales.tsgetSalesOverview(city, period) – nowe leady, SLA pierwszego kontaktu (AVG(julianday(first_contact_at) - julianday(created_at)) * 24), konwersje, źródła, lejek 5-etapowy oraz ranking powodów odmowy (getRejectionReasons, grupowanie po rejection_reason dla leadów ze statusem rejected). (KPI #5, #6, #7, #8, #9, #9a, #28)
  • lib/actions/analytics/marketing.tsgetMarketingRoi(city, period) – per kanał: budżet, leady, klienci, przychód, CAC (budżet/klienci) i ROI ((przychód−budżet)/budżet). Przychód atrybuowany przez lead.source → converted_player_id → payment; budżet agregowany po miesiącach okresu (getPeriodMonthRange). (KPI #9b, #38)
  • lib/actions/marketing-budget.tsgetMonthlyBudgets(year, month, city) i saveMarketingBudgets(inputs) (batchowy upsert ON CONFLICT). Zapisy wymagają roli ADMIN/BACKOFFICE.
  • lib/actions/operating-cost.tsgetMonthlyCosts, getOperatingCostsTotal(city, period) (suma kosztów po miesiącach okresu) i saveOperatingCosts (batchowy upsert). Tabela operating_cost (migracja 0157, ręcznie wpisywane koszty per kategoria/miesiąc). Marża na Dashboard L1. (KPI #4)
  • lib/actions/analytics/retention.tsgetRetentionOverview(city, period) – churn (klienci, którzy stali się nieaktywni w okresie / aktywni na początku okresu, z player.inactive_since/created_at) oraz nieobecności (aktywni bez żadnej gry w okresie, NOT EXISTS na game.attendees, tenant-scoped). Dodatkowo zwraca churnReasons (ranking GROUP BY reason z churn_record w oknie okresu) i winback (returned/total/ratePct z winback_action). (KPI #10, #11, #12, #13)
  • lib/actions/retention-manual.tscreateChurnRecord i createWinbackAction – ręczne wpisy powodów rezygnacji i akcji win-back (tabele churn_record / winback_action, migracja 0158, created_at w ISO przez strftime). Walidacja reason/channel/outcome po słownikach z types/retention.ts; zapis tylko dla ADMIN/BACKOFFICE, revalidatePath na stronie retencji. (KPI #11, #13)
  • lib/actions/analytics/cross-sell.tsgetCrossSellOverview(city, period) – cross-sell rate (aktywni klienci z COUNT(DISTINCT payment_type) >= 2 w okresie / aktywni klienci, JOIN paymentplayer), przychód per rodzina (SUM(amount) / COUNT(DISTINCT klucz_rodziny), gdzie klucz to COALESCE(guardian_email, guardian_phone, owner_email, 'player:'||id), z deltą YoY) oraz event sales rate (aktywni klienci z płatnością camp/tennis_course / aktywni klienci). Filtr opłaconych statusów i daty wg lib/analytics/payments.ts, tenant- i city-scoped. (KPI #14, #15, #16)
  • lib/actions/analytics/occupancy.tsgetOccupancyOverview(city, period) – obłożenie kortów w oknach szczyt/poza-szczytem/weekend. Czysta logika w lib/analytics/court-occupancy.ts (computeOccupancy): per kort i dzień liczy dostępne minuty z godzin otwarcia (getEffectiveHoursRange z opening-hours.ts, domyślnie 07:00–23:00) minus zamknięcia, oraz zajęte minuty z gier (game) przyciętych do godzin otwarcia i okna. Okres przycięty do „teraz", czasy gier konwertowane UTC→Europe/Warsaw. (KPI #17, #18, #19)
  • lib/actions/analytics/quality.tsgetQualityOverview(city, period) – CSAT z camp_feedback (AVG(rating)/5×100), liczba skarg per kategoria z complaint, średnia ocena per trener z trainer_rating (JOIN employee). (KPI #21, #22, #25)
  • lib/actions/quality.tscreateComplaint, createTrainerRating, getRateableEmployees (lista trenerów z lokalnej tabeli employee). Zapisy wymagają roli ADMIN/BACKOFFICE.
  • Dashboard Tygodniowy (weekly/page.tsx) nie ma własnej akcji – składa istniejące klocki: getRevenueOverview(city, 'week'|'month'|'ytd') (#26), nowe getActiveSchoolMembersByLocation() w members.ts (#27, GROUP BY city) oraz lejek z getSalesOverview(city, 'month') (#28). Bez selektora okresu (stałe horyzonty).
  • lib/actions/analytics/segments.tsgetCustomerSegments(city) – jednym zapytaniem liczy per aktywny klient sumę opłaconych płatności w bieżących i poprzednich 12 miesiącach (LEFT JOIN payment), po czym klasyfikuje do segmentów (rare/occasional/regular/vip), buduje histogram wartości i podsumowanie migracji (awans/spadek/bez zmian) bez tabeli snapshot. (KPI #31–36)
  • lib/actions/analytics/advanced.tsgetAdvancedMetrics(city, period) – RevPAR (przychód payment_type='court_reservation' / dostępne godziny z getOccupancyOverview), wskaźnik poleceń (leady source='referral' / wszystkie), wypełnienie szkółek (activity_types.max_participants vs zapisani aktywni, tenant-wide) i sezonowość (przychód miesięczny z 12 mies. + indeks vs średnia). Dodatkowo: aktywność w aplikacji (booking.booking_source='online'), CLV (śr. miesięczny przychód × śr. długość relacji per płacący klient) oraz zapełnienie eventów (camp_term/tennis_course_term
    • opłacone rejestracje + final_price). (KPI #39, #41, #42, #43, #45, #46, #49)
  • lib/actions/lead.tsgetLeads, getLeadById, createLead, updateLead, updateLeadStatus. Zapisy wymagają roli ADMIN lub BACKOFFICE (checkUserPermissions). Zmiana statusu ustawia znaczniki etapów (first_contact_at, trial_scheduled_at, trial_done_at, enrolled_at, rejected_at) przez applyStatusSideEffects (czas pierwszego kontaktu tylko dla statusów implikujących kontakt – nie przy new → rejected/inactive). Wszystkie znaczniki w formacie ISO (...T...Z), spójnym z zakresami resolvePeriod.

Model danych – tabela lead

Tworzona migracją migrations/0148_create_lead_table.sql. Kluczowe kolumny:

KolumnaOpis
sourceKanał pozyskania: google, meta, other_online, offline, referral
statusEtap: new, contacted, trial_scheduled, trial_done, enrolled, rejected, inactive
first_contact_atrejected_atZnaczniki czasu etapów (napędzają SLA i konwersje)
converted_player_idFK do player po zapisaniu się leada
product_interest, rejection_reason, notePola opisowe
city, street, tenant_idScoping lokalizacji i najemcy

Typy i stałe (źródła, statusy, etapy lejka, powody odmowy LEAD_REJECTION_REASONS): types/lead.ts. Indeksy: (tenant_id, status), (tenant_id, source), (tenant_id, created_at), city.

Model danych – tabela marketing_budget

Tworzona migracją migrations/0149_create_marketing_budget_table.sql. Ręcznie wpisywany budżet per kanał/miesiąc. Kluczowe kolumny: channel (zgodne z lead.source), year, month, amount, city, tenant_id, UNIQUE (tenant_id, city, channel, year, month) (klucz dla upsertu). Typy: types/marketing.ts.

Model danych – tabele complaint i trainer_rating

Tworzone migracją migrations/0153_quality_module.sql. complaint (category, description, status, city, tenant_id, created_at) – ręcznie wpisywane skargi. trainer_rating (employee_id FK→employee, rating 1–5, comment, city, tenant_id, created_at) – ręcznie wpisywane oceny trenerów. CSAT korzysta z istniejącej camp_feedback (bez nowej tabeli). Typy: types/quality.ts.

⚠️ Wymagane migracje: dopóki nie zastosujesz yarn db:update (migracje 0148 = lead, 0149 = marketing_budget), podstrony Sprzedaż/LEADS CRM renderują się z pustymi danymi (akcje łapią błąd „no such table" i zwracają puste wyniki).

Kontrola dostępu (RBAC)

Dostęp do tras kontroluje lib/roles.ts (isRouteAllowed) + route_access. Podstrony L1/Sprzedaż/CRM są zarejestrowane z rolą ADMIN (migrations/0150_add_statistics_routes_access.sql); podstrona Retencja jest zarejestrowana bez ról (allowed_roles = NULL, migrations/0151_add_retention_route_access.sql) oraz Obłożenie kortów (allowed_roles = NULL, migrations/0152_add_occupancy_route_access.sql) i Jakość (migrations/0153_quality_module.sql) oraz Dashboard Tygodniowy (migrations/0154_add_weekly_route_access.sql) i Segmentacja klientów (migrations/0155_add_segments_route_access.sql) i Własne propozycje (migrations/0156_add_advanced_route_access.sql) oraz Dosprzedaż (migrations/0159_add_cross_sell_route_access.sql) – pojawiają się w UI route-access do nadania ról, ale dopóki ról nie ma, ich zakładki są ukryte (rola do przypisania przez administratora). Uwagi:

  • ADMIN omija sprawdzanie dostępu na poziomie strony (getIsRouteAllowed zwraca true dla ADMIN) – odebranie roli adminowi nie zablokuje samej strony.
  • Serwer dev z SKIP_AUTH=true w ogóle nie uruchamia sprawdzania route_access w middleware.
  • Zakładki (StatisticsTabs) respektują route_access po stronie klienta – pusta/null lista ról to jawna odmowa i zakładka znika (także dla admina).
  • Aby udostępnić podstrony innym rolom (np. recepcji), dodaj je do allowed_roles w UI route-access lub w migracji.

Testy

Testy DB-backed (in-memory SQLite, better-sqlite3):

  • lib/actions/analytics/sales.getSalesOverview.test.ts – lejek, konwersje, źródła, SLA, liczenie leadów, ranking powodów odmowy.
  • lib/actions/analytics/marketing.getMarketingRoi.test.ts – CAC, ROI, agregacja totali.
  • lib/actions/marketing-budget.test.ts – upsert budżetów (idempotencja), walidacja kanałów, uprawnienia.
  • lib/actions/lead.updateLeadStatus.test.ts – znaczniki etapów, brak fałszywego kontaktu przy odmowie, uprawnienia.
  • lib/actions/analytics/retention.getRetentionOverview.test.ts – churn rate, wskaźnik nieobecności, obsługa braku aktywnych klientów, ranking powodów rezygnacji i współczynnik win-back w oknie okresu.
  • lib/actions/retention-manual.test.ts – zapis powodów rezygnacji i akcji win-back, walidacja słowników (reason/channel/outcome), uprawnienia.
  • lib/actions/analytics/cross-sell.getCrossSellOverview.test.ts – cross-sell rate i event sales rate liczone tylko wśród aktywnych klientów, przychód per rodzina po wszystkich płacących, obsługa braku aktywnych klientów.
  • lib/analytics/court-occupancy.test.ts – podział minut na okna peak/off-peak/weekend, odejmowanie zamknięć, przycinanie zajętości do godzin otwarcia.
  • lib/actions/analytics/quality.getQualityOverview.test.ts – CSAT, skargi per kategoria, średnia ocen per trener.
  • lib/actions/quality.test.ts – walidacja i zapis skarg/ocen, uprawnienia.
  • lib/actions/analytics/members.byLocation.test.ts – grupowanie aktywnych w szkółkach per miasto.
  • lib/actions/analytics/segments.getCustomerSegments.test.ts – klasyfikacja segmentów, histogram, migracja segmentów.
  • lib/actions/analytics/advanced.getAdvancedMetrics.test.ts – RevPAR, wskaźnik poleceń, wypełnienie szkółek, sezonowość.

Mapowanie na rejestr KPI (Excel „AcePark – Statystyki IT")

KPI z ExcelaRealizacja
#1 ObrótDashboard L1 → getRevenueOverview
#2 / #3 Aktywne dzieci / dorośli w szkółkachDashboard L1 → getActiveSchoolMembers
#4 Marża operacyjnaDashboard L1 → marża (koszty ręczne)
#26 Przychód tydz./mies./YTD vs poprzedniDashboard Tygodniowy → 3× getRevenueOverview
#27 Aktywni per lokalizacjaDashboard Tygodniowy → getActiveSchoolMembersByLocation
#28 Lejek leadów (Dashboard Tygodniowy)Dashboard Tygodniowy → getSalesOverview
#31–34 Segmenty wg rocznych wydatkówSegmentacja → getCustomerSegments
#35 Rozkład wartości klientówSegmentacja → histogram
#36 Migracja między segmentamiSegmentacja → migracja (12M vs 12M)
#41 Wypełnienie szkółkiWłasne propozycje → wypełnienie szkółek
#42 RevPAR kortowyWłasne propozycje → RevPAR
#43 Sezonowość przychodówWłasne propozycje → sezonowość
#46 Wskaźnik poleceńWłasne propozycje → polecenia
#39 Aktywność w aplikacjiWłasne propozycje → aktywność
#45 Zapełnienie eventówWłasne propozycje → eventy
#49 CLVWłasne propozycje → CLV
#5 Nowe leadySprzedaż → getSalesOverview
#6 Czas pierwszego kontaktu (SLA)Sprzedaż → SLA
#7 Konwersja lead → próbneSprzedaż
#8 Konwersja próbne → stałySprzedaż
#9 Powody odmowySprzedaż → karta „Powody odmowy"
#10 Rezygnacje (Churn)Retencja → getRetentionOverview
#11 Powody rezygnacjiRetencja → karta „Powody rezygnacji" (ręcznie)
#12 NieobecnościRetencja → getRetentionOverview
#13 Win-back (reaktywacja)Retencja → karta „Win-back" (ręcznie)
#14 Cross-sell (2+ usługi)Dosprzedaż → getCrossSellOverview
#15 Przychód per rodzinaDosprzedaż → getCrossSellOverview
#16 Sprzedaż eventów do obecnychDosprzedaż → getCrossSellOverview
#17 Obłożenie w szczycieObłożenie kortów → peak
#18 Obłożenie poza szczytemObłożenie kortów → off-peak
#19 Obłożenie weekendoweObłożenie kortów → weekend
#21 Satysfakcja (CSAT)Jakość → getQualityOverview (CSAT)
#22 Skargi i reklamacjeJakość → karta „Skargi"
#25 Średnia ocena treneraJakość → „Oceny trenerów"
#9a Źródła pozyskaniaSprzedaż → źródła
#9b ROI marketingowy per kanałSprzedaż → getMarketingRoi
#9c Raport LEADS CRMLEADS CRM → tabela + eksport CSV
#28 Lejek leadów (5 etapów)Sprzedaż → lejek
#38 CAC (koszt pozyskania klienta)Sprzedaż → tabela ROI (CAC per kanał)