diff --git a/CHANGELOG.md b/CHANGELOG.md index ff8f767..286655a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project follows [Semantic Versioning](https://semver.org/). ### Fixed - Self-service kiosk: closed events no longer appear in the event picker — only open events are selectable. -- Self-service kiosk: entering an email or phone number that matches an existing account now autofills the visitor's name, the other contact channel, and notification preference, instead of requiring staff to re-enter details already on file. +- Self-service kiosk: added a "Look up existing account" search field (by email or phone, triggered only by Enter or the Search button — never as-you-type) that autofills a returning visitor's name, contact details, and notification preference from their exact matching account, instead of requiring staff to re-enter details already on file. ## [1.3.0] - 2026-07-27 diff --git a/frontend/src/app/self-service/page.tsx b/frontend/src/app/self-service/page.tsx index 32a5a27..45af3c9 100644 --- a/frontend/src/app/self-service/page.tsx +++ b/frontend/src/app/self-service/page.tsx @@ -113,8 +113,12 @@ export default function SelfServicePage() { const [createAccount, setCreateAccount] = useState(false); const [visitorPassword, setVisitorPassword] = useState(""); const [accountExists, setAccountExists] = useState(false); - const [checkingAccount, setCheckingAccount] = useState(false); const [quantities, setQuantities] = useState>({}); + + // ── Account lookup (search-by-email/phone) state ────────────────── + const [lookupQuery, setLookupQuery] = useState(""); + const [lookupLoading, setLookupLoading] = useState(false); + const [lookupMessage, setLookupMessage] = useState(null); const [formLoading, setFormLoading] = useState(false); const [formError, setFormError] = useState(null); @@ -416,6 +420,8 @@ export default function SelfServicePage() { setCreateAccount(false); setVisitorPassword(""); setAccountExists(false); + setLookupQuery(""); + setLookupMessage(null); setFormError(null); setCurrentRegistrationId(null); setCurrentUserId(null); @@ -431,44 +437,51 @@ export default function SelfServicePage() { }; }, []); - // ─── Check whether an account already exists for the entered email/phone, and - // autofill the rest of the visitor's details from it (name, the other contact - // channel, notification preference) so staff don't have to re-ask for info - // already on file. Only fills fields that are still blank — never overwrites - // whatever the operator is actively typing. ── - useEffect(() => { - const email = visitorEmail.trim(); - const phone = visitorPhone.trim(); - if (!supervisorToken || (!email && !phone)) { - setAccountExists(false); - return; - } - const handle = setTimeout(async () => { - setCheckingAccount(true); - try { - const params = new URLSearchParams(); - if (email) params.set("email", email); - if (phone) params.set("phone", phone); - const data = await apiFetch<{ - exists: boolean; - user?: { name: string; email: string | null; phoneNumber: string | null; notificationPreference: "email" | "whatsapp" | "both" } | null; - }>(`/api/users/check-exists?${params.toString()}`, { authToken: supervisorToken }); - setAccountExists(!!data?.exists); - if (data?.user) { - const found = data.user; - setVisitorName((prev) => (prev.trim() ? prev : found.name)); - if (found.email && !email) setVisitorEmail(found.email); - if (found.phoneNumber && !phone) setVisitorPhone(found.phoneNumber); - setNotificationPref(found.notificationPreference); - } - } catch { + // ─── Explicit account lookup — only runs when the operator submits the search + // (Enter or the Search button), never on keystroke. Matches exactly against a + // single email or phone value and only ever returns that one matched account + // (or nothing) — never a broader/fuzzy match. ── + async function handleLookup() { + const q = lookupQuery.trim(); + if (!q || !supervisorToken) return; + setLookupLoading(true); + setLookupMessage(null); + try { + // Classify the query as email- or phone-shaped and send it as only that param — + // sending the same raw string as both could let digits embedded in an email + // (e.g. a numeric local-part) get misread as an unrelated phone number. + const params = new URLSearchParams(); + if (q.includes("@")) params.set("email", q); + else params.set("phone", q); + const data = await apiFetch<{ + exists: boolean; + user?: { name: string; email: string | null; phoneNumber: string | null; notificationPreference: "email" | "whatsapp" | "both" } | null; + }>(`/api/users/check-exists?${params.toString()}`, { authToken: supervisorToken }); + if (data?.exists && data.user) { + const found = data.user; + setVisitorName(found.name); + setVisitorEmail(found.email || ""); + setVisitorPhone(found.phoneNumber || ""); + setNotificationPref(found.notificationPreference); + setAccountExists(true); + setLookupMessage("Account found — details filled in below."); + } else { setAccountExists(false); - } finally { - setCheckingAccount(false); + setLookupMessage("No matching account found."); } - }, 400); - return () => clearTimeout(handle); - }, [visitorEmail, visitorPhone, supervisorToken]); + } catch { + setLookupMessage("Lookup failed. Please try again."); + } finally { + setLookupLoading(false); + } + } + + function handleLookupKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter") { + e.preventDefault(); + handleLookup(); + } + } // Existing accounts are linked automatically — never show the "create account" toggle for them useEffect(() => { @@ -639,6 +652,34 @@ export default function SelfServicePage() {

{fmtDate(selectedEvent.startDate)}

+
+ +

Search by email or phone number to fill in a returning visitor's details.

+
+ setLookupQuery(e.target.value)} + onKeyDown={handleLookupKeyDown} + className="flex-1 border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Email or phone number" + /> + +
+ {lookupMessage && ( +

+ {lookupMessage} +

+ )} +
+
@@ -806,7 +847,7 @@ export default function SelfServicePage() { {/* Account creation toggle — hidden once we know an account already exists */} {accountExists ? (
- An account with this email/number already exists — we've filled in their details below, and you'll be registered using that account. + An account with this email/number already exists — you'll be registered using that account.
) : (
@@ -819,9 +860,7 @@ export default function SelfServicePage() {

Create an account

-

- {checkingAccount ? "Checking for an existing account…" : "Save your details for future events"} -

+

Save your details for future events

{createAccount && (