Attaching people to calendar events.
Authorization
Editing an event's participants requires edit access to the event
(PhoenixKitCalendar.Events.can_edit?/2 semantics). Per-KIND gating on
top (quorum-hardened, and validated HERE — never only in the UI):
| kind | requires |
|---|---|
user | calendar.invite_platform_users |
staff_person | calendar.invite_staff + staff module enabled |
crm_contact / crm_company | calendar.invite_crm + CRM module enabled |
free_text | nothing beyond event edit access |
Replace semantics
replace_participants/3 is a full-replace-with-diff inside one
transaction: rows not in the new set are deleted (visibility revoked
immediately), new entries are inserted, unchanged rows are kept
untouched. Only NEWLY added entries notify — each is live-resolved to a
platform user (PhoenixKitCalendar.Sources.resolve_user/1) and logged
with target_uuid, which core's notification system fans out to an
in-app notification. Company adds create no per-member notifications
(members see the event via live visibility instead).
Summary
Functions
Dedup/identity key for a participant entry — free-text keys on the
downcased name, everything else on {kind, target_uuid}. This MUST mirror
the DB partial-unique index; the LiveView's pending-chip dedup reuses it so
the two can't drift.
Whether the scope may add participants of the given kind at all — drives which picker sources the UI offers (the context re-validates on save regardless).
Lists an event's participants (insertion order), authorized against the
event's persisted owner. Returns [] unless the scope may read the event
(owner-view or participant, module enabled) — so this public read honors
the same boundary as Events.get_event/2 and can't be used to enumerate a
disabled module's participants.
Replaces the event's participant set with entries
([%{kind, target_uuid, display_name}]), diffing against the current
rows. Returns {:ok, participants} or {:error, :unauthorized} /
{:error, changeset}.
Functions
Dedup/identity key for a participant entry — free-text keys on the
downcased name, everything else on {kind, target_uuid}. This MUST mirror
the DB partial-unique index; the LiveView's pending-chip dedup reuses it so
the two can't drift.
@spec kind_allowed?(PhoenixKit.Users.Auth.Scope.t() | nil, String.t()) :: boolean()
Whether the scope may add participants of the given kind at all — drives which picker sources the UI offers (the context re-validates on save regardless).
@spec list_for_event( PhoenixKit.Users.Auth.Scope.t() | nil, PhoenixKitCalendar.Schemas.Event.t() ) :: [ PhoenixKitCalendar.Schemas.Participant.t() ]
Lists an event's participants (insertion order), authorized against the
event's persisted owner. Returns [] unless the scope may read the event
(owner-view or participant, module enabled) — so this public read honors
the same boundary as Events.get_event/2 and can't be used to enumerate a
disabled module's participants.
@spec replace_participants( PhoenixKit.Users.Auth.Scope.t() | nil, PhoenixKitCalendar.Schemas.Event.t(), [map()] ) :: {:ok, [PhoenixKitCalendar.Schemas.Participant.t()]} | {:error, :not_found | :unauthorized | :invalid_participant | Ecto.Changeset.t()}
Replaces the event's participant set with entries
([%{kind, target_uuid, display_name}]), diffing against the current
rows. Returns {:ok, participants} or {:error, :unauthorized} /
{:error, changeset}.
Authorization: event edit access, plus the per-kind invite permission
for every NEWLY ADDED entry (existing rows of a kind the editor can't
grant are preserved — an editor without invite_crm can't add clients
but doesn't silently strip someone else's).