[M03] Member Person Lookup
Summary
A search screen in membership-ui for finding an existing member — a person who holds a prior or active membership in the current organisation — from a few known details: DOB, ID/passport/other number (the id_number field only), phone, or email. Results list the matching people; selecting one opens their record (M04). The screen exists because operators frequently need to locate a returning member at a desk or on a call, and the app currently offers no person-search surface.
Scope note: membership-ui today operates on Helderberg Nature Reserve (HNR, organisation ID 8) only. The organisation is resolved server-side (never hard-coded in the endpoint); the frontend obtains it from an org-context service (US-4) rather than the literal 8 it hard-codes today.
Actor & Context
Actor: membership administrator / desk operator for the current organisation (authenticated staff user).
Frequency: routine — several times a day at a staffed desk; the primary entry into an existing member’s record.
Precondition: user is authenticated; the session JWT carries the operator’s orgId. The person being sought has at least one non-draft membership in that organisation (otherwise they are, by design, not findable here — see AF-2).
Entry point: a "Find member" / "Person lookup" navigation item in membership-ui, routed at person/search.
Main Flow
-
Operator opens the lookup screen. A single search box is focused, with a Search by selector defaulting to Auto.
-
Operator types a value — an ID number, a date of birth, a phone number, or an email — and submits (Enter or the search button). In Auto mode the field is inferred from the input; the operator may instead pick an explicit field (ID number / DOB / Phone / Email) to force a single-field search.
-
The screen calls
POST /api/people/member-searchwith{ field, value }and the current organisation. The backend runs the meta search, then intersects the candidates with people holding a non-draft membership in that organisation, and returns a list of person summaries. -
Results render in a responsive list (desktop table / mobile cards) with columns: first name, last name, DOB, gender, phone, email.
-
Operator clicks a result row and is taken to that person’s detail screen (
/person/:id/view, M04).
Field inference (Auto mode)
Auto mode mirrors and extends the existing PersonService.query heuristic:
-
value contains
@→ searchperson_email -
value is a 6- or 8-digit number (after stripping
-and/) that parses as a date → searchdate_of_birth -
value is otherwise numeric → search
id_numberandcontact_number(phone); results are unioned and de-duplicated -
value is non-numeric text → search
first_name/last_name
Explicit-field selection searches only the chosen meta key. The explicit ID number field searches only id_number (never other_number, never names) — this is the one place the spec deliberately narrows behaviour relative to Auto. A short helper line tells the operator that Auto casts a wider net than the explicit fields.
Alternative Flows
-
AF-1 — Ambiguous numeric input. A digit string can be a DOB, an ID number, or a phone number. In Auto mode the backend unions matches across those fields and returns all; the operator disambiguates by reading the result rows, or re-runs with an explicit field.
-
AF-2 — No members match. The value matches nobody with a non-draft membership in the organisation (including the case where a person exists globally but is not a member here). Show an empty state: "No members found for that <field>." — not an error.
-
AF-3 — Query too short / empty. Below the minimum searchable length (mirroring the backend’s minimum for numeric queries), show inline guidance and do not call the endpoint.
-
AF-4 — Backend error. On a non-2xx response show a dismissible error and keep the entered value so the operator can retry.
Acceptance Criteria
-
A single search box with a Search by selector (Auto default; ID number / DOB / Phone / Email) is present and the box is focused on load.
-
Auto mode infers the field per the rules above, including email (
@) and phone, and unions numeric matches acrossid_numberandcontact_number. -
The explicit ID-number field searches only
id_number. -
Results show first name, last name, DOB, gender, phone, email, and are responsive (desktop table / mobile cards).
-
Every result is a person with a non-draft membership in the current organisation; non-members and global-only persons never appear.
-
Clicking a result navigates to
/person/:id/viewfor that person. -
Empty, too-short, and error states are handled per AF-2 / AF-3 / AF-4.
-
The DOB value is serialised as a local
YYYY-MM-DDcalendar date on the request (no UTC day-shift — see ADO #801 / #834). -
The organisation is obtained from the org-context service (US-4), not a hard-coded
8.
API Surface
| Call | Purpose |
|---|---|
|
Search members of the current org by an inferred or explicit field; returns |
The direct-data shape (rather than progressive-match tokens) is justified in ADR-0007.
Out of Scope
-
Creating or editing a person (existing person CRUD is untouched).
-
Searching the global person population or non-members — the endpoint is org+member scoped by design.
-
Free-text natural-language search — matching is explicit-field,
LIKE 'value%'prefix overwp_usermeta. -
Multi-organisation switching — a single active org is assumed (US-4 leaves room for it but does not deliver it).
Notes
-
Reuse targets: the membership-list radio-filter pattern (
membership.component.ts) for the Auto/explicit selector (radio value === form-control name); the responsive desktop-table / mobile-card result pattern fromperson.component.html;PersonService.match()for the DOB serialisation guard. -
Open spec decisions for implementation (US-1): phone storage format in
contact_number(spaces /+27vs0…) — decide normalise-on-search vs prefixLIKE; email match case-sensitivity; pagination cap.