Some checks failed
Deploy production / deploy (push) Has been cancelled
dataholders schema (orgs + identity) with migration and personal_app grants, idempotent CSV import (upsert on name+login, never wipes, keeps site edits and ticks), owner-only /trackers/data-holders with search, filters, pagination, inline edit/add and a moving checklist per identifier
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
// Shared (client + server) bits of the private data-holders tracker: which identifiers we
|
|
// track, and when an org still has an old value ("due" in the move checklist).
|
|
|
|
export const identityKinds = ['address', 'email', 'phone', 'iban'] as const;
|
|
export type IdentityKind = (typeof identityKinds)[number];
|
|
export const isIdentityKind = (v: unknown): v is IdentityKind =>
|
|
typeof v === 'string' && (identityKinds as readonly string[]).includes(v);
|
|
|
|
export const identityLabels: Record<IdentityKind, string> = {
|
|
address: 'Address',
|
|
email: 'Email',
|
|
phone: 'Phone',
|
|
iban: 'IBAN'
|
|
};
|
|
|
|
export const actions = ['update', 'close', 'check', 'none'] as const;
|
|
export const priorities = ['must', 'should', 'nice'] as const;
|
|
export const addressStatuses = ['confirmed', 'likely', 'unknown'] as const;
|
|
export const priorityLabels = { must: 'Must-do', should: 'Should-do', nice: 'Nice-to-have' };
|
|
|
|
export type Identity = { kind: string; value: string; since: string };
|
|
|
|
export type OrgLike = {
|
|
archived: boolean;
|
|
action: string;
|
|
holdsAddress: boolean;
|
|
holdsEmail: boolean;
|
|
holdsPhone: boolean;
|
|
holdsIban: boolean;
|
|
addressOnFile: string | null;
|
|
emailOnFile: string | null;
|
|
phoneOnFile: string | null;
|
|
addressUpdatedOn: string | null;
|
|
emailUpdatedOn: string | null;
|
|
phoneUpdatedOn: string | null;
|
|
ibanUpdatedOn: string | null;
|
|
};
|
|
|
|
export const holds = (o: OrgLike, kind: IdentityKind) =>
|
|
({ address: o.holdsAddress, email: o.holdsEmail, phone: o.holdsPhone, iban: o.holdsIban })[kind];
|
|
|
|
export const updatedOn = (o: OrgLike, kind: IdentityKind) =>
|
|
({
|
|
address: o.addressUpdatedOn,
|
|
email: o.emailUpdatedOn,
|
|
phone: o.phoneUpdatedOn,
|
|
iban: o.ibanUpdatedOn
|
|
})[kind];
|
|
|
|
const norm = (s: string) =>
|
|
s
|
|
.toLowerCase()
|
|
.normalize('NFKD')
|
|
.replace(/[^a-z0-9@.]+/g, ' ')
|
|
.trim();
|
|
|
|
/** Does the value on file already match the current one? Addresses match on street + number. */
|
|
export function matchesCurrent(onFile: string | null, current: string, kind: IdentityKind) {
|
|
if (!onFile || !current) return false;
|
|
const head = kind === 'address' ? current.split(',')[0] : current;
|
|
return norm(onFile).includes(norm(head));
|
|
}
|
|
|
|
/** Still has the old value: holds it, not archived/no-action, not ticked since the change. */
|
|
export function isDue(o: OrgLike, kind: IdentityKind, current: Identity | undefined): boolean {
|
|
if (!current || o.archived || o.action === 'none' || !holds(o, kind)) return false;
|
|
const ticked = updatedOn(o, kind);
|
|
if (ticked && ticked >= current.since) return false;
|
|
const onFile = {
|
|
address: o.addressOnFile,
|
|
email: o.emailOnFile,
|
|
phone: o.phoneOnFile,
|
|
iban: null
|
|
}[kind];
|
|
if (kind === 'email' && onFile)
|
|
return !onFile.split(/[;,]\s*/).every((e) => matchesCurrent(e, current.value, kind));
|
|
return !matchesCurrent(onFile, current.value, kind);
|
|
}
|