nordabiz/docs/superpowers/specs/2026-03-31-event-guests-design.md
Maciej Pienczyn 4157138c2a docs: spec for event guest registration feature
Allows users to register accompanying guests (non-portal users) for events.
Covers data model, API endpoints, UI design, and migration plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:31:18 +02:00

9.1 KiB

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

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:

@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/<int:event_id>/guests

Dodaje gościa na wydarzenie.

Request body (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):

{
    "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/<int:event_id>/guests/<int:guest_id>

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/<int:event_id>/guests/<int:guest_id>

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:

# 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

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