batch import csv

This commit is contained in:
debu-2 2026-10-05 04:53:45 +05:30
parent a679775f57
commit dd0ceef665
20 changed files with 1614 additions and 190 deletions

View file

@ -0,0 +1,177 @@
const SAMPLE_CSV = `First name*,Last name*,Work email*,Invitation email,Initial password
Rahul,Sharma,rahul,rahul@gmail.com,Roadrentals123
Anurag,Jangid,anurag.jangid,anurag.personal@gmail.com,Roadrentals123`;
const HEADER_ALIASES = {
"first name": "first_name",
"first name*": "first_name",
"last name": "last_name",
"last name*": "last_name",
"work email": "work_email",
"work email*": "work_email",
"invitation email": "invitation_email",
"initial password": "password",
};
export const IMPORT_ROW_LIMIT = 300;
function normalizeHeader(value) {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}
function parseCsvLine(line) {
const cells = [];
let current = "";
let inQuotes = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if (char === '"') {
if (inQuotes && line[index + 1] === '"') {
current += '"';
index += 1;
} else {
inQuotes = !inQuotes;
}
continue;
}
if (char === "," && !inQuotes) {
cells.push(current.trim());
current = "";
continue;
}
current += char;
}
cells.push(current.trim());
return cells;
}
export function buildSampleCsvBlob() {
return new Blob([SAMPLE_CSV], { type: "text/csv;charset=utf-8" });
}
export function downloadSampleCsv() {
const url = URL.createObjectURL(buildSampleCsvBlob());
const link = document.createElement("a");
link.href = url;
link.download = "agmail-people-import-sample.csv";
link.click();
URL.revokeObjectURL(url);
}
export function parseUsersCsv(text) {
const lines = text
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
if (lines.length < 2) {
throw new Error("CSV must include a header row and at least one person.");
}
const headers = parseCsvLine(lines[0]).map(normalizeHeader);
const columnMap = headers.map((header) => HEADER_ALIASES[header]);
if (columnMap.some((column) => !column)) {
throw new Error(
"CSV headers must be: First name*, Last name*, Work email*, Invitation email, Initial password",
);
}
const rows = lines.slice(1).map((line, index) => {
const cells = parseCsvLine(line);
const row = {
first_name: "",
last_name: "",
work_email: "",
invitation_email: "",
password: "",
};
columnMap.forEach((field, columnIndex) => {
row[field] = cells[columnIndex]?.trim() || "";
});
return { rowNumber: index + 2, ...row };
});
if (rows.length > IMPORT_ROW_LIMIT) {
throw new Error(`You can import up to ${IMPORT_ROW_LIMIT} people at a time.`);
}
return rows;
}
const PASSWORD_PATTERN = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
export function validateImportRows(rows, { companyDomain, sendInvitation }) {
const errors = [];
const seenEmails = new Set();
const seenInvites = new Set();
rows.forEach((row) => {
const rowErrors = [];
if (!row.first_name.trim()) rowErrors.push("First name is required.");
if (!row.last_name.trim()) rowErrors.push("Last name is required.");
const workEmailRaw = row.work_email.trim().toLowerCase();
const workEmail = workEmailRaw.includes("@")
? workEmailRaw
: `${workEmailRaw}@${companyDomain}`;
const localPart = workEmail.split("@")[0];
if (!workEmailRaw) {
rowErrors.push("Work email is required.");
} else if (!workEmail.endsWith(`@${companyDomain}`)) {
rowErrors.push(`Work email must use @${companyDomain}.`);
} else if (!/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/.test(localPart)) {
rowErrors.push("Work email format is invalid.");
} else if (seenEmails.has(workEmail)) {
rowErrors.push("Duplicate work email in this file.");
}
seenEmails.add(workEmail);
const password = row.password.trim();
if (!password) {
rowErrors.push("Initial password is required.");
} else if (!PASSWORD_PATTERN.test(password)) {
rowErrors.push("Password must be at least 8 characters with upper, lower, and a number.");
}
const invitationEmail = row.invitation_email.trim().toLowerCase();
if (sendInvitation) {
if (!invitationEmail) {
rowErrors.push("Invitation email is required when sending invitations.");
} else if (invitationEmail.endsWith(`@${companyDomain}`)) {
rowErrors.push("Use a personal email for invitations.");
} else if (seenInvites.has(invitationEmail)) {
rowErrors.push("Duplicate invitation email in this file.");
} else {
seenInvites.add(invitationEmail);
}
}
if (rowErrors.length > 0) {
errors.push({ row: row.rowNumber, email: workEmail, messages: rowErrors });
}
});
return errors;
}
export function rowsToImportPayload(rows, options) {
const { orgRole, sendInvitation, forcePasswordChange, companyDomain } = options;
return {
org_role: orgRole,
send_invitation: sendInvitation,
force_password_change: forcePasswordChange,
rows: rows.map((row) => ({
first_name: row.first_name.trim(),
last_name: row.last_name.trim(),
work_email: row.work_email.trim().toLowerCase().includes("@")
? row.work_email.trim().toLowerCase()
: `${row.work_email.trim().toLowerCase()}@${companyDomain}`,
invitation_email: row.invitation_email.trim() || undefined,
password: row.password.trim(),
})),
};
}