166 lines
3.9 KiB
TypeScript
166 lines
3.9 KiB
TypeScript
import { readFile } from "node:fs/promises"
|
|
import { resolve } from "node:path"
|
|
import { config as loadEnv } from "dotenv"
|
|
import { fetchMutation } from "convex/nextjs"
|
|
import { api } from "@/convex/_generated/api"
|
|
|
|
loadEnv({ path: ".env.local" })
|
|
|
|
type ImportRecord = {
|
|
firstName?: string
|
|
lastName?: string
|
|
name?: string
|
|
email?: string
|
|
phone?: string
|
|
company?: string
|
|
notes?: string
|
|
}
|
|
|
|
function normalizePhone(value?: string | null) {
|
|
const digits = String(value || "").replace(/\D/g, "")
|
|
if (!digits) {
|
|
return ""
|
|
}
|
|
if (digits.length === 10) {
|
|
return `+1${digits}`
|
|
}
|
|
if (digits.length === 11 && digits.startsWith("1")) {
|
|
return `+${digits}`
|
|
}
|
|
if (digits.length >= 11) {
|
|
return `+${digits}`
|
|
}
|
|
return ""
|
|
}
|
|
|
|
function splitCsvLine(line: string) {
|
|
const values: string[] = []
|
|
let current = ""
|
|
let inQuotes = false
|
|
|
|
for (let index = 0; index < line.length; index += 1) {
|
|
const char = line[index]
|
|
const next = line[index + 1]
|
|
|
|
if (char === '"' && inQuotes && next === '"') {
|
|
current += '"'
|
|
index += 1
|
|
continue
|
|
}
|
|
|
|
if (char === '"') {
|
|
inQuotes = !inQuotes
|
|
continue
|
|
}
|
|
|
|
if (char === "," && !inQuotes) {
|
|
values.push(current.trim())
|
|
current = ""
|
|
continue
|
|
}
|
|
|
|
current += char
|
|
}
|
|
|
|
values.push(current.trim())
|
|
return values
|
|
}
|
|
|
|
function parseCsv(content: string): ImportRecord[] {
|
|
const lines = content
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
|
|
if (lines.length < 2) {
|
|
return []
|
|
}
|
|
|
|
const headers = splitCsvLine(lines[0]).map((header) => header.toLowerCase())
|
|
return lines.slice(1).map((line) => {
|
|
const values = splitCsvLine(line)
|
|
const record: Record<string, string> = {}
|
|
headers.forEach((header, index) => {
|
|
record[header] = values[index] || ""
|
|
})
|
|
|
|
return {
|
|
firstName: record.firstname || record["first name"] || record.first_name,
|
|
lastName: record.lastname || record["last name"] || record.last_name,
|
|
name: record.name || record.fullname || record["full name"],
|
|
email: record.email || record["email address"],
|
|
phone: record.phone || record["phone number"] || record.mobile,
|
|
company: record.company || record["company name"],
|
|
notes: record.notes || record.note,
|
|
}
|
|
})
|
|
}
|
|
|
|
function parseJson(content: string): ImportRecord[] {
|
|
const value = JSON.parse(content)
|
|
if (!Array.isArray(value)) {
|
|
throw new Error("JSON import file must contain an array of contacts.")
|
|
}
|
|
return value
|
|
}
|
|
|
|
async function loadRecords(pathname: string) {
|
|
const absolutePath = resolve(pathname)
|
|
const content = await readFile(absolutePath, "utf8")
|
|
if (absolutePath.endsWith(".json")) {
|
|
return parseJson(content)
|
|
}
|
|
return parseCsv(content)
|
|
}
|
|
|
|
async function main() {
|
|
const inputPath = process.argv[2]
|
|
if (!inputPath) {
|
|
throw new Error("Usage: tsx scripts/import-ghl-contacts-to-contact-profiles.ts <contacts.json|contacts.csv>")
|
|
}
|
|
|
|
const records = await loadRecords(inputPath)
|
|
let imported = 0
|
|
let skipped = 0
|
|
|
|
for (const record of records) {
|
|
const normalizedPhone = normalizePhone(record.phone)
|
|
if (!normalizedPhone) {
|
|
skipped += 1
|
|
continue
|
|
}
|
|
|
|
const displayName =
|
|
record.name?.trim() ||
|
|
[record.firstName, record.lastName].filter(Boolean).join(" ").trim()
|
|
|
|
await fetchMutation(api.contactProfiles.upsertByPhone, {
|
|
normalizedPhone,
|
|
displayName: displayName || undefined,
|
|
firstName: record.firstName?.trim() || undefined,
|
|
lastName: record.lastName?.trim() || undefined,
|
|
email: record.email?.trim() || undefined,
|
|
company: record.company?.trim() || undefined,
|
|
reminderNotes: record.notes?.trim() || undefined,
|
|
source: "ghl-import",
|
|
})
|
|
imported += 1
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
imported,
|
|
skipped,
|
|
total: records.length,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|