[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

  1. Operator opens the lookup screen. A single search box is focused, with a Search by selector defaulting to Auto.

  2. 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.

  3. The screen calls POST /api/people/member-search with { 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.

  4. Results render in a responsive list (desktop table / mobile cards) with columns: first name, last name, DOB, gender, phone, email.

  5. 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 @ → search person_email

  • value is a 6- or 8-digit number (after stripping - and /) that parses as a date → search date_of_birth

  • value is otherwise numeric → search id_number and contact_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 across id_number and contact_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/view for 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-DD calendar 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

POST /api/people/member-search

Search members of the current org by an inferred or explicit field; returns PersonSummaryDTO[] (personId, firstName, lastName, dateOfBirth, gender, phone, email). Body { field: AUTO|ID_NUMBER|DOB|PHONE|EMAIL|NAME, value }; organisationId param resolved + enforced server-side; paginated.

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 over wp_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 from person.component.html; PersonService.match() for the DOB serialisation guard.

  • Open spec decisions for implementation (US-1): phone storage format in contact_number (spaces / +27 vs 0…) — decide normalise-on-search vs prefix LIKE; email match case-sensitivity; pagination cap.