Przejdź do treści

Aktywności (Activity)

Zajęcia grupowe i wydarzenia — joga, aqua aerobik, warsztaty ceramiczne i inne aktywności z harmonogramem sesji.

Ostatnia aktualizacja dokumentacji: 2026-02-26 Stan synchronizacji z kodem: Zsynchronizowany


Co to jest?

Domena Aktywności zarządza zajęciami grupowymi i wydarzeniami. To jak karta kursów w domu kultury — każda aktywność (np. "Joga dla początkujących") ma wiele sesji (poniedziałki o 18:00), na które zapisują się uczestnicy. System zarządza harmonogramem, pojemnością, trenerami i rezerwacjami.

Kluczowa koncepcja: Activity (szablon) → ActivityItem (sesja)

flowchart TD
    A[Activity: Joga dla początkujących] --> B[Sesja: Pon 18:00]
    A --> C[Sesja: Śr 18:00]
    A --> D[Sesja: Pt 18:00]
    B --> E[Rezerwacje uczestników]
    C --> F[Rezerwacje uczestników]
    D --> G[Rezerwacje uczestników]

Jakie problemy rozwiązuje?

Korzyści dla JST

  • Automatyzacja harmonogramu — generowanie sesji na wiele tygodni
  • Zarządzanie pojemnością — system pilnuje limitu miejsc
  • Przypisanie trenerów — kontrola dostępności instruktorów
  • Online rezerwacje — mieszkańcy zapisują się samodzielnie

Kluczowe funkcjonalności

Dla administratora

Funkcja Opis Status
Tworzenie aktywności Definiowanie nowych zajęć Dostępne
Generowanie sesji Automatyczne tworzenie harmonogramu Dostępne
Przypisanie trenera Wybór instruktora do sesji Dostępne
Zarządzanie pojemnością Min/max uczestników Dostępne
Cennik Płatne/bezpłatne zajęcia Dostępne
Anulowanie sesji Odwoływanie pojedynczych zajęć Dostępne
Zmiana kategorii Przeniesienie aktywności do innej kategorii Dostępne
Zmiana stanu Aktywacja/dezaktywacja aktywności i sesji Dostępne

Dla klienta

Funkcja Opis Status
Przeglądanie zajęć Lista dostępnych aktywności Dostępne
Sprawdzanie miejsc Ile wolnych miejsc Dostępne
Rezerwacja Zapis na sesję Dostępne
Opinie Ocena po zajęciach Dostępne

Activity vs ActivityItem

Activity (Szablon aktywności)

Co to jest: Definicja zajęć — nazwa, opis, pojemność, cena.

Pole Typ Opis Przykład
name string Nazwa zajęć (max 255) "Joga dla początkujących"
desc string Opis zajęć (max 5000) "Relaksujące zajęcia jogi..."
comment string? Komentarz wewnętrzny (max 5000) "Uwagi dla administracji"
min_participants int Minimalna liczba uczestników 5
max_participants int Maksymalna liczba uczestników 20
is_paid bool Czy płatne true
is_active bool Czy aktywne true
is_public bool Czy widoczne publicznie true
is_public_price_list bool Czy ceny widoczne bez logowania true
can_pay_online bool Czy dostępna płatność online (Przelewy24) true
category_id UUID Kategoria "Fitness"
item_id UUID Lokalizacja (sala) — musi być aktywna "Sala gimnastyczna A"
organization_id UUID Organizacja (ustawiane automatycznie z item)
meta_title string? Tytuł SEO (max 255) "Joga - zajęcia grupowe"
meta_description string? Opis SEO (max 5000) "Zajęcia jogi w centrum..."
tags array? Tagi ["joga", "relaks"]
statute_src file? Regulamin PDF (max ~10MB)
cancellation_deadline_hours int? Godziny przed sesją na anulowanie 24
cancellation_policy_description string? Opis zasad anulowania (max 1000) "Bezpłatne anulowanie do 24h przed..."

Atrybuty wyliczane Activity

Atrybut Typ Opis
avg_reviews float Średnia ocen (z aktywnych recenzji)
total_reviews int Liczba recenzji
current_pricing object? Aktualny aktywny cennik (bez client_id)
current_price float Aktualna cena netto
current_price_gross float Aktualna cena brutto (z VAT)
current_price_vat_rate string? Stawka VAT aktualnej ceny
lowest_pricing object? Najniższy cennik z ostatnich 31 dni
lowest_price float Najniższa cena netto z 31 dni
lowest_price_gross float Najniższa cena brutto z 31 dni
lowest_price_vat_rate string? Stawka VAT najniższej ceny
total_reservations int Łączna liczba rezerwacji (przez ActivityItems)
thumbnail string? URL miniaturki zdjęcia
is_in_use bool Czy ma powiązane rezerwacje lub opinie
can_be_edit bool Czy można edytować

ActivityItem (Sesja)

Co to jest: Konkretny termin zajęć — data, godzina, trener.

Pole Typ Opis Przykład
activity_id UUID Powiązana aktywność
date date Data sesji 2024-02-12
start_time time Godzina rozpoczęcia 18:00
end_time time Godzina zakończenia 19:30
trainer_id UUID? Trener "Anna Kowalska"
level string Poziom trudności "beginner"
is_active bool Czy sesja aktywna true
is_canceled bool Czy anulowana false
reservation_deadline_hours int? Godziny przed sesją kiedy zamykamy zapisy 24
comment string? Komentarz wewnętrzny (max 5000) "Zastępstwo trenera"

Atrybuty wyliczane ActivityItem

Atrybut Typ Zwracane w API Opis
level_lang string Tak Przetłumaczona nazwa poziomu trudności (z ActivityEnumService::getLevels())
date_start_time string Nie (wewnętrzne) Data + godzina rozpoczęcia (Y-m-d H:i:s) — używane wewnętrznie do obliczeń (np. duration_time_in_minutes)
date_end_time string Nie (wewnętrzne) Data + godzina zakończenia (Y-m-d H:i:s) — używane wewnętrznie do obliczeń
total_relations int Nie (wewnętrzne) Liczba powiązanych relacji — używane wewnętrznie
duration_time_in_minutes float Tak Czas trwania w minutach
available_slots int Tak Liczba wolnych miejsc (uwzględnia rezerwacje oczekujące)
active_waiting_reservations_count int Tak Liczba aktywnych rezerwacji oczekujących (niewygasłych)
is_in_use bool Tak Czy są powiązane rezerwacje
can_be_edit bool Tak Czy można edytować

Struktura tabel w bazie danych

Tabela activities

Kolumna Typ Nullable Default Opis
id UUID auto Klucz główny
organization_id UUID FK → organizations.id (CASCADE)
item_id UUID FK → items.id (CASCADE)
category_id UUID FK → categories.id (CASCADE)
name string Nazwa aktywności
desc text Opis
comment text NULL Komentarz wewnętrzny
min_participants integer Min. uczestników
max_participants integer Max. uczestników
is_paid boolean false Czy płatne
is_active boolean true Czy aktywne
is_public boolean true Czy publiczne
meta_title string NULL Tytuł SEO
meta_description text NULL Opis SEO
tags json NULL Tagi
statute_src text NULL Ścieżka regulaminu PDF
is_public_price_list boolean true Czy cennik publiczny (indeksowane)
can_pay_online boolean true Czy dostępna płatność online
cancellation_deadline_hours integer NULL Godziny na anulowanie
cancellation_policy_description text NULL Opis polityki anulowania
created_at timestamp Data utworzenia
updated_at timestamp Data aktualizacji

Tabela activity_items

Kolumna Typ Nullable Default Opis
id UUID auto Klucz główny
activity_id UUID FK → activities.id (CASCADE)
trainer_id UUID NULL FK → trainers.id (SET NULL), indeksowane
date date Data sesji
start_time time Godzina rozpoczęcia
end_time time Godzina zakończenia
comment text NULL Komentarz
is_public boolean true Czy publiczna
is_active boolean true Czy aktywna
is_canceled boolean false Czy anulowana
level enum NULL Poziom: beginner, intermediate, advanced, not_applicable
reservation_deadline_hours integer NULL Deadline rezerwacji (h)
created_at timestamp Data utworzenia
updated_at timestamp Data aktualizacji

Indeksy:

  • activity_items_date_activity_start_index — indeks złożony na (date, activity_id, start_time)
  • activity_items_trainer_id_index — indeks na trainer_id

Jak to działa?

Scenariusz 1: Tworzenie nowych zajęć

sequenceDiagram
    participant A as Administrator
    participant S as System
    participant Q as Kolejka

    A->>S: Tworzy aktywność "Aqua aerobik"
    A->>S: Dodaje sesje: Pon/Śr/Pt 10:00
    A->>S: Ustawia powtarzanie: 10 tygodni
    S->>S: Observer ustawia organization_id z item
    S->>S: Tworzy cennik (jeśli is_paid=true)
    S->>Q: Dispatchuje CreateManyActivityItemsJob
    Q->>S: Generuje 30 sesji (3×10)
    S-->>A: Aktywność gotowa
  1. Administrator definiuje aktywność (max 10 definicji sesji jednocześnie)
  2. Wybiera dni tygodnia i godziny
  3. Ustawia ile tygodni z góry generować (max 100)
  4. System asynchronicznie tworzy sesje (kolejka domain)
  5. Zajęcia gotowe do rezerwacji

Scenariusz 2: Klient zapisuje się na zajęcia

sequenceDiagram
    participant K as Klient
    participant S as System
    participant R as Rezerwacja

    K->>S: Przegląda zajęcia
    K->>S: Wybiera "Joga" w poniedziałek
    S->>S: Sprawdza dostępność miejsc
    S->>S: Sprawdza deadline rezerwacji
    K->>R: Tworzy rezerwację
    R-->>K: Potwierdzenie zapisu

Scenariusz 3: Anulowanie sesji

flowchart TD
    A[Administrator anuluje sesję] --> B[System ustawia is_canceled=true]
    B --> C[Observer: is_active = false]
    C --> D[Observer: dispatch CancelReservationsForActivityItemJob]
    D --> E[Job na kolejce cancellations]
    E --> F[Dla każdej rezerwacji: CancelReservation.cancelDueToActivityCancellation]
    F --> G[Anulowanie slotów + zwroty + notyfikacja email]

Gdy sesja zostaje anulowana (is_canceled=true), observer automatycznie:

  1. Dezaktywuje sesję (is_active = false)
  2. Dispatchuje CancelReservationsForActivityItemJob na kolejkę cancellations (z afterCommit())
  3. Job przetwarza każdą rezerwację osobno — pełny flow anulowania:
    • Zmiana statusu rezerwacji na canceled
    • Anulowanie aktywnych slotów
    • Automatyczny zwrot płatności (jeśli opłacona)
    • Wysyłka powiadomienia email do klienta z informacją o odwołanych zajęciach

Scenariusz 4: Dezaktywacja aktywności

flowchart TD
    A[Activity.is_active = false] --> B[Observer wykrywa zmianę]
    B --> C[CancelItemsForDisabledActivity]
    C --> D[Wszystkie ActivityItems.is_active = false]

Sesje są dezaktywowane w partiach po 2000 dla wydajności.

Scenariusz 5: Zmiana is_paid na false

Gdy aktywność zostanie przełączona na bezpłatną (is_paid=false), observer UpdatedActivityObserver automatycznie tworzy nowy cennik z wartością 0 (aktywny, VAT 23%, bez client_id). Poprzednie cenniki pozostają bez zmian.


Powiązania z innymi domenami

Domena Powiązanie Typ relacji
Rezerwacje (Reservation) Zapisy na sesje (przez ActivityItem) HasManyThrough / MorphMany
Zasoby (Item) Lokalizacja zajęć (sala) BelongsTo
Trenerzy (Trainer) Instruktorzy prowadzący (na poziomie sesji) BelongsTo
Kategorie (Category) Kategoryzacja zajęć BelongsTo
Cennik (Pricing) Ceny za uczestnictwo MorphMany (priceable)
Opinie (Review) Oceny zajęć MorphMany (reviewable)
Notatki (Note) Notatki wewnętrzne MorphMany (notable)
Organizacje (Organization) Organizator BelongsTo
Media Zdjęcia aktywności Spatie MediaLibrary

Wartość biznesowa

Argumenty dla decydentów

  1. Automatyzacja — harmonogram generowany automatycznie
  2. Oszczędność czasu — brak ręcznego zarządzania listami
  3. Transparentność — mieszkańcy widzą dostępność online
  4. Kontrola — monitoring frekwencji i przychodów

Zarządzanie pojemnością

Definicja limitów

Parametr Poziom Opis
min_participants Activity Minimalna liczba do uruchomienia (min: 0)
max_participants Activity Twarda granica pojemności (min: 1)

Obliczanie dostępnych miejsc

available_slots = max_participants - active_reservations_count - active_waiting_reservations_count

Gdzie:

  • active_reservations_count — rezerwacje z wykluczeniem statusów CANCELED i REJECTED
  • active_waiting_reservations_count — rezerwacje oczekujące z expires_at > now() (niewygasłe)

Filtr wolnych miejsc

Scope activity_item_has_available_slots działa spójnie — liczy rezerwacje ze statusami PENDING i CONFIRMED plus aktywne rezerwacje oczekujące (niewygasłe). Suma musi być mniejsza od max_participants.

Walidacja przy aktualizacji

Ograniczenie

Nie można zmniejszyć max_participants poniżej aktualnej liczby aktywnych rezerwacji na sesji z największą liczbą uczestników. System waliduje to regułą MaxParticipantsActivityUpdateRule.


Poziomy trudności

Poziom Kod Opis
Początkujący beginner Dla osób bez doświadczenia
Średniozaawansowany intermediate Podstawowe umiejętności wymagane
Zaawansowany advanced Dla doświadczonych
Nie dotyczy not_applicable Bez określonego poziomu

Poziomy zdefiniowane w ActivityEnumService z tłumaczeniami (getLevels()).


Przypisanie trenera

Walidacja dostępności

System sprawdza czy trener nie ma konfliktu (TrainerAvailabilityRule):

flowchart TD
    A[Przypisz trenera do sesji] --> B{Sprawdź konflikty}
    B -->|Brak konfliktów| C[Przypisz]
    B -->|Konflikt| D{force_add_trainer?}
    D -->|Tak| C
    D -->|Nie| E[Błąd: Trener zajęty]

Wykrywanie konfliktów

System sprawdza czy czasy się nakładają (tylko sesje nieanulowane):

existingStart < newEnd AND existingEnd > newStart

Walidacja przy tworzeniu vs aktualizacji

Scenariusz Sprawdzane daty Wykluczone
Store (nowa sesja) Wszystkie wygenerowane daty (repeat)
Update (edycja sesji) Jedna data (bieżąca lub nowa) Aktualna sesja (po ID)

Wymuszenie przypisania

Parametr force_add_trainer=true pozwala pominąć walidację (dla wyjątkowych sytuacji).


Cennik

Konfiguracja

Pole Opis
is_paid Czy zajęcia są płatne
pricing Obiekt cennika (wymagany jeśli is_paid=true)

Atrybuty cenowe

Atrybut Opis
current_price Aktualna cena netto
current_price_gross Cena brutto (z VAT)
current_price_vat_rate Stawka VAT
lowest_price Najniższa cena z ostatnich 31 dni
lowest_price_gross Najniższa cena brutto
lowest_price_vat_rate Stawka VAT najniższej ceny

Dyrektywa Omnibus

System automatycznie śledzi najniższą cenę z 31 dni dla zgodności z dyrektywą Omnibus. Na endpointach listujących (GET /activities) najniższa cena wyliczana z cenników aktywnych (is_active=true), bez cenników indywidualnych (client_id IS NULL). Na endpointach szczegółowych (GET /activities/{activityId}) accessor GetLowestPricingAttr nie filtruje po is_active — patrz sekcja "Znane uwagi techniczne".

Optymalizacja cenowa (Aggregates ORM)

Na listach (GET /activities) ceny wyliczane są za pomocą subqueries SQL (ApplyActivityAggregatesOrm) zamiast ładowania relacji, co eliminuje problem N+1 query.


Deadline rezerwacji

Konfiguracja

Parametr Poziom Opis
reservation_deadline_hours ActivityItem Godziny przed sesją kiedy zamykamy zapisy

Przykład

Sesja: 2024-02-12 18:00
reservation_deadline_hours: 24
Deadline: 2024-02-11 18:00 (dzień wcześniej)

Po przekroczeniu deadline'u nowe rezerwacje są blokowane.


Polityka anulowania

Pola konfiguracyjne

Pole Typ Poziom Opis
cancellation_deadline_hours int? Activity Godziny przed sesją na anulowanie
cancellation_policy_description text? Activity Opis zasad anulowania (max 1000 znaków)

Wykorzystanie

  • Pokazywane klientowi przy rezerwacji
  • Sprawdzane przy próbie anulowania
  • Wpływa na decyzję o zwrocie

Media i dokumenty

Zdjęcia

Kolekcja Rozmiar Format
preview 300×300 WebP
web 1200×1200 WebP

Konwersje niekolejkowane (nonQueued) — przetwarzane synchronicznie.

Regulamin zajęć

Pole Opis
statute_src Plik PDF (max ~10MB, walidacja mimes:pdf)

Widoczność publiczna

Flagi widoczności

Flaga Opis
is_public Czy aktywność widoczna publicznie
is_public_price_list Czy ceny widoczne bez logowania
can_pay_online Czy dostępna płatność online (Przelewy24)
is_active Czy aktywność aktywna

Logika wyświetlania

flowchart TD
    A[Użytkownik przegląda zajęcia] --> B{is_public?}
    B -->|Nie| C[Ukryj]
    B -->|Tak| D{is_active?}
    D -->|Nie| C
    D -->|Tak| E[Pokaż]
    E --> F{is_public_price_list?}
    F -->|Tak| G[Pokaż cenę]
    F -->|Nie| H{Zalogowany?}
    H -->|Tak| G
    H -->|Nie| I[Ukryj cenę]

Ograniczenia dla niezalogowanych

ActivityResource — pola total_reservations i can są ukrywane dla niezalogowanych użytkowników. Dodatkowo, jeśli is_public_price_list=false, ukrywane są dodatkowe klucze cenowe (current_price, current_price_gross, current_price_vat_rate, lowest_price, lowest_price_gross, lowest_price_vat_rate).

ActivityItemResource — pole can jest ukrywane dla niezalogowanych użytkowników. Pola cenowe nie dotyczą sesji (brak logiki getExtraUnallowedNoAuthKeys).


Observery i cykl życia

ActivityObserver

Zdarzenie Klasa Opis
creating CreatingActivityObserver Ustawia organization_id na podstawie item.organization_id
updating UpdatingActivityObserverUpdatingIsActiveActivityObserver Gdy is_active zmienia się na false — dezaktywuje wszystkie sesje
updated UpdatedActivityObserver Gdy is_paid zmienia się na false — tworzy nowy cennik z value=0 (aktywny, VAT 23%, bez client_id)

ActivityItemObserver

Zdarzenie Klasa Opis
updating UpdatingActivityItemObserverUpdatingIsCanceledActivityItemObserver Gdy is_canceled zmienia się na true — dezaktywuje sesję i dispatchuje CancelReservationsForActivityItemJob (pełne anulowanie rezerwacji ze zwrotami i notyfikacjami)

Walidacja aktywacji (Exceptions)

Aktywacja Activity

Nie można aktywować aktywności (changeState) jeśli:

Warunek HTTP Komunikat
Organizacja jest nieaktywna 409 "Cannot activate this activity because the organization is inactive."
Kategoria jest nieaktywna 409 "Cannot activate this activity because the category is inactive."

Aktywacja ActivityItem

Nie można aktywować sesji (changeState) jeśli:

Warunek HTTP Komunikat
Sesja jest anulowana 409 "Cannot activate this activity item because is already canceled."
Aktywność jest nieaktywna 409 "Cannot activate this activity item because the activity is inactive."
Organizacja jest nieaktywna 409 "Cannot activate this activity item because the organization is inactive."
Kategoria jest nieaktywna 409 "Cannot activate this activity item because the category is inactive."

Anulowanie ActivityItem

Nie można anulować sesji (canceled) jeśli:

Warunek HTTP Komunikat
Sesja już anulowana 409 "Sorry, but this activity item is already canceled."

Wyszukiwanie i filtrowanie

Filtry Activity (GET /activities)

Filtr Typ Opis
id exact UUID aktywności
organization_id exact UUID organizacji
item_id exact UUID obiektu
category_id exact UUID kategorii
is_paid exact Czy płatne
is_active exact Czy aktywne
trainer_id exact UUID trenera
item.voivodeship partial Województwo obiektu
item.district partial Powiat obiektu
item.commune partial Gmina obiektu
item.city partial Miasto obiektu
category.name partial Nazwa kategorii
organization.name partial Nazwa organizacji
item.name partial Nazwa obiektu

Filtry ActivityItem (GET /activity-items)

Filtr Typ Opis
id exact UUID sesji
activity_id exact UUID aktywności
is_active exact Czy sesja aktywna
is_canceled exact Czy sesja anulowana
level exact Poziom trudności
activity.is_active exact Czy aktywność aktywna
activity.is_public exact Czy aktywność publiczna
activity.category_id exact UUID kategorii aktywności
activity.is_paid exact Czy aktywność płatna
activity.is_public_price_list exact Czy cennik publiczny
search scope Szukaj w nazwie, opisie, tagach, meta, kategorii aktywności
price_from scope Minimalna cena (cennik aktywny, bez indywidualnych)
price_to scope Maksymalna cena (cennik aktywny, bez indywidualnych)
date_greater_than_today scope Sesje z date+start_time >= now() (uwzględnia godzinę)
activity_item_has_available_slots scope Tylko z wolnymi miejscami
dates_between scope Zakres dat (format: od,do lub od|do)

Filtry geograficzne (z Activity → Item)

Filtr Opis
activity.item.voivodeship Województwo
activity.item.district Powiat
activity.item.commune Gmina
activity.item.city Miasto
activity.item.name Nazwa obiektu
activity.name Nazwa aktywności
activity.organization.name Nazwa organizacji
activity.category.name Nazwa kategorii

Filtry udogodnień (z Activity → Item)

Filtr Opis
activity.item.has_parking Parking
activity.item.has_wifi WiFi
activity.item.has_air_conditioning Klimatyzacja
activity.item.is_kid_friendly Przyjazne dzieciom
activity.item.are_there_facilities_for_disabled_people Udogodnienia dla osób niepełnosprawnych
activity.item.is_there_an_elevator Winda
activity.item.is_pet_friendly Przyjazne zwierzętom
activity.item.has_restrooms Toalety
activity.item.has_kitchen Kuchnia
activity.item.has_garden Ogród

Przeszukuje przez relację activity:

  • name — nazwa aktywności
  • desc — opis aktywności
  • tags — tagi
  • meta_title — tytuł SEO
  • meta_description — opis SEO
  • category.name — nazwa kategorii

Sortowanie (ActivityItem)

Sortowanie Opis Typ
start_time Godzina rozpoczęcia Bezpośrednie
date Data sesji Bezpośrednie
name Nazwa aktywności (join) Custom sort
created_at Data utworzenia aktywności (join) Custom sort
avg_reviews Średnia ocen aktywności Custom sort (subquery)
lowest_price Najniższa cena aktywności Custom sort (subquery)

API Endpoints

Publiczne (bez autoryzacji)

Endpoint Metoda Opis
/no-auth/activities GET Lista aktywności
/no-auth/activities/{activityId} GET Szczegóły aktywności
/no-auth/activity-items GET Lista sesji
/no-auth/activity-items/{activityItemId} GET Szczegóły sesji

Dla zalogowanych użytkowników

Endpoint Metoda Opis Wymagana rola
/activities GET Lista aktywności Dowolny zalogowany
/activities/{activityId} GET Szczegóły aktywności Dowolny zalogowany
/activities POST Utwórz aktywność Admin / Supervisor / Employee
/activities/{activityId} PATCH Aktualizuj aktywność Admin / Supervisor / Employee
/activities/{activityId}/change-state PATCH Zmień status (is_active) Admin / Supervisor / Employee
/activities/{activityId}/change-category PATCH Zmień kategorię Admin / Supervisor / Employee
/activity-items GET Lista sesji Dowolny zalogowany
/activity-items/{activityItemId} GET Szczegóły sesji Dowolny zalogowany
/activity-items POST Utwórz sesję Admin / Supervisor / Employee
/activity-items/{activityItemId} PATCH Aktualizuj sesję Admin / Supervisor / Employee
/activity-items/{activityItemId}/change-state PATCH Zmień status sesji Admin / Supervisor / Employee
/activity-items/{activityItemId}/canceled PATCH Anuluj sesję Admin / Supervisor / Employee

Walidacja — szczegółowe reguły

POST /activities (StoreActivityControllerRequest)

Pole Reguły Opis
item_id required\|uuid\|exists:items,id\|in:aktywne_items Musi być aktywnym obiektem
category_id required\|uuid\|exists:categories,id\|ActiveCategoryRule(activity) Aktywna kategoria z zakresem activity
name required\|string\|max:255 Nazwa
desc required\|string\|max:5000 Opis
comment sometimes\|nullable\|string\|max:5000 Komentarz
min_participants required\|integer\|min:0\|lte:max_participants Min. uczestników
max_participants required\|integer\|min:1\|gte:min_participants Max. uczestników
is_paid required\|boolean Czy płatne
is_active required\|boolean Czy aktywne
meta_title sometimes\|nullable\|string\|max:255 SEO tytuł
meta_description sometimes\|nullable\|string\|max:5000 SEO opis
tags sometimes\|array Tagi
tags.* string\|max:255 Każdy tag
is_public_price_list sometimes\|boolean Cennik publiczny
can_pay_online sometimes\|boolean Płatność online
cancellation_deadline_hours sometimes\|nullable\|integer\|min:0 Godziny na anulowanie
cancellation_policy_description sometimes\|nullable\|string\|max:1000 Polityka anulowania
statute_src sometimes\|nullable\|file\|mimes:pdf\|max:10048 Regulamin PDF
pricing required_if:is_paid,true\|array Cennik (zagnieżdżony z StorePricingControllerRequest)
activity_items sometimes\|array\|max:10 Definicje sesji (max 10)
activity_items.*.day required\|integer\|min:1\|max:7 Dzień tygodnia
activity_items.*.start_time required\|date_format:H:i\|before:end_time Godzina start
activity_items.*.end_time required\|date_format:H:i\|after:start_time Godzina koniec
activity_items.*.comment sometimes\|nullable\|string\|max:5000 Komentarz
activity_items.*.teacher_name sometimes\|nullable\|string\|max:255 Imię nauczyciela/instruktora
activity_items.*.teacher_phone sometimes\|nullable\|string\|max:255 Telefon nauczyciela
activity_items.*.teacher_email sometimes\|nullable\|email\|max:255 Email nauczyciela
activity_items.*.level required\|string\|in:beginner,intermediate,advanced,not_applicable Poziom
activity_items.*.reservation_deadline_hours required\|integer\|min:0 Deadline
activity_items.*.start_date required\|date_format:Y-m-d Data pierwszej sesji
activity_items.*.how_many_repeat_times required\|integer\|min:0\|max:100 Powtórzenia
activity_items.*.trainer_id sometimes\|nullable\|uuid\|exists:trainers,id Trener

PATCH /activities/{activityId} (UpdateActivityControllerRequest)

Wszystkie pola są opcjonalne (sometimes). Dodatkowe reguły:

  • item_idsometimes|nullable|uuid|exists:items,id + musi być aktywnym obiektem
  • category_idsometimes|nullable|uuid|exists:categories,id + ActiveCategoryRule(activity)
  • max_participants — walidacja MaxParticipantsActivityUpdateRule: nie można zmniejszyć poniżej aktualnej liczby aktywnych rezerwacji
  • pricing — opcjonalny array, dodaje nowy cennik (nie zastępuje)

Duplikacja zmiany kategorii

Pole category_id jest dostępne zarówno w PATCH /activities/{activityId} jak i w dedykowanym PATCH /activities/{activityId}/change-category. Obie ścieżki pozwalają na zmianę kategorii.

POST /activity-items (StoreActivityItemControllerRequest)

Pole Reguły Opis
activity_id required\|uuid\|exists:activities,id Powiązana aktywność
date required\|date_format:Y-m-d Data sesji
start_time required\|date_format:H:i\|before:end_time Godzina start
end_time required\|date_format:H:i\|after:start_time Godzina koniec
comment sometimes\|nullable\|string\|max:5000 Komentarz
trainer_id sometimes\|nullable\|uuid\|exists:trainers,id\|TrainerAvailabilityRule Trener (z walidacją dostępności)
is_active required\|boolean Czy aktywna
level required\|string\|in:beginner,... Poziom trudności
reservation_deadline_hours required\|integer\|min:0 Deadline rezerwacji
how_many_repeat_times required\|integer\|min:0\|max:100 Ile razy powtórzyć
force_add_trainer sometimes\|nullable\|boolean Wymuszenie trenera (pomija walidację)

PATCH /activity-items/{activityItemId} (UpdateActivityItemControllerRequest)

Wszystkie pola opcjonalne (sometimes). TrainerAvailabilityRule zastosowana do date, start_time, end_time i trainer_id — wyklucza aktualną sesję z kontroli konfliktów.

PATCH /activities/{activityId}/change-category (ChangeCategoryActivityControllerRequest)

Pole Reguły Opis
category_id required\|uuid\|exists:categories,id\|ActiveCategoryRule(activity) Nowa kategoria

Generowanie powtarzających się sesji

Parametry

Parametr Typ Walidacja Opis
start_date date Y-m-d, wymagane Data pierwszej sesji
day int 1-7, wymagane Dzień tygodnia (1=Pon, 7=Nd)
how_many_repeat_times int 0-100, wymagane Ile tygodni generować
start_time time H:i, wymagane, before end_time Godzina rozpoczęcia
end_time time H:i, wymagane, after start_time Godzina zakończenia
trainer_id UUID? exists:trainers Opcjonalny trener
level string wymagane Poziom trudności
reservation_deadline_hours int wymagane, min: 0 Deadline rezerwacji

Przykład

{
  "activity_items": [
    {
      "start_date": "2024-02-12",
      "day": 1,
      "how_many_repeat_times": 10,
      "start_time": "18:00",
      "end_time": "19:30",
      "trainer_id": "uuid-trenera",
      "level": "beginner",
      "reservation_deadline_hours": 24
    }
  ]
}

Wygeneruje 10 sesji w kolejne poniedziałki.

Limity

  • Maksymalnie 10 definicji sesji jednocześnie przy tworzeniu aktywności
  • Maksymalnie 100 powtórzeń na definicję
  • Sesje tworzone asynchronicznie przez CreateManyActivityItemsJob (kolejka domain)

Uprawnienia

Role wymagane (middleware)

Akcje store, update, changeState, changeCategory, canceled wymagają roli: Admin, Supervisor lub Employee.

Rola Podgląd Tworzenie Edycja Zmiana stanu Anulowanie sesji
Administrator
Supervisor
Employee
Klient Publiczne

Polityki autoryzacji (Policy)

Poza wymaganymi rolami, system sprawdza polityki organizacyjne (ActivityPolicy, ActivityItemPolicy):

ActivityPolicy

Akcja Supervisor Employee
view belongsToOrganization belongsToOrganization
create belongsToOrganization (przez Item) isItemManager (musi zarządzać danym Item)
update belongsToOrganization isItemManager (Activity musi mieć item_id)

ActivityItemPolicy

Akcja Supervisor Employee
view belongsToOrganization (przez Activity) belongsToOrganization (przez Activity)
create belongsToOrganization (przez Activity) isItemManager (Activity musi mieć item_id)
update belongsToOrganization (przez Activity) isItemManager (Activity musi mieć item_id)

Kluczowa reguła

Employee może zarządzać aktywnościami tylko dla obiektów (Item), do których jest przypisany jako manager. Supervisor może zarządzać wszystkimi aktywnościami w swojej organizacji.


Struktura plików domeny

app/Domain/Activity/
├── Action/
│   ├── Attr/                              # Atrybuty wyliczane
│   │   ├── GetAvailableSlotsAttr.php
│   │   ├── GetCurrentPriceAttr.php
│   │   ├── GetCurrentPriceGrossAttr.php
│   │   ├── GetCurrentPriceVatRateAttr.php
│   │   ├── GetCurrentPricingAttr.php
│   │   ├── GetDateXTimeAttr.php
│   │   ├── GetLowestPriceAttr.php
│   │   ├── GetLowestPriceGrossAttr.php
│   │   ├── GetLowestPriceVatRateAttr.php
│   │   └── GetLowestPricingAttr.php
│   ├── Controller/                        # Akcje kontrolerów
│   │   ├── IndexActivityController.php
│   │   ├── IndexActivityItemController.php
│   │   ├── ShowActivityController.php
│   │   ├── ShowActivityItemController.php
│   │   ├── StoreActivityController.php
│   │   ├── StoreActivityItemController.php
│   │   ├── UpdateActivityController.php
│   │   └── UpdateActivityItemController.php
│   ├── Other/                             # Logika biznesowa i observery
│   │   ├── ActivityItemWithMaxReservations.php
│   │   ├── ApplyActivityAggregatesOrm.php
│   │   ├── ApplyActivityItemAggregatesOrm.php
│   │   ├── CancelItemsForDisabledActivity.php
│   │   ├── CreateActivity.php
│   │   ├── CreateActivityItem.php
│   │   ├── CreateManyActivityItems.php
│   │   ├── CreatingActivityObserver.php
│   │   ├── ExceptionCanceledActivityItem.php
│   │   ├── ExceptionChangeStateActivity.php
│   │   ├── ExceptionChangeStateActivityItem.php
│   │   ├── UpdateActivity.php
│   │   ├── UpdatedActivityObserver.php
│   │   ├── UpdatingActivityItemObserver.php
│   │   ├── UpdatingActivityObserver.php
│   │   ├── UpdatingIsActiveActivityObserver.php
│   │   └── UpdatingIsCanceledActivityItemObserver.php
│   ├── Scope/                             # Query scopes
│   │   ├── ScopeActivityItemHasAvailableSlotsFunc.php
│   │   ├── ScopeDateGreaterThanTodayFunc.php
│   │   ├── ScopeDatesBetweenFunc.php
│   │   ├── ScopePriceFromFunc.php
│   │   ├── ScopePriceToFunc.php
│   │   └── ScopeSearchFunc.php
│   └── Sort/                              # Custom sorty
│       ├── SortByActivityCreatedAt.php
│       ├── SortByActivityName.php
│       ├── SortByAvgReviews.php
│       └── SortByLowestPrice.php
├── Controller/
│   ├── ActivityController.php
│   └── ActivityItemController.php
├── Job/
│   ├── CancelReservationsForActivityItemJob.php
│   └── CreateManyActivityItemsJob.php
├── Model/
│   ├── Activity.php
│   └── ActivityItem.php
├── Observer/
│   ├── ActivityObserver.php
│   └── ActivityItemObserver.php
├── Policy/
│   ├── ActivityPolicy.php
│   └── ActivityItemPolicy.php
├── Request/
│   ├── ChangeCategoryActivityControllerRequest.php
│   ├── IndexActivityControllerRequest.php
│   ├── IndexActivityItemControllerRequest.php
│   ├── Rule/
│   │   ├── MaxParticipantsActivityUpdateRule.php
│   │   └── TrainerAvailabilityRule.php
│   ├── StoreActivityControllerRequest.php
│   ├── StoreActivityItemControllerRequest.php
│   ├── UpdateActivityControllerRequest.php
│   └── UpdateActivityItemControllerRequest.php
├── Resource/
│   ├── ActivityItemResource.php
│   └── ActivityResource.php
└── Service/
    ├── ActivityEnumService.php
    ├── TraitActivity.php
    └── TraitControllerActivity.php

Znane uwagi techniczne

⚠️ Cross-domain coupling: ExceptionChangeStateActivity

Klasa ExceptionChangeStateActivity (walidacja aktywacji aktywności) rzuca wyjątek ChangeStateItemException z domeny Item zamiast ChangeStateActivityException z domeny Activity. Dodatkowo używa TraitItem zamiast TraitActivity. Funkcjonalnie działa poprawnie (HTTP 409 z prawidłowym komunikatem), ale stanowi coupling między domenami. Klasa ExceptionChangeStateActivityItem poprawnie używa ChangeStateActivityItemException.

Niespójność filtrowania is_active w wyliczaniu najniższej ceny

Na endpointach listujących (GET /activities) najniższa cena wyliczana jest przez ApplyActivityAggregatesOrm z filtrem is_active = true na cennikach. Na endpointach szczegółowych (GET /activities/{activityId}) accessor GetLowestPricingAttr NIE filtruje po is_active i używa withoutGlobalScopes(). W rezultacie te dwie ścieżki mogą zwrócić różną wartość lowest_price — accessor uwzględnia historyczne (dezaktywowane) cenniki, a ORM aggregate tylko aktywne.

Pole is_public nie jest zwracane w odpowiedziach API

Zarówno ActivityResource jak i ActivityItemResource nie zawierają pola is_public w odpowiedzi JSON, mimo że pole istnieje w bazie danych. Widoczność publiczna jest używana wewnętrznie (filtrowanie na liście), ale nie jest eksponowana klientom API.

Niedeterministyczne computed_current_price w ApplyActivityAggregatesOrm

Subquery computed_current_price (i computed_current_price_vat_rate) używa LIMIT 1 bez ORDER BY. Jeśli aktywność ma więcej niż jeden aktywny cennik (z client_id IS NULL), zwrócony wiersz zależy od silnika bazy danych i nie jest gwarantowany. Dla porównania, computed_lowest_price poprawnie używa ->orderBy('pricings.value').

Rozbieżność metod zliczania rezerwacji: scope vs aggregates

ScopeActivityItemHasAvailableSlotsFunc liczy rezerwacje metodą inkluzji (statusy PENDING + CONFIRMED), natomiast ApplyActivityItemAggregatesOrm (active_reservations_count) metodą ekskluzji (wszystkie oprócz CANCELED i REJECTED). Jeśli istnieją rezerwacje z innymi statusami (np. completed, no_show), wyniki obu metod mogą się różnić.

Pole is_public brak w $casts modelu Activity

Pole is_public jest w $fillable modelu Activity, ale nie jest zdefiniowane w $casts (w odróżnieniu od is_active i is_paid rzutowanych na boolean). MySQL zwraca tinyint, co działa prawidłowo w warunkach logicznych, ale nie przechodzi explicit boolean cast.

CreateActivity nie przekazuje wszystkich pól walidowanych

Klasa CreateActivity (Action/Other/CreateActivity.php) tworzy model Activity za pomocą jawnej listy pól w Activity::create([...]). Pola is_public_price_list, can_pay_online, cancellation_deadline_hours i cancellation_policy_description nie są przekazywane mimo ich akceptacji w StoreActivityControllerRequest. Wartości wysłane przez klienta API w POST /activities dla tych pól są ignorowane — zastosowane zostaną domyślne wartości z migracji. Dla porównania, UpdateActivity używa $activity->fill($validated)->save(), więc update poprawnie obsługuje te pola.

Duplikat filtra category.name w IndexActivityController

W IndexActivityController filtr category.name (partial) pojawia się dwukrotnie w tablicy filtrów (linie 22 i 29). Spatie QueryBuilder ignoruje duplikaty, więc efekt funkcjonalny jest zerowy, ale stanowi niespójność w kodzie.

Różnice w eager loading: Index vs Show

IndexActivityController ładuje relacje ActivityResource::RELATIONS + ['item.childrens'], natomiast ShowActivityController ładuje tylko ActivityResource::RELATIONS (bez item.childrens). Może to powodować N+1 query na endpoincie Show dla accessora is_parent na relacji item. Analogicznie, IndexActivityItemController ładuje rozbudowany zestaw zagnieżdżonych relacji (activity.organization, activity.category, activity.media, activity.reviews, activity.item, activity.pricings, trainer), natomiast ShowActivityItemController ładuje jedynie ActivityItemResource::RELATIONS (['activity', 'trainer']), co wymusza lazy loading zagnieżdżonych relacji.

Potencjalny bug: parametr day — walidacja 1-7 vs algorytm 0-6

Walidacja activity_items.*.day w StoreActivityControllerRequest dopuszcza zakres min:1|max:7 (dokumentacja: 1=Pon, 7=Nd). Jednak FindDateXNameDaysToFuture (Support/Action/Util/) używa tablicy DAYS_OF_WEEK z indeksami 0-6 (0=niedziela, 6=sobota, standard Carbon dayOfWeek). Wartość day=7 przejdzie walidację, ale spowoduje array index out-of-bounds w runtime. Wartości 1-6 działają poprawnie (1=poniedziałek, 6=sobota). Problem dotyczy tylko endpoint POST /activities — w POST /activity-items pole day jest wyliczane automatycznie z Carbon::parse($date)->dayOfWeek (zakres 0-6).

Pole is_public w tabeli activity_items — brak w $fillable

Kolumna is_public istnieje w tabeli activity_items, ale nie jest zdefiniowana w $fillable modelu ActivityItem. Nie jest mass-assignable przez standardowe operacje Eloquent. Akcje CreateManyActivityItems i CreateActivityItem nie ustawiają tego pola — przyjmuje wartość domyślną z bazy (true).

Vestigial key reservations w ActivityItemResource::UNALLOWED_NO_AUTH_KEYS

Stała UNALLOWED_NO_AUTH_KEYS w ActivityItemResource zawiera klucz reservations, ale relacja reservations jest zakomentowana w RELATIONS i pole nie jest obecne w toArray(). Filtrowanie tego klucza w reduceResourceForNoAuth jest bezskuteczne (no-op). Prawdopodobnie pozostałość po wcześniejszej wersji resource, gdy reservations był eager-loaded.

Rozbieżność definicji najniższej ceny: Sort vs ORM vs Accessor

System posiada trzy różne definicje „najniższej ceny":

  1. SortByLowestPrice (sortowanie na liście sesji) — MIN(value) z wszystkich aktywnych cenników (bez ograniczenia czasowego)
  2. ApplyActivityAggregatesOrm (wyświetlana wartość na liście) — MIN(value) z aktywnych cenników z ostatnich 31 dni (created_at >= 31 dni temu)
  3. GetLowestPricingAttr (wyświetlana wartość na szczegółach) — MIN(value) ze wszystkich cenników (bez filtra is_active, z withoutGlobalScopes())

W rezultacie sortowanie po lowest_price może dać kolejność niespójną z wyświetlanymi wartościami lowest_price.

Brak server-side filtrowania is_public=false dla endpointów no-auth

Endpointy publiczne (/no-auth/activities, /no-auth/activity-items) używają tych samych kontrolerów co endpointy autoryzowane. System NIE filtruje automatycznie aktywności z is_public=false na endpointach no-auth. Aby wyświetlić tylko publiczne aktywności, frontend musi jawnie przekazać filter[is_public]=true lub filter[activity.is_public]=true jako parametr zapytania. Bez tego parametru niezalogowani użytkownicy mogą zobaczyć niepubliczne aktywności.

Scope date_greater_than_today uwzględnia godzinę rozpoczęcia

Scope ScopeDateGreaterThanTodayFunc porównuje STR_TO_DATE(CONCAT(date, ' ', start_time)) z now() — filtruje nie tylko po dacie, ale po dokładnej dacie+godzinie rozpoczęcia. Sesja zaplanowana na dziś, ale z godziną rozpoczęcia w przeszłości, zostanie wykluczona z wyników.

Brak walidacji dostępności trenera przy inline session items

Przy tworzeniu aktywności z sesjami inline (POST /activities z activity_items), pole trainer_id nie jest objęte regułą TrainerAvailabilityRule. Walidacja dostępności trenera odbywa się tylko na endpoincie POST /activity-items (tworzenie samodzielnych sesji). Sesje inline generowane asynchronicznie przez CreateManyActivityItemsJob mogą tworzyć konflikty trenerów.


Best practices

Wskazówki dla administratorów

  1. Generuj z wyprzedzeniem — twórz sesje na kilka tygodni naprzód
  2. Ustawiaj deadline — daj sobie czas na organizację
  3. Monitoruj frekwencję — analizuj popularne terminy
  4. Przypisuj trenerów — sprawdzaj dostępność przed przypisaniem
  5. Dokumentuj anulacje — informuj uczestników o przyczynach
  6. Sprawdzaj limity — nie zmniejszaj max_participants poniżej aktualnych rezerwacji

Historia zmian

Data Typ Opis
26 lut 2026 Korekta Audyt dokumentacji Activity (faza 5 — service traits, actions, resources, scopes, sorts) — dodano 4 nowe uwagi techniczne: rozbieżność trzech definicji lowest_price (Sort vs ORM vs Accessor); brak server-side filtrowania is_public=false na endpointach no-auth; scope date_greater_than_today uwzględnia godzinę (nie tylko datę); brak TrainerAvailabilityRule na inline session items w POST /activities. Poprawiono opis filtra date_greater_than_today w tabeli filtrów
26 lut 2026 Korekta Audyt dokumentacji Activity (faza 4 — deep-verify action classes i resources) — dodano 3 nowe uwagi techniczne: potencjalny bug day walidacja 1-7 vs algorytm FindDateXNameDaysToFuture 0-6 (day=7 powoduje array out-of-bounds); pole is_public w tabeli activity_items brak w $fillable; vestigial key reservations w ActivityItemResource::UNALLOWED_NO_AUTH_KEYS. Rozszerzono sekcje "Ograniczenia dla niezalogowanych" o opis zachowania obu Resources (ActivityResource i ActivityItemResource)
26 lut 2026 Korekta Audyt dokumentacji Activity (faza 3 — deep-verify controllers) — poprawiono opis Dyrektywy Omnibus (dodano rozroznienie miedzy ORM a Attr); dodano 3 nowe uwagi techniczne: CreateActivity nie przekazuje pol is_public_price_list, can_pay_online, cancellation_deadline_hours, cancellation_policy_description (bug w kodzie); duplikat filtra category.name w IndexActivityController; roznice w eager loading miedzy Index a Show (potencjalne N+1 na Show); rozszerzono tabele atrybutow wyliczanych ActivityItem o atrybuty wewnetrzne (date_start_time, date_end_time, total_relations) z oznaczeniem widocznosci w API
26 lut 2026 Korekta Audyt dokumentacji Activity (faza 1) — dodano kody HTTP 409 do tabel wyjątków aktywacji/anulowania; dodano brakujące pola walidacji inline activity items (teacher_name, teacher_phone, teacher_email); dodano brakujący atrybut level_lang do tabeli atrybutów wyliczanych ActivityItem; dodano sekcję "Znane uwagi techniczne": cross-domain coupling w ExceptionChangeStateActivity (używa ChangeStateItemException z domeny Item), niespójność filtrowania is_active w wyliczaniu lowest_price między ORM a Attr, brak is_public w odpowiedziach API Resources
26 lut 2026 Korekta Audyt dokumentacji Activity (faza 2 — deep verification) — poprawiono parametry routingu z {id} na {activityId} / {activityItemId} (16 endpointów + 3 nagłówki walidacji); poprawiono opis observera UpdatedActivityObserver z „zeruje aktywny cennik" na „tworzy nowy cennik z value=0"; dodano pola item_id i category_id jako aktualizowalne w PATCH /activities/{activityId} z adnotacją o duplikacji zmiany kategorii; dodano 3 nowe uwagi techniczne: niedeterministyczne computed_current_price (LIMIT 1 bez ORDER BY), rozbieżność metod zliczania rezerwacji (scope inkluzja vs aggregate ekskluzja), brak is_public w $casts modelu Activity
25 lut 2026 Aktualizacja dokumentacji Rozszerzenie dokumentacji domeny Activity — dodano sekcję "Atrybuty wyliczane Activity" (14 atrybutów), sekcję "Struktura tabel w bazie danych" (pełne definicje kolumn obu tabel z indeksami i FK), rozszerzono sekcję "Uprawnienia" o szczegółowe polityki autoryzacji (ActivityPolicy, ActivityItemPolicy) z logiką organizacyjną (belongsToOrganization, isItemManager), dodano sekcję "Walidacja — szczegółowe reguły" (pełne reguły walidacji dla wszystkich 5 Form Requestów), uzupełniono strukturę plików domeny (dodano katalog Policy/ i CancelReservationsForActivityItemJob)
18 lut 2026 Optymalizacja Eliminacja N+1 query w GET /api/activities — Dodano 'item.childrens' do eager loading w IndexActivityController (array_merge(ActivityResource::RELATIONS, ['item.childrens'])). Eliminuje x5 Item→Item N+1 wywoływane przez GetIsParentAttr gdy ActivityResource odpytuje is_parent na załadowanym obiekcie item
17 lut 2026 Nowa funkcjonalność Pełne anulowanie rezerwacji przy odwołaniu zajęć — zamieniono cancelModelReservations (tylko zmiana statusu) na CancelReservationsForActivityItemJob dispatchowany na dedykowanej kolejce cancellations. Nowy flow: anulowanie slotów, automatyczne zwroty płatności, notyfikacja email (ReservationCancelledDueToActivityCancellationNotification) z informacją o odwołanych zajęciach (nazwa, data, godzina). Nowe enum cases: CANCELLATION_TYPE_DUE_TO_ACTIVITY_CANCELLATION, REFUND_TYPE_ACTIVITY_CANCELLATION. Job przetwarzany z afterCommit() — ruszy dopiero po commitcie transakcji
15 lut 2026 Aktualizacja dokumentacji Kompleksowa aktualizacja dokumentacji — poprawiono opis filtrów (dodano brakujące filtry udogodnień: winda, zwierzęta, toalety, kuchnia, ogród), poprawiono nazwę filtra has_disabled_accessare_there_facilities_for_disabled_people, poprawiono opis scope search (szuka w name/desc/tags/meta/category, nie w organizacji/lokalizacji), dodano brakujący sort date, uzupełniono tabelę pól Activity (comment, meta_title, meta_description, tags) i ActivityItem (is_active, comment), dodano sekcję observerów, walidacji aktywacji (exceptions), strukturę plików domeny, atrybuty wyliczane, typy relacji, limity walidacyjne (max 10 definicji sesji, max 100 powtórzeń)
20 lut 2026 Aktualizacja Rezerwacje oczekujące w obliczaniu wolnych miejscApplyActivityItemAggregatesOrm dodaje active_waiting_reservations_count (withCount na waitingReservations z filtrem expires_at > now()). Formuła available_slots uwzględnia teraz rezerwacje oczekujące: max_participants - active_reservations_count - active_waiting_reservations_count. Scope activity_item_has_available_slots również uwzględnia rezerwacje oczekujące w zapytaniu SQL. ActivityItemResource zwraca nowe pole active_waiting_reservations_count
11 lut 2026 Optymalizacja Eliminacja N+1 query w GET /api/activities i GET /api/activity-items — (1) Utworzono ApplyActivityAggregatesOrm dla Activity z subqueries cenowych (computed_current_price, computed_lowest_price + vat_rate), withCount (reviews, reservations), withAvg (reviews_avg_rating), withExists (reservations, reviews), with('media'). Zmodyfikowano 8 accessorów w modelu Activity, (2) Utworzono ApplyActivityItemAggregatesOrm z withCount (active_reservations_count), withExists (reservations). Zmodyfikowano getIsInUseAttribute w modelu ActivityItem. Zaktualizowano TraitActivity, IndexActivityController i IndexActivityItemController