data holders: private tracker of every organisation holding my address, email, phone or IBAN
Some checks failed
Deploy production / deploy (push) Has been cancelled
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
This commit is contained in:
78
src/lib/dataholders.ts
Normal file
78
src/lib/dataholders.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user