The single resolver for "whose work is this?" questions against the polymorphic assignment assignee (person OR team OR department).
A person's effective scope is themselves plus one level of inherited
membership: the teams they belong to (staff TeamMembership) and the
departments those teams sit in plus their own primary department. One level
only, by design — transitive roll-ups (team of team, parent departments)
don't exist in the staff model and would be guesswork here.
match/2 answers whether an assignment falls in a scope and HOW — :direct
or {:team, name} / {:department, name} — so UIs can show provenance
("via Engineering") instead of implying personal ownership. Consumers pick
their semantics: inherited = any match; direct-only = :direct matches.
Built for the Overview calendar's assignee filter, but deliberately UI-agnostic: any future per-person surface (widgets, queues, a staff-side consumer via an events contract) should resolve through this module rather than re-deriving membership joins — the panel-review risk was exactly this logic drifting between call sites.
Staff reads are rescued to safe defaults (same convention as
Projects.list_assignments_for_user/1) — an assignee filter must never
crash the dashboard.
Summary
Types
A resolved person scope: the person plus their one-level memberships, with display names captured for provenance labels.
Functions
How assignment falls inside scope
Builds the effective scope for a staff person: their uuid, their teams
(via memberships), and their departments (the teams' departments + their
primary department). Names are localized to lang for provenance labels.
The scope for the CURRENT USER (auth uuid → staff person), or nil when
they have no staff person. Backs the filter's "Me" shortcut.
Searches people for the assignee picker (core <.search_picker> contract):
name/email typeahead, LIMITed at the database (limit+1 probes has_more for
the picker's Load more), trashed people excluded. An empty query is browse
mode — the first page of everyone, name-sorted — per the workspace picker
rule that a picker must offer options before any typing.
Whether the assignment has no assignee at all (person/team/department).
Types
Functions
@spec match(PhoenixKitProjects.Schemas.Assignment.t(), scope()) :: :direct | {:team, String.t()} | {:department, String.t()} | nil
How assignment falls inside scope:
:direct— assigned to the person themselves{:team, name}— assigned to a team the person belongs to{:department, name}— assigned to a department in the person's scopenil— outside the scope
Direct-only consumers accept only :direct; inherited consumers accept any
non-nil result (and can show the tuple's name as provenance).
Builds the effective scope for a staff person: their uuid, their teams
(via memberships), and their departments (the teams' departments + their
primary department). Names are localized to lang for provenance labels.
Returns nil when the person doesn't exist (or the staff read fails).
The scope for the CURRENT USER (auth uuid → staff person), or nil when
they have no staff person. Backs the filter's "Me" shortcut.
@spec search_people(String.t() | nil, pos_integer(), keyword()) :: {[map()], boolean()}
Searches people for the assignee picker (core <.search_picker> contract):
name/email typeahead, LIMITed at the database (limit+1 probes has_more for
the picker's Load more), trashed people excluded. An empty query is browse
mode — the first page of everyone, name-sorted — per the workspace picker
rule that a picker must offer options before any typing.
Returns {rows, has_more} where each row is the picker's
%{kind:, uuid:, label:, sublabel:, icon:} shape. {[], false} on a
failed staff read. opts[:exclude] drops the given person uuids at the
database (already-picked chips shouldn't reappear as suggestions) without
disturbing the page/has_more math.
Queries the staff Person schema directly (already a hard schema dep via
the assignment FKs) because the staff context's list_people/1 has no
LIMIT — the whole point here is not loading 1000 people per keystroke.
@spec unassigned?(PhoenixKitProjects.Schemas.Assignment.t()) :: boolean()
Whether the assignment has no assignee at all (person/team/department).