Initial commit: Kinship Directory application
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
function parseCsv(text) {
|
||||
const rows = []; let row = []; let field = ""; let quoted = false;
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const char = text[index];
|
||||
if (char === '"') {
|
||||
if (quoted && text[index + 1] === '"') { field += '"'; index++; } else quoted = !quoted;
|
||||
} else if (char === "," && !quoted) { row.push(field); field = ""; }
|
||||
else if ((char === "\n" || char === "\r") && !quoted) {
|
||||
if (char === "\r" && text[index + 1] === "\n") index++;
|
||||
row.push(field); if (row.some(Boolean)) rows.push(row); row = []; field = "";
|
||||
} else field += char;
|
||||
}
|
||||
if (field || row.length) { row.push(field); rows.push(row); }
|
||||
const headers = rows.shift().map((header) => header.replace(/^\uFEFF/, "").trim());
|
||||
return rows.map((values) => Object.fromEntries(headers.map((header, index) => [header, (values[index] || "").trim()])));
|
||||
}
|
||||
|
||||
const value = (row, name) => row[name] || "";
|
||||
const personId = (key, role) => createHash("sha256").update(`${key}:${role}`).digest("hex").slice(0, 24);
|
||||
function person(row, key, role, number, lastName) {
|
||||
const prefix = `${role} ${number}`;
|
||||
const firstName = value(row, `${prefix} First Name`);
|
||||
if (!firstName) return null;
|
||||
return {
|
||||
id: personId(key, `${role}-${number}`), firstName, lastName,
|
||||
birthday: value(row, `${prefix} Birthday`) || undefined,
|
||||
email: (value(row, `${prefix} Email Address`) || value(row, `${prefix} Email Address `)).toLowerCase() || undefined,
|
||||
phone: value(row, `${prefix} Mobile Phone Number`) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function additionalAdults(row, key, lastName) {
|
||||
const match = value(row, "Additional Details").match(/Additional adult household members:\s*([^;]+)/i);
|
||||
if (!match) return [];
|
||||
return match[1].split(",").map((name) => name.trim()).filter(Boolean).map((firstName, index) => ({
|
||||
id: personId(key, `additional-${index}`), firstName, lastName,
|
||||
}));
|
||||
}
|
||||
|
||||
const [joinCode, csvPath] = process.argv.slice(2);
|
||||
if (!joinCode || !csvPath) {
|
||||
console.error("Usage: yarn import:contacts <organization-join-code> <csv-file>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new MongoClient(process.env.MONGODB_URI || "mongodb://127.0.0.1:27017");
|
||||
await client.connect();
|
||||
try {
|
||||
const database = client.db(process.env.MONGODB_DB || "kinship_directory");
|
||||
const organization = await database.collection("organizations").findOne({ joinCode: joinCode.toUpperCase() });
|
||||
if (!organization) throw new Error(`Organization ${joinCode} was not found.`);
|
||||
const rows = parseCsv(await readFile(csvPath, "utf8"));
|
||||
const users = await database.collection("users").find({ organizationId: organization._id }).toArray();
|
||||
const familyByEmail = new Map(users.map((user) => [String(user.email).toLowerCase(), user.familyId]));
|
||||
let created = 0; let updated = 0; let adultsImported = 0; let childrenImported = 0;
|
||||
|
||||
for (const [index, row] of rows.entries()) {
|
||||
const lastName = value(row, "Family Last Name");
|
||||
if (!lastName) continue;
|
||||
const key = `${joinCode.toUpperCase()}:${index + 2}:${lastName}:${value(row, "Address")}`;
|
||||
const adults = [person(row, key, "Adult", 1, lastName), person(row, key, "Adult", 2, lastName)].filter(Boolean);
|
||||
adults.push(...additionalAdults(row, key, lastName));
|
||||
const children = [1, 2, 3, 4].map((number) => person(row, key, "Child", number, lastName)).filter(Boolean);
|
||||
const accountFamilyId = adults.map((adult) => adult.email?.toLowerCase()).filter(Boolean).map((email) => familyByEmail.get(email)).find(Boolean);
|
||||
const address = [value(row, "Address"), value(row, "Address Line 2")].filter(Boolean).join(", ");
|
||||
const document = {
|
||||
organizationId: organization._id,
|
||||
familyName: lastName,
|
||||
address: address || undefined,
|
||||
city: value(row, "City") || undefined,
|
||||
state: value(row, "State") || undefined,
|
||||
postalCode: value(row, "ZIP Code") || undefined,
|
||||
homePhone: value(row, "Family Phone") || undefined,
|
||||
adults, children,
|
||||
importKey: key,
|
||||
importDetails: value(row, "Additional Details") || undefined,
|
||||
anniversary: value(row, "Anniversary Date") || undefined,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
const existing = accountFamilyId
|
||||
? await database.collection("families").findOne({ _id: accountFamilyId, organizationId: organization._id })
|
||||
: await database.collection("families").findOne({ organizationId: organization._id, importKey: key });
|
||||
if (existing) {
|
||||
await database.collection("families").updateOne({ _id: existing._id }, { $set: document }); updated++;
|
||||
} else {
|
||||
await database.collection("families").insertOne({ ...document, createdAt: new Date() }); created++;
|
||||
}
|
||||
adultsImported += adults.length; childrenImported += children.length;
|
||||
}
|
||||
console.log(JSON.stringify({ organization: organization.name, rows: rows.length, created, updated, adults: adultsImported, children: childrenImported }, null, 2));
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
Reference in New Issue
Block a user