Files
website/src/lib/dataholders.ts
Stan Runge 772e699655
Some checks failed
Deploy production / deploy (push) Has been cancelled
data holders: private tracker of every organisation holding my address, email, phone or IBAN
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
2026-10-25 16:10:24 +00:00

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);
}