import { isUsStateCode } from "./usStates"; // The server's answer: POST /R4/organization-time-zone-lookup. export type TimeZoneLookupResult = | { status: "verified"; timeZone: string; address?: string } | { status: "ambiguous"; candidates: string[] } | { status: "not-found"; candidates: string[] }; export type TimeZoneLookupAddress = { street: string; city: string; state: string; postalCode: string }; // Every state carries the address it belongs to, so an answer for an address since edited is // dropped rather than shown against the new one. export type ZoneCheck = | { state: "unchecked" } | { state: "checking"; key: string } | { state: "derived"; key: string; timeZone: string; address?: string } | { state: "ambiguous"; key: string; candidates: string[] } | { state: "unverified"; key: string; candidates: string[] } | { state: "failed"; key: string } | { state: "corrected"; key: string; timeZone: string }; export const UNCHECKED: ZoneCheck = { state: "unchecked" }; // The address to look up, or undefined until every part is present and well formed. export const lookupAddressOf = (parts: { street?: string; city?: string; state?: string; postalCode?: string; }): TimeZoneLookupAddress | undefined => { const address = { street: (parts.street ?? "").trim(), city: (parts.city ?? "").trim(), state: (parts.state ?? "").trim().toUpperCase(), postalCode: (parts.postalCode ?? "").trim(), }; if (!address.street || !address.city || !isUsStateCode(address.state)) return undefined; if (!/^\d{5}(-\d{4})?$/.test(address.postalCode)) return undefined; return address; }; export const addressKey = (address: TimeZoneLookupAddress): string => [address.street, address.city, address.state, address.postalCode].join("|").toLowerCase(); export const fromLookup = (key: string, result: TimeZoneLookupResult): ZoneCheck => { if (result.status === "verified") { return { state: "derived", key, timeZone: result.timeZone, address: result.address }; } if (result.status === "ambiguous") return { state: "ambiguous", key, candidates: result.candidates }; return { state: "unverified", key, candidates: result.candidates }; }; // The check only while it still belongs to the address in the form. Between an edit and the next // lookup starting, the previous address's answer must not stand in for the new one. export const currentCheck = (check: ZoneCheck, key: string | undefined): ZoneCheck => key !== undefined && check.state !== "unchecked" && check.key === key ? check : UNCHECKED; // The zone a save may use: one derived from the address, or one the admin chose. export const confirmedZone = (check: ZoneCheck): string | undefined => check.state === "derived" || check.state === "corrected" ? check.timeZone : undefined; export const needsPicker = (check: ZoneCheck): boolean => check.state === "ambiguous" || check.state === "unverified" || check.state === "failed" || check.state === "corrected"; export const candidatesOf = (check: ZoneCheck): string[] => check.state === "ambiguous" || check.state === "unverified" ? check.candidates : []; // The lookup's suggestions first, then every other zone in its usual order. export const orderZoneOptions = (options: T[], candidates: string[]): T[] => [ ...candidates.map((zone) => options.find((o) => o.value === zone)).filter((o): o is T => Boolean(o)), ...options.filter((o) => !candidates.includes(o.value)), ];