diff --git a/docs/superpowers/specs/2026-03-31-event-guests-design.md b/docs/superpowers/specs/2026-03-31-event-guests-design.md new file mode 100644 index 0000000..2bd3ad2 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-event-guests-design.md @@ -0,0 +1,280 @@ +# Osoby towarzyszące na wydarzeniach (Event Guests) + +**Data:** 2026-03-31 +**Status:** Draft +**Kontekst:** Użytkownicy portalu chcą zapisywać na wydarzenia osoby towarzyszące — współpracowników, pracowników, osoby spoza Izby — które nie mają konta na portalu. Scenariusz zgłoszony przez prezesa Leszka Glazę: sam nie uczestniczy, ale chce zapisać kogoś w swoim imieniu. + +--- + +## Wymagania + +1. Zalogowany użytkownik może dodać jednego lub wielu gości na wydarzenie +2. Użytkownik nie musi sam być zapisany, żeby dodać gościa +3. Dane gościa (imię, nazwisko, firma/organizacja) są opcjonalne, ale minimum jedno pole musi być wypełnione +4. Goście wliczają się do limitu `max_attendees` +5. Goście są widoczni na liście uczestników z imieniem, nazwiskiem, firmą i informacją kto ich zapisał +6. Host może edytować i usuwać swoich gości +7. Admin (OFFICE_MANAGER+) może usuwać/edytować dowolnych gości +8. Nie dotyczy wydarzeń zewnętrznych (`is_external = True`) — tam rejestracja jest u organizatora +9. Max 5 gości per użytkownik per wydarzenie (stała aplikacyjna) + +--- + +## Model danych + +### Nowa tabela `event_guests` + +| Kolumna | Typ | Nullable | Default | Opis | +|---------|-----|----------|---------|------| +| `id` | `Integer` PK | — | auto | — | +| `event_id` | `Integer` FK → `norda_events.id` | NOT NULL | — | CASCADE DELETE | +| `host_user_id` | `Integer` FK → `users.id` | NOT NULL | — | CASCADE DELETE | +| `first_name` | `String(100)` | TAK | `NULL` | Imię gościa | +| `last_name` | `String(100)` | TAK | `NULL` | Nazwisko gościa | +| `organization` | `String(255)` | TAK | `NULL` | Firma/organizacja | +| `created_at` | `DateTime` | NOT NULL | `now()` | Timestamp dodania | + +**Indeksy:** +- `ix_event_guests_event_id` na `event_id` +- `ix_event_guests_host_user_id` na `host_user_id` + +**Brak unique constraint** — użytkownik może zapisać osoby bez danych lub z powtarzającymi się danymi. + +### Nowy model SQLAlchemy: `EventGuest` + +```python +class EventGuest(db.Model): + __tablename__ = 'event_guests' + + id = db.Column(db.Integer, primary_key=True) + event_id = db.Column(db.Integer, db.ForeignKey('norda_events.id', ondelete='CASCADE'), nullable=False, index=True) + host_user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + first_name = db.Column(db.String(100), nullable=True) + last_name = db.Column(db.String(100), nullable=True) + organization = db.Column(db.String(255), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + + event = db.relationship('NordaEvent', backref=db.backref('guests', cascade='all, delete-orphan', lazy='dynamic')) + host = db.relationship('User', backref=db.backref('hosted_guests', lazy='dynamic')) +``` + +### Zmiany w `NordaEvent` + +Nowa property: + +```python +@property +def total_attendee_count(self): + """Łączna liczba uczestników + gości (do sprawdzania limitu max_attendees).""" + return len(self.attendees) + self.guests.count() +``` + +Istniejąca `attendee_count` pozostaje bez zmian (kompatybilność wsteczna). + +--- + +## API + +Stała: `MAX_GUESTS_PER_USER = 5` + +### POST `/kalendarz//guests` + +Dodaje gościa na wydarzenie. + +**Request body (JSON):** +```json +{ + "first_name": "Jan", + "last_name": "Kowalski", + "organization": "Sigma Budownictwo" +} +``` + +Wszystkie pola opcjonalne, ale minimum jedno niepuste. + +**Logika:** +1. `@login_required` +2. Sprawdź czy wydarzenie istnieje i nie jest przeszłe +3. Sprawdź `event.can_user_attend(current_user)` +4. Sprawdź `event.is_external == False` +5. Sprawdź limit gości: `EventGuest.query.filter_by(event_id=event_id, host_user_id=current_user.id).count() < MAX_GUESTS_PER_USER` +6. Sprawdź `max_attendees`: jeśli ustawiony, `event.total_attendee_count < event.max_attendees` +7. Walidacja: minimum jedno pole (first_name, last_name, organization) niepuste +8. Utwórz `EventGuest`, commit +9. Zwróć `201` z danymi gościa + +**Odpowiedź (201):** +```json +{ + "action": "added", + "guest": { + "id": 42, + "first_name": "Jan", + "last_name": "Kowalski", + "organization": "Sigma Budownictwo" + } +} +``` + +**Błędy:** 400 (walidacja), 403 (brak uprawnień), 409 (limit gości lub max_attendees) + +### PATCH `/kalendarz//guests/` + +Edytuje dane gościa. + +**Request body (JSON):** jak POST, pola które mają się zmienić. + +**Logika:** +1. `@login_required` +2. Sprawdź czy gość istnieje i należy do tego wydarzenia +3. Sprawdź czy `current_user` jest hostem gościa LUB ma rolę `OFFICE_MANAGER+` +4. Sprawdź czy wydarzenie nie jest przeszłe +5. Walidacja: po aktualizacji minimum jedno pole niepuste +6. Aktualizuj pola, commit +7. Zwróć `200` z zaktualizowanymi danymi + +### DELETE `/kalendarz//guests/` + +Usuwa gościa z wydarzenia. + +**Logika:** +1. `@login_required` +2. Sprawdź czy gość istnieje i należy do tego wydarzenia +3. Sprawdź czy `current_user` jest hostem LUB ma rolę `OFFICE_MANAGER+` +4. Usuń rekord, commit +5. Zwróć `200` z `{"action": "removed"}` + +### Zmiana w istniejącym RSVP + +W route `calendar_rsvp`, zmiana sprawdzania limitu: + +```python +# Było: +if event.max_attendees and event.attendee_count >= event.max_attendees: + +# Jest: +if event.max_attendees and event.total_attendee_count >= event.max_attendees: +``` + +--- + +## UI — strona wydarzenia (`event.html`) + +### Sekcja "Osoby towarzyszące" + +Wyświetlana **poniżej** przycisku RSVP. Warunki widoczności: +- `event.can_user_attend(current_user)` = True +- Wydarzenie nie jest przeszłe +- Wydarzenie nie jest external + +#### Przycisk + +``` +[+ Dodaj osobę towarzyszącą] +``` + +Po kliknięciu rozwija inline formularz (toggle, bez przeładowania strony). + +#### Formularz (inline, rozwijany) + +``` +┌─────────────────────────────────────┐ +│ Imię: [____________] │ +│ Nazwisko: [____________] │ +│ Firma/org.: [____________] │ +│ │ +│ [Dodaj] [Anuluj] │ +└─────────────────────────────────────┘ +``` + +- Tryb dodawania: przycisk "Dodaj", pola puste +- Tryb edycji: przycisk "Zapisz", pola wypełnione danymi gościa +- Walidacja frontend: minimum jedno pole niepuste, komunikat "Podaj przynajmniej imię, nazwisko lub firmę" +- Po sukcesie: formularz się czyści (tryb dodawania) lub zamyka (tryb edycji), lista gości się odświeża +- Komunikacja via fetch/JSON, spójnie z istniejącym RSVP + +#### Lista gości bieżącego użytkownika + +Pod formularzem, widoczna tylko jeśli użytkownik ma gości na tym wydarzeniu: + +``` +Twoi goście (2/5): +• Jan Kowalski (Sigma Budownictwo) [edytuj] [✕] +• Anna Nowak [edytuj] [✕] +``` + +- Kliknięcie `[edytuj]` otwiera formularz w trybie edycji +- Kliknięcie `[✕]` usuwa gościa (po potwierdzeniu `nordaConfirm()`) +- `(2/5)` — informacja o wykorzystanym limicie + +### Lista uczestników (istniejąca sekcja, zmodyfikowana) + +Goście wyświetlani pod swoim hostem z wcięciem: + +``` +Zapisani (7): +• Leszek Glaza — Sigma Budownictwo + └ gość: Jan Kowalski (Sigma Budownictwo) + └ gość: Anna Nowak +• Roman Wierciński — Sigma Budownictwo +• Maciej Pienczyn — InPi + └ gość: Tomek Zieliński (ARP) +``` + +Jeśli host nie jest zapisany sam (scenariusz Leszka), wyświetlany jest bez oznaczenia uczestnictwa: + +``` +• Leszek Glaza (nie uczestniczy) — Sigma Budownictwo + └ gość: Jan Kowalski (Sigma Budownictwo) +``` + +### Licznik + +``` +7 osób zapisanych (było: {{ event.attendee_count }}, jest: {{ event.total_attendee_count }}) +``` + +--- + +## Migracja SQL + +Plik: `database/migrations/091_event_guests.sql` + +```sql +CREATE TABLE event_guests ( + id SERIAL PRIMARY KEY, + event_id INTEGER NOT NULL REFERENCES norda_events(id) ON DELETE CASCADE, + host_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + first_name VARCHAR(100), + last_name VARCHAR(100), + organization VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE INDEX ix_event_guests_event_id ON event_guests(event_id); +CREATE INDEX ix_event_guests_host_user_id ON event_guests(host_user_id); + +GRANT ALL ON TABLE event_guests TO nordabiz_app; +GRANT USAGE, SELECT ON SEQUENCE event_guests_id_seq TO nordabiz_app; +``` + +--- + +## Poza zakresem + +- Powiadomienia email dla gości (brak konta = brak dostarczenia) +- Rejestracja gości na wydarzenia zewnętrzne +- Samodzielna rejestracja gościa (bez konta) +- Eksport listy uczestników do CSV/PDF +- Panel admin do zarządzania gośćmi (admin korzysta z widoku wydarzenia) + +--- + +## Podsumowanie zmian + +| Komponent | Zmiana | +|-----------|--------| +| `database.py` | Nowy model `EventGuest`, nowa property `total_attendee_count` na `NordaEvent` | +| `blueprints/community/calendar/routes.py` | 3 nowe endpointy (POST/PATCH/DELETE guests), zmiana sprawdzania limitu w RSVP | +| `templates/calendar/event.html` | Sekcja gości (formularz + lista), modyfikacja listy uczestników i licznika | +| `database/migrations/091_event_guests.sql` | Nowa tabela |