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.
-
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.
-
Dashboard Tygodniowy – przychód w trzech horyzontach, aktywni per lokalizacja i lejek leadów.
-
Segmentacja klientów – segmenty wg rocznych wydatków, rozkład wartości i migracja segmentów.
-
Własne propozycje – RevPAR kortowy, sezonowość, polecenia, wykorzystanie i konwersja trenerów oraz 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ń – domyślnie bieżący tydzień (od poniedziałku).
- Miesiąc – domyślnie bieżący miesiąc.
- Kwartał – bieżący kwartał.
- Od początku roku – od 1 stycznia do dziś.
- Rok – bieżący rok.
Wybór konkretnego tygodnia / miesiąca (Dashboard L1). Na Dashboard L1, gdy okres ustawiony jest na Tydzień lub Miesiąc, obok selektora okresu pojawia się dodatkowa lista wyboru:
- Przy okresie Tydzień – lista ostatnich tygodni w formacie „od poniedziałku do niedzieli"
(np.
od 20 sty 2026 do 26 sty 2026). Możesz wskazać dowolny miniony tydzień lub bieżący. Dla bieżącego tygodnia raport pokazuje dane od poniedziałku do dziś – np. wybierając bieżący tydzień i robiąc raport w środę, zobaczysz wynik z tych 3 dni. Dla minionego tygodnia liczony jest pełny zakres pon–ndz. - Przy okresie Miesiąc – lista ostatnich miesięcy (np.
styczeń 2026), żeby szybko porównywać różne miesiące. Bieżący miesiąc liczony jest do dziś, miniony – pełny.
Kwartał / YTD / rok nie mają dodatkowego wyboru (liczone od bieżącej daty).
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). Liczba jest podawana na koniec wybranego okresu (dla bieżącego tygodnia/
miesiąca – na dziś), więc zmienia się między miesiącami – dzieci zapisują się przez cały rok.
Uczestnik jest liczony, jeśli był już zapisany (
created_at≤ koniec okresu) i nie stał się nieaktywny przed tą datą. - 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 (przy wyborze konkretnego tygodnia/miesiąca – po miesiącach, w które ten okres wpada).
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:
| Status | Znaczenie |
|---|---|
| Nowy | Świeże zapytanie, jeszcze nieobsłużone |
| Skontaktowany | Oddzwoniliśmy / odpisaliśmy |
| Umówione próbne | Lead ma ustalony termin zajęć próbnych |
| Odbyte próbne | Lead pojawił się na zajęciach próbnych |
| Zapisany | Został stałym klientem |
| Odmowa | Zrezygnował – wybierz powód odmowy z listy |
| Nieaktywny | Brak 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_email→guardian_phone→owner_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
camplubtennis_course). Liczony jakoklienci, 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:
- Selektory tygodnia i miesiąca (w prawym górnym rogu) – dwie niezależne listy wyboru:
tydzień (format „od poniedziałku do niedzieli", np.
od 20 sty 2026 do 26 sty 2026) steruje kaflem „Przychód – tydzień", a miesiąc (np.styczeń 2026) steruje kaflem „Przychód – miesiąc" oraz tabelą „Aktywni per lokalizacja". Domyślnie bieżący tydzień i miesiąc; bieżące okresy liczone są do dziś, minione – pełne. - 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. Stan na koniec wybranego miesiąca (dla bieżącego – na dziś), więc liczby zmieniają się między miesiącami.
- 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. - Wykorzystanie trenerów – per trener: godziny faktycznie przepracowanych zajęć vs godziny
dostępne w grafiku (
zajęcia / dostępne), z procentem wykorzystania. Zbyt niskie (< 60%) = trener nieefektywny kosztowo; zbyt wysokie (> 95%) = brak buforu i ryzyko wypalenia. Godziny dostępne pochodzą z grafiku dostępności trenera (z rozwinięciem wpisów cyklicznych), więc wskaźnik działa dopiero po wprowadzeniu grafików. - Konwersja z zajęć próbnych – per trener: odsetek uczestników jego zajęć próbnych, którzy
zapisali się na stałe (
zapisani / próbni). Liczą się opłacone zajęcia próbne z wybranego okresu; „zapis na stałe" to opłacona płatność szkółkowa tego samego uczestnika z datą nie wcześniejszą niż termin jego zajęć próbnych. Wysoka konwersja = trener skutecznie zamienia próbnych na stałych klientów. Zależy od wybranego okresu i lokalizacji. - Odnowienia pakietów (renewal) – odsetek pakietów/abonamentów odnowionych przed lub w dniu
wygaśnięcia (
odnowione / wygasające w okresie). Pakiety wprowadza się ręcznie przyciskiem „Zarządzaj" (wyszukanie klienta, nazwa pakietu, data rozpoczęcia i wygaśnięcia); odnowienie to nowy pakiet powiązany z poprzednim, którego data rozpoczęcia jest nie późniejsza niż data wygaśnięcia poprzedniego (brak przerwy). Wysoki renewal = lojalność i przewidywalność przychodów.
👤 Jak używać: wykorzystanie trenerów wymaga wprowadzonych grafików dostępności trenerów. Konwersja z zajęć próbnych wymaga oznaczania trenera przy zajęciach próbnych oraz rejestrowania płatności szkółkowych. Trenerzy na obu kafelkach są identyfikowani jako „Imię Nazwisko (email)". Renewal wymaga rejestrowania pakietów klientów (przycisk „Zarządzaj" na kafelku „Odnowienia pakietów") i oznaczania ich odnowień w momencie przedłużenia.
11. Konwersja próbnych
Osobny raport odpowiadający na pytanie „ilu uczestników zajęć próbnych zapisało się na zajęcia stałe" — na trzech poziomach naraz:
- Konwersja ogółem – wynik dla całego wybranego zakresu: procent oraz liczby „uczestnicy zajęć próbnych" i „zapisani na stałe". Gdy w selektorze lokalizacji (górny pasek) wybrane jest Wszystkie, jest to wynik całej organizacji; gdy wybrane jest konkretne miasto — wynik tej lokalizacji.
- Konwersja wg lokalizacji – ta sama miara w podziale na miasta, posortowana od najwyższej. Uczestnik liczony jest w lokalizacji, w której odbyły się jego zajęcia próbne, a jako zapisany — gdy opłacił zajęcia stałe w tej samej lokalizacji.
- Konwersja z zajęć próbnych (per trener) – ranking trenerów:
zapisani / próbnioraz procent, posortowany od najskuteczniejszego. To jest widok porównawczy trenerów.
Wybór przedziału czasowego. Nad kafelkami znajduje się kalendarz zakresu i przycisk „Generuj
raport". Wskazujesz dowolną datę początkową i końcową (albo używasz skrótów: Ten miesiąc,
Poprzedni miesiąc, Ostatnie 90 dni, Ten rok), a raport przelicza się dopiero po kliknięciu
przycisku. Domyślnie pokazywany jest bieżący miesiąc. Wybrany zakres jest zapisany w adresie URL
(?from=YYYY-MM-DD&to=YYYY-MM-DD), więc link do raportu można wysłać dalej. Maksymalna długość
zakresu to 731 dni (ok. 24 miesiące) — przy dłuższym wyborze raport liczy się dla najnowszych
731 dni i pokazuje o tym informację pod tabelami.
Ten sam raport otwarty ponownie w ciągu kilkunastu minut wczytuje się z pamięci podręcznej zamiast liczyć od nowa (10 minut dla zakresu obejmującego dziś, 60 minut dla zakresu zamkniętego). Jeśli ktoś zapisał się na zajęcia stałe przed chwilą, w raporcie pojawi się po tym czasie.
Jak liczone są dane. Do zakresu wliczają się opłacone zajęcia próbne, które odbyły się w wybranym przedziale. Uczestnik liczy się jako zapisany na stałe, jeśli ma opłaconą płatność szkółkową z datą nie wcześniejszą niż jego zajęcia próbne — również poza wybranym przedziałem (zapis miesiąc po zajęciach też się liczy). Każdy uczestnik liczony jest raz w danym zestawieniu, dlatego suma lokalizacji lub trenerów może się różnić od wyniku ogółem (ta sama osoba mogła być na zajęciach próbnych u dwóch trenerów). Zajęcia próbne bez przypisanego trenera liczą się do wyniku ogółem i do lokalizacji, ale — z oczywistych powodów — nie pojawiają się w rankingu trenerów.
👤 Jak używać: żeby ranking trenerów był miarodajny, zajęcia próbne muszą mieć przypisanego trenera, a zapisy na stałe — zarejestrowaną płatność szkółkową. Przy porównywaniu trenerów patrz też na liczbę próbnych: 100% z 1 osoby to nie to samo co 60% z 30 osób. Ten sam kafelek per trener jest dostępny również na zakładce Własne propozycje, ale tam działa na predefiniowanym okresie (tydzień/miesiąc/kwartał/YTD/rok) zamiast na dowolnym zakresie dat.
🛠️ 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żka | Plik | Opis |
|---|---|---|
/dashboard/statistics | page.tsx | Uczestnicy (istniejący panel widgetów) |
/dashboard/statistics/overview | overview/page.tsx | Dashboard L1 |
/dashboard/statistics/sales | sales/page.tsx | Sprzedaż (lejek, źródła) |
/dashboard/statistics/crm | crm/page.tsx | LEADS CRM |
/dashboard/statistics/retention | retention/page.tsx | Retencja (churn, nieobecności) |
/dashboard/statistics/cross-sell | cross-sell/page.tsx | Dosprzedaż (cross-sell, per rodzina) |
/dashboard/statistics/occupancy | occupancy/page.tsx | Obłożenie kortów |
/dashboard/statistics/quality | quality/page.tsx | Jakość (CSAT, skargi, oceny trenerów) |
/dashboard/statistics/weekly | weekly/page.tsx | Dashboard Tygodniowy |
/dashboard/statistics/segments | segments/page.tsx | Segmentacja klientów |
/dashboard/statistics/advanced | advanced/page.tsx | Własne propozycje |
/dashboard/statistics/trial-conversion | trial-conversion/page.tsx | Konwersja próbnych (raport) |
Nawigacja między zakładkami: components/statistics/StatisticsTabs.tsx (zachowuje parametry
city i period w URL). Filtry przekazywane są przez searchParams (city, period, a na
Dashboard L1 i Tygodniowym dodatkowo week = yyyy-MM-dd poniedziałku i month = yyyy-MM, a na
Konwersji próbnych from/to = yyyy-MM-dd granic zakresu).
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.ts–resolvePeriod(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ś).calculateDeltaPctliczy zmianę %.getPeriodMonthRange(type)zwraca zakres miesięcy (startYm/endYm) okresu – używane do agregacji miesięcznych budżetów marketingowych.- Dowolny zakres dat (
periods.ts):normalizeDayParam(value?)waliduje parametryyyy-MM-dd, aresolveCustomRange(from?, to?)zwraca{ fromDate, toDate, current, previous }albonull, gdy któraś z granic jest pusta/nieprawidłowa (wtedy strona wraca do okresu predefiniowanego). Granice to początek i koniec dnia w Europe/Warsaw, odwrócony zakres jest zamieniany miejscami, apreviousto bezpośrednio poprzedzający przedział o tej samej długości. Zakres dłuższy niżMAX_CUSTOM_RANGE_DAYS(731 dni ≈ 24 miesiące) jest przycinany do najnowszych 731 dni, a wynik dostajeclamped: true— strona pokazuje wtedy ostrzeżenie. Limit działa też dla ręcznie wpisanego?from=, więc nie da się URL-em wymusić skanu całej historii. Używane przez raport „Konwersja próbnych". - Zakotwiczone okresy (wybór konkretnego tygodnia/miesiąca,
periods.ts):normalizeWeekParam(value?)– waliduje parametrweek(yyyy-MM-dd) i zwraca poniedziałek tygodnia, w który wpada (lub bieżący poniedziałek dla braku/nieprawidłowej wartości).normalizeMonthParam(value?)– analogicznie dla parametrumonth(yyyy-MM).resolveAnchoredWeek(week?)– zakres kotwiczony na poniedziałku:current= od poniedziałku domin(niedziela, teraz)(bieżący tydzień = częściowy do dziś, miniony = pełny),previous= poprzedni pełny tydzień,asOfUtc= koniec zakresu (data odniesienia dla stanu szkółek).resolveAnchoredMonth(month?)– zakres kotwiczony na miesiącu:current= od 1. dnia domin(koniec miesiąca, teraz),previous= ten sam miesiąc rok wcześniej (YoY),asOfUtc= koniec zakresu.getMonthRangeFromPeriodRange(current)– wyodrębniony helper liczącystartYm/endYmz dowolnegoPeriodRange(getPeriodMonthRangedeleguje do niego). Używany do sum kosztów w miesiącach, w które wpada zakotwiczony okres.
- Komponenty selektorów okresu:
components/statistics/WeekSelect.tsx(współdzielona lista tygodni „od–do", wartość = poniedziałek ISO; etykieta z klucza i18nstatistics.weekRange),app/(dashboard)/dashboard/statistics/overview/components/PeriodAnchorSelector.tsx(kontekstowo renderujeWeekSelectdla okresuweeklub listę miesięcy dlamonth, obokPeriodSelector) orazapp/(dashboard)/dashboard/statistics/weekly/components/WeeklyPeriodSelectors.tsx(dwa niezależne selektory tydzień + miesiąc na Dashboard Tygodniowym). Wszystkie zapisują wybór dosearchParams(week,month). lib/analytics/payments.ts– współdzielona lista statusów „opłacone" (PAID_PAYMENT_STATUSES) oraz wyrażenie daty płatnościCOALESCE(p.paid_at, p.updated_at, p.created_at).lib/analytics/format.ts–formatPLN,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.ts→getRevenueOverview(city, period)– sumapayment.amountdla statusów opłaconych, pogrupowana popayment_type, z porównaniem okresów. Wspólny rdzeń (buildRevenueOverview) wystawiony też jakogetRevenueForRange(city, current, previous, period)– przyjmuje jawny zakres bieżący/porównawczy (PeriodRange), używany przy wyborze konkretnego tygodnia/miesiąca (Dashboard L1 i Tygodniowy). (KPI #1, #26)lib/actions/analytics/members.ts→getActiveSchoolMembers(city, asOfIso?)–COUNT(DISTINCT player)przezplayer_activity_types+activity_types.type = 'group', podział wieku zplayer.date_of_birth(poniżej 18 / 18+). Stan na datęasOfIso(domyślnie „teraz"): uczestnik liczony, gdycreated_at≤asOfIsoi (inactive_since IS NULLlub> asOfIso); wiek liczony względemasOfIso. Dzięki temu liczba zależy od wybranego okresu. (KPI #2, #3)lib/actions/analytics/sales.ts→getSalesOverview(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 porejection_reasondla leadów ze statusemrejected). (KPI #5, #6, #7, #8, #9, #9a, #28)lib/actions/analytics/marketing.ts→getMarketingRoi(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 przezlead.source → converted_player_id → payment; budżet agregowany po miesiącach okresu (getPeriodMonthRange). (KPI #9b, #38)lib/actions/marketing-budget.ts→getMonthlyBudgets(year, month, city)isaveMarketingBudgets(inputs)(batchowy upsertON CONFLICT). Zapisy wymagają roli ADMIN/BACKOFFICE.lib/actions/operating-cost.ts→getMonthlyCosts,getOperatingCostsTotal(city, period)(suma kosztów po miesiącach okresu, deleguje dogetOperatingCostsForMonthRange(city, startYm, endYm)– wariant przyjmujący jawny zakres miesięcy, używany przy zakotwiczonym tygodniu/miesiącu na Dashboard L1) isaveOperatingCosts(batchowy upsert). Tabelaoperating_cost(migracja 0157, ręcznie wpisywane koszty per kategoria/miesiąc). Marża na Dashboard L1. (KPI #4)lib/actions/analytics/retention.ts→getRetentionOverview(city, period)– churn (klienci, którzy stali się nieaktywni w okresie / aktywni na początku okresu, zplayer.inactive_since/created_at) oraz nieobecności (aktywni bez żadnej gry w okresie,NOT EXISTSnagame.attendees, tenant-scoped). Dodatkowo zwracachurnReasons(rankingGROUP BY reasonzchurn_recordw oknie okresu) iwinback(returned/total/ratePctzwinback_action). (KPI #10, #11, #12, #13)lib/actions/retention-manual.ts→createChurnRecordicreateWinbackAction– ręczne wpisy powodów rezygnacji i akcji win-back (tabelechurn_record/winback_action, migracja 0158,created_atw ISO przezstrftime). Walidacjareason/channel/outcomepo słownikach ztypes/retention.ts; zapis tylko dla ADMIN/BACKOFFICE,revalidatePathna stronie retencji. (KPI #11, #13)lib/actions/analytics/cross-sell.ts→getCrossSellOverview(city, period)– cross-sell rate (aktywni klienci zCOUNT(DISTINCT payment_type) >= 2w okresie / aktywni klienci, JOINpayment→player), przychód per rodzina (SUM(amount) / COUNT(DISTINCT klucz_rodziny), gdzie klucz toCOALESCE(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 wglib/analytics/payments.ts, tenant- i city-scoped. (KPI #14, #15, #16)lib/actions/analytics/occupancy.ts→getOccupancyOverview(city, period)– obłożenie kortów w oknach szczyt/poza-szczytem/weekend. Czysta logika wlib/analytics/court-occupancy.ts(computeOccupancy): per kort i dzień liczy dostępne minuty z godzin otwarcia (getEffectiveHoursRangezopening-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.ts→getQualityOverview(city, period)– CSAT zcamp_feedback(AVG(rating)/5×100), liczba skarg per kategoria zcomplaint, średnia ocena per trener ztrainer_rating(JOINemployee). (KPI #21, #22, #25)lib/actions/quality.ts→createComplaint,createTrainerRating,getRateableEmployees(lista trenerów z lokalnej tabeliemployee). Zapisy wymagają roli ADMIN/BACKOFFICE.- Dashboard L1 (
overview/page.tsx) nie ma własnej akcji – rozstrzyga zakres wg okresu i parametrówweek/month: dlaweek/monthużywaresolveAnchoredWeek/resolveAnchoredMonth(zasOfUtc), dla pozostałychresolvePeriod. Następnie wołagetRevenueForRange(#1),getActiveSchoolMembers(city, asOfUtc)(#2, #3) orazgetOperatingCostsForMonthRange(#4) dla miesięcy wyliczonychgetMonthRangeFromPeriodRange. - Dashboard Tygodniowy (
weekly/page.tsx) nie ma własnej akcji – składa istniejące klocki z dwoma niezależnymi selektorami:getRevenueForRangedla zakotwiczonego tygodnia i miesiąca orazgetRevenueOverview(city, 'ytd')(#26),getActiveSchoolMembersByLocation(asOfUtc)wmembers.ts(#27,GROUP BY city, stan na koniec wybranego miesiąca) oraz lejek zgetSalesOverview(city, 'month')(#28). lib/actions/analytics/segments.ts→getCustomerSegments(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.ts→getAdvancedMetrics(city, period)– RevPAR (przychódpayment_type='court_reservation'/ dostępne godziny zgetOccupancyOverview), wskaźnik poleceń (leadysource='referral'/ wszystkie), wypełnienie szkółek (activity_types.max_participantsvs 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)
- opłacone rejestracje +
lib/actions/analytics/trainer-utilization.ts→getTrainerUtilization(period)– per trener: godziny przepracowane (suma czasu trwania gier zgamew okresie) / godziny dostępne (zinstructor_availability, z rozwinięciem wpisów cyklicznych). Czysta logika wlib/analytics/trainer-utilization.ts(computeTrainerUtilization) reużywaexpandRecurringAvailability+mergeAvailabilityByDatezlib/utils/availability.ts. Tenant-wide (grafik nie ma lokalizacji).game.instructor_idto Auth0user_id, więc nazwy trenerów pobierane są zgetInstructors(Auth0) i formatowane wspólnymlib/analytics/instructors.ts(formatInstructorLabel→ „Imię Nazwisko (email)"). (KPI #40)lib/actions/analytics/trainer-conversion.ts→getTrainerConversion(city, period)– per trener: odsetek uczestników zajęć próbnych, którzy zapisali się na stałe. Opłacone płatnościtrial(statusy zlib/analytics/payments.ts) łączone z grą przez tabelępayment_related_gamew celu ustaleniainstructor_id; „na stałe" = opłacona płatnośćschooltego samegoplayer_idz datą (COALESCE(paid_at, updated_at, created_at)) nie wcześniejszą niż termin zajęć próbnych. Filtr okresu pogame.start_time, lokalizacji popayment.city; nazwy trenerów jak wyżej.lib/actions/analytics/trainer-conversion.ts→getTrialConversionReport({ city, period, from, to })– ten sam rdzeń, ale zwraca komplet raportu:overall(wynik dla całego zakresu filtra – przycity = 'ALL'jest to wynik całej organizacji),locations[](podział popayment.city),trainers[](ranking trenerów) orazrange/fromDate/toDate/isCustomRange. Gdyfromitosą poprawne, zakres bierze się zresolveCustomRange, w przeciwnym razie zresolvePeriod(period). Zliczanie odbywa się w pamięci na dwóch zapytaniach (próbne pogrupowane poinstructor_id + player_id + city, szkółkowe poplayer_id + city), więc każdy uczestnik liczony jest raz na zestawienie — sumy per lokalizacja/trener nie muszą się składać na wynik ogółem. Zapytanie o płatności szkółkowe jest zawężone przezplayer_id IN (<uczestnicy próbnych z zakresu>)— bez tego agregowałoby całą historię szkółek tenanta przy każdym generowaniu raportu. Zajęcia próbne bez przypisanego trenera (instructor_idNULL lub'none') wliczają się do wyniku ogółem i do lokalizacji, a odpadają dopiero przy budowaniu rankingu trenerów — filtr trenera jest w kodzie, nie w SQL-u, żeby nie zaniżać zestawień zbiorczych. Wynik dla własnego zakresu dat jest cache'owany wAPP_CACHE(getCached, namespacetrial-conversion:<tenantId>, kluczcity:from:to): 10 minut dla zakresu obejmującego dziś i 60 minut dla zakresu już zamkniętego. Raporty dla okresów predefiniowanych (period) nie są cache'owane — ich granica końcowa przesuwa się z zegarem (ytd), więc klucz byłby za każdym razem inny i każde wejście generowałoby zapis do KV. Konwersje zarejestrowane po zbudowaniu wpisu pojawią się w raporcie dopiero po wygaśnięciu TTL. Konwersja per lokalizacja wymaga płatności szkółkowej w tej samej lokalizacji, per trener i ogółem — w dowolnej lokalizacji objętej filtrem.getTrainerConversion(city, period)jest cienką nakładką na tę funkcję (używa jej zakładka „Własne propozycje").lib/actions/membership.ts→getRenewalOverview(city, period)(renewal rate: pakiety wygasające w okresie i ile z nich odnowiono),getExpiringMemberships,createMembership,renewMembership(tworzy pakiet zrenewed_from_idwskazującym poprzedni). Odnowienie liczone, gdy istnieje pakiet zrenewed_from_id = m.idistart_date <= m.expires_at. Granice okresu liczone jako daty lokalne Europe/Warsaw (porównanie zexpires_attypuyyyy-MM-dd). Zapisy: ADMIN/BACKOFFICE. (KPI #44)lib/actions/lead.ts→getLeads,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) przezapplyStatusSideEffects(czas pierwszego kontaktu tylko dla statusów implikujących kontakt – nie przynew → rejected/inactive). Wszystkie znaczniki w formacie ISO (...T...Z), spójnym z zakresamiresolvePeriod.
Model danych – tabela lead
Tworzona migracją migrations/0148_create_lead_table.sql. Kluczowe kolumny:
| Kolumna | Opis |
|---|---|
source | Kanał pozyskania: google, meta, other_online, offline, referral |
status | Etap: new, contacted, trial_scheduled, trial_done, enrolled, rejected, inactive |
first_contact_at … rejected_at | Znaczniki czasu etapów (napędzają SLA i konwersje) |
converted_player_id | FK do player po zapisaniu się leada |
product_interest, rejection_reason, note | Pola opisowe |
city, street, tenant_id | Scoping 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.
Model danych – tabela membership
Tworzona migracją migrations/0161_create_membership_table.sql. Reprezentuje pakiet/abonament klienta
z datą wygaśnięcia (model dla renewal rate #44). Kluczowe kolumny: player_id (FK→player),
package_name, start_date, expires_at (oba yyyy-MM-dd), renewed_from_id (FK→membership –
wskazuje pakiet odnawiany), city, tenant_id, created_at (ISO). Odnowienie to nowy wiersz z
ustawionym renewed_from_id. Indeksy: (tenant_id, expires_at), player_id, renewed_from_id,
city. Typy: types/membership.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 ma rolę ADMIN (migrations/0165_set_retention_route_admin_role.sql); Obłożenie kortów
ma rolę ADMIN (migrations/0170_set_occupancy_route_admin_role.sql); Jakość ma rolę ADMIN
(migrations/0153_quality_module.sql + migrations/0171_set_quality_route_admin_role.sql); Dashboard
Tygodniowy ma rolę ADMIN (migrations/0154_add_weekly_route_access.sql +
migrations/0173_set_weekly_route_admin_role.sql); Segmentacja klientów ma rolę ADMIN
(migrations/0155_add_segments_route_access.sql +
migrations/0174_set_segments_route_admin_role.sql); Własne propozycje ma rolę ADMIN
(migrations/0156_add_advanced_route_access.sql +
migrations/0178_set_advanced_route_admin_role.sql); Dosprzedaż ma rolę ADMIN
(migrations/0159_add_cross_sell_route_access.sql +
migrations/0167_set_cross_sell_route_admin_role.sql); Konwersja próbnych ma rolę ADMIN
(migrations/0206_add_trial_conversion_route_access.sql). Uwagi:
- ADMIN omija sprawdzanie dostępu na poziomie strony (
getIsRouteAllowedzwracatruedla ADMIN) – odebranie roli adminowi nie zablokuje samej strony. - Serwer dev z
SKIP_AUTH=truew ogóle nie uruchamia sprawdzaniaroute_accessw middleware. - Zakładki (
StatisticsTabs) respektująroute_accesspo stronie klienta – pusta/nulllista 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_rolesw 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 oraz stan na datęasOf(wykluczenie zapisanych po dacie i tych, którzy stali się nieaktywni przed datą).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ść.lib/actions/analytics/trainer-conversion.test.ts– konwersja per trener (kafelek „Własne propozycje") oraz pełny raportgetTrialConversionReport: wynik ogółem bez podwójnego liczenia uczestnika, podział per lokalizacja, przycięcie do dowolnego zakresu dat, zamiana odwróconego zakresu, powrót do okresu predefiniowanego przy niekompletnym zakresie, przycięcie zbyt długiego zakresu i zawężenie wszystkich zestawień do wybranej lokalizacji. Osobny blok pokrywa cache KV: powtórzone wywołanie dla tego samego zakresu nie dotyka bazy (licznikprepare()na atrapie D1), każdy zakres i lokalizacja mają własny klucz, a raporty dla okresów predefiniowanych nie zapisują nic do KV.lib/analytics/trainer-utilization.test.ts– sumowanie godzin dostępnych (wpisy jednorazowe i rozwinięcie cykliczne) i przepracowanych, obsługa trenera bez grafiku oraz wpisów poza okresem.lib/actions/membership.test.ts– renewal rate dla pakietów wygasających w okresie, pominięcie odnowienia rozpoczętego po dacie wygaśnięcia, zapis pakietu i odnowienia, uprawnienia.
Mapowanie na rejestr KPI (Excel „AcePark – Statystyki IT")
| KPI z Excela | Realizacja |
|---|---|
| #1 Obrót | Dashboard L1 → getRevenueOverview |
| #2 / #3 Aktywne dzieci / dorośli w szkółkach | Dashboard L1 → getActiveSchoolMembers |
| #4 Marża operacyjna | Dashboard L1 → marża (koszty ręczne) |
| #26 Przychód tydz./mies./YTD vs poprzedni | Dashboard Tygodniowy → 3× getRevenueOverview |
| #27 Aktywni per lokalizacja | Dashboard Tygodniowy → getActiveSchoolMembersByLocation |
| #28 Lejek leadów (Dashboard Tygodniowy) | Dashboard Tygodniowy → getSalesOverview |
| #31–34 Segmenty wg rocznych wydatków | Segmentacja → getCustomerSegments |
| #35 Rozkład wartości klientów | Segmentacja → histogram |
| #36 Migracja między segmentami | Segmentacja → migracja (12M vs 12M) |
| #41 Wypełnienie szkółki | Własne propozycje → wypełnienie szkółek |
| #42 RevPAR kortowy | Własne propozycje → RevPAR |
| #43 Sezonowość przychodów | Własne propozycje → sezonowość |
| #46 Wskaźnik poleceń | Własne propozycje → polecenia |
| #39 Aktywność w aplikacji | Własne propozycje → aktywność |
| #45 Zapełnienie eventów | Własne propozycje → eventy |
| #49 CLV | Własne propozycje → CLV |
| #40 Wykorzystanie trenerów | Własne propozycje → getTrainerUtilization |
| #44 Renewal rate (odnowienia) | Własne propozycje → getRenewalOverview (model pakietów) |
| #5 Nowe leady | Sprzedaż → getSalesOverview |
| #6 Czas pierwszego kontaktu (SLA) | Sprzedaż → SLA |
| #7 Konwersja lead → próbne | Sprzedaż |
| #8 Konwersja próbne → stały | Sprzedaż + Konwersja próbnych (ogółem / lokalizacja / trener) |
| #9 Powody odmowy | Sprzedaż → karta „Powody odmowy" |
| #10 Rezygnacje (Churn) | Retencja → getRetentionOverview |
| #11 Powody rezygnacji | Retencja → karta „Powody rezygnacji" (ręcznie) |
| #12 Nieobecności | Retencja → getRetentionOverview |
| #13 Win-back (reaktywacja) | Retencja → karta „Win-back" (ręcznie) |
| #14 Cross-sell (2+ usługi) | Dosprzedaż → getCrossSellOverview |
| #15 Przychód per rodzina | Dosprzedaż → getCrossSellOverview |
| #16 Sprzedaż eventów do obecnych | Dosprzedaż → getCrossSellOverview |
| #17 Obłożenie w szczycie | Obłożenie kortów → peak |
| #18 Obłożenie poza szczytem | Obłożenie kortów → off-peak |
| #19 Obłożenie weekendowe | Obłożenie kortów → weekend |
| #21 Satysfakcja (CSAT) | Jakość → getQualityOverview (CSAT) |
| #22 Skargi i reklamacje | Jakość → karta „Skargi" |
| #25 Średnia ocena trenera | Jakość → „Oceny trenerów" |
| #9a Źródła pozyskania | Sprzedaż → źródła |
| #9b ROI marketingowy per kanał | Sprzedaż → getMarketingRoi |
| #9c Raport LEADS CRM | LEADS CRM → tabela + eksport CSV |
| #28 Lejek leadów (5 etapów) | Sprzedaż → lejek |
| #38 CAC (koszt pozyskania klienta) | Sprzedaż → tabela ROI (CAC per kanał) |
Wykorzystanie zakładek
Zakładka Statystyki → Wykorzystanie zakładek pokazuje, jak intensywnie użytkownicy korzystają z poszczególnych ekranów aplikacji.
Instrukcja (dla użytkownika)
- Tabela wymienia zakładki (ścieżki
/dashboard/...) posortowane wg liczby wejść. - Dla każdej zakładki widać: Wejścia, Unikalni użytkownicy, Śr. czas (średni czas jednej wizyty) oraz Łączny czas aktywnego korzystania.
- Selektor Rola filtruje dane do wybranej roli (ADMIN, BACKOFFICE, INSTRUCTOR, CLIENT) lub pokazuje wszystkie łącznie.
- Selektor okresu (tydzień / miesiąc / kwartał / YTD / rok) zawęża zakres dat.
- Wyróżniona karta Średni czas w aplikacji na klienta pokazuje średni aktywny czas przypadający na jednego klienta (rola CLIENT) w wybranym okresie — łączny czas klientów podzielony przez liczbę unikalnych klientów.
- Kolumny tabeli można sortować (klik w nagłówek).
- Wartość biznesowa: wskazuje najczęściej używane i „martwe" ekrany, pomaga priorytetyzować rozwój UI i ocenić adopcję funkcji.
Jak to działa (dla developera)
- Zbieranie:
components/analytics/page-analytics-tracker.tsx(montowany wDashboardLayoutWrapper) mierzy aktywny czas na trasie (pauzuje przy ukrytej karcie) i wysyła paczki zdarzeń przeznavigator.sendBeaconnavisibilitychange/pagehideoraz co 15 s. - Normalizacja ścieżek:
lib/analytics/normalize-route.tsucina dynamiczne segmenty (ID, UUID), żeby ograniczyć kardynalność (/dashboard/player/45→/dashboard/player). - Zapis:
POST /api/analytics/page-viewagreguje zdarzenia i robi upsert dopage_analytics_daily(klucztenant_id, day, route, role→visits,total_ms) orazINSERT OR IGNOREdopage_analytics_daily_users(unikalni użytkownicy). Rola i e-mail pochodzą z sesji Auth0, dzień liczony w strefie Europe/Warsaw. - Odczyt:
getPageUsageOverview(period)(lib/actions/analytics/page-usage.ts) filtruje po zakresie dni i grupuje po trasie i roli. - Migracja:
migrations/0172_add_page_analytics.sql(tabele + wpisroute_accessdashboardStatisticsPageUsage, domyślnie bez ról — widoczność nadaje się w panelu route-access). - RODO:
page_analytics_daily_usersprzechowuje e-mail powiązany z aktywnością — dane osobowe; uwzględnić w rejestrze czynności przetwarzania i retencji.