@hasna/contacts 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1568 -206
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts +1 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/deals.d.ts +14 -0
- package/dist/db/deals.d.ts.map +1 -0
- package/dist/db/events.d.ts +14 -0
- package/dist/db/events.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1190 -81
- package/dist/lib/apple-contacts.d.ts +3 -0
- package/dist/lib/apple-contacts.d.ts.map +1 -0
- package/dist/lib/audit.d.ts +12 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +3 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/stats.d.ts +24 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/timeline.d.ts +11 -0
- package/dist/lib/timeline.d.ts.map +1 -0
- package/dist/lib/upcoming.d.ts +14 -0
- package/dist/lib/upcoming.d.ts.map +1 -0
- package/dist/mcp/index.js +1263 -173
- package/dist/server/index.js +125 -4
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2448,6 +2448,39 @@ var init_database = __esm(() => {
|
|
|
2448
2448
|
);
|
|
2449
2449
|
|
|
2450
2450
|
ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
|
|
2451
|
+
`,
|
|
2452
|
+
`
|
|
2453
|
+
ALTER TABLE contacts ADD COLUMN do_not_contact INTEGER NOT NULL DEFAULT 0;
|
|
2454
|
+
ALTER TABLE contacts ADD COLUMN priority INTEGER NOT NULL DEFAULT 3 CHECK(priority BETWEEN 1 AND 5);
|
|
2455
|
+
ALTER TABLE contacts ADD COLUMN timezone TEXT;
|
|
2456
|
+
|
|
2457
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
2458
|
+
id TEXT PRIMARY KEY,
|
|
2459
|
+
title TEXT NOT NULL,
|
|
2460
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
|
|
2461
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
2462
|
+
stage TEXT NOT NULL DEFAULT 'lead' CHECK(stage IN ('lead','qualified','proposal','negotiation','won','lost','cancelled')),
|
|
2463
|
+
value_usd REAL,
|
|
2464
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
2465
|
+
close_date TEXT,
|
|
2466
|
+
notes TEXT,
|
|
2467
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2468
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2469
|
+
);
|
|
2470
|
+
|
|
2471
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
2472
|
+
id TEXT PRIMARY KEY,
|
|
2473
|
+
title TEXT NOT NULL,
|
|
2474
|
+
type TEXT NOT NULL DEFAULT 'meeting' CHECK(type IN ('meeting','call','lunch','email','demo','conference','intro','other')),
|
|
2475
|
+
event_date TEXT NOT NULL,
|
|
2476
|
+
duration_min INTEGER,
|
|
2477
|
+
contact_ids TEXT NOT NULL DEFAULT '[]',
|
|
2478
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
2479
|
+
notes TEXT,
|
|
2480
|
+
outcome TEXT,
|
|
2481
|
+
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
2482
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2483
|
+
);
|
|
2451
2484
|
`
|
|
2452
2485
|
];
|
|
2453
2486
|
});
|
|
@@ -2463,6 +2496,24 @@ var init_activity = __esm(() => {
|
|
|
2463
2496
|
});
|
|
2464
2497
|
|
|
2465
2498
|
// src/db/contacts.ts
|
|
2499
|
+
var exports_contacts = {};
|
|
2500
|
+
__export(exports_contacts, {
|
|
2501
|
+
updateContact: () => updateContact,
|
|
2502
|
+
unarchiveContact: () => unarchiveContact,
|
|
2503
|
+
searchContacts: () => searchContacts,
|
|
2504
|
+
mergeContacts: () => mergeContacts,
|
|
2505
|
+
listRecentContacts: () => listRecentContacts,
|
|
2506
|
+
listContacts: () => listContacts,
|
|
2507
|
+
listColdContacts: () => listColdContacts,
|
|
2508
|
+
getContactByEmail: () => getContactByEmail,
|
|
2509
|
+
getContact: () => getContact,
|
|
2510
|
+
deleteContact: () => deleteContact,
|
|
2511
|
+
createContact: () => createContact,
|
|
2512
|
+
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
2513
|
+
archiveContact: () => archiveContact,
|
|
2514
|
+
addPhoneToContact: () => addPhoneToContact,
|
|
2515
|
+
addEmailToContact: () => addEmailToContact
|
|
2516
|
+
});
|
|
2466
2517
|
function rowToContact(row) {
|
|
2467
2518
|
return {
|
|
2468
2519
|
...row,
|
|
@@ -2472,7 +2523,10 @@ function rowToContact(row) {
|
|
|
2472
2523
|
status: row.status ?? "active",
|
|
2473
2524
|
follow_up_at: row.follow_up_at ?? null,
|
|
2474
2525
|
archived: !!row.archived,
|
|
2475
|
-
project_id: row.project_id ?? null
|
|
2526
|
+
project_id: row.project_id ?? null,
|
|
2527
|
+
do_not_contact: !!row.do_not_contact,
|
|
2528
|
+
priority: row.priority ?? 3,
|
|
2529
|
+
timezone: row.timezone ?? null
|
|
2476
2530
|
};
|
|
2477
2531
|
}
|
|
2478
2532
|
function rowToEmail(row) {
|
|
@@ -2557,8 +2611,8 @@ function createContact(input, db) {
|
|
|
2557
2611
|
const firstName = input.first_name ?? "";
|
|
2558
2612
|
const lastName = input.last_name ?? "";
|
|
2559
2613
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
2560
|
-
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, created_at, updated_at)
|
|
2561
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2614
|
+
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, do_not_contact, priority, timezone, created_at, updated_at)
|
|
2615
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2562
2616
|
id,
|
|
2563
2617
|
firstName,
|
|
2564
2618
|
lastName,
|
|
@@ -2577,6 +2631,9 @@ function createContact(input, db) {
|
|
|
2577
2631
|
input.status ?? "active",
|
|
2578
2632
|
input.follow_up_at ?? null,
|
|
2579
2633
|
input.project_id ?? null,
|
|
2634
|
+
input.do_not_contact ? 1 : 0,
|
|
2635
|
+
input.priority ?? 3,
|
|
2636
|
+
input.timezone ?? null,
|
|
2580
2637
|
timestamp,
|
|
2581
2638
|
timestamp
|
|
2582
2639
|
]);
|
|
@@ -2620,12 +2677,18 @@ function listContacts(opts = {}, db) {
|
|
|
2620
2677
|
last_contacted_after,
|
|
2621
2678
|
last_contacted_before,
|
|
2622
2679
|
order_by = "display_name",
|
|
2623
|
-
order_dir = "asc"
|
|
2680
|
+
order_dir = "asc",
|
|
2681
|
+
include_dnc = false,
|
|
2682
|
+
priority_min,
|
|
2683
|
+
updated_since
|
|
2624
2684
|
} = opts;
|
|
2625
2685
|
const conditions = [];
|
|
2626
2686
|
const params = [];
|
|
2627
2687
|
conditions.push("c.archived = ?");
|
|
2628
2688
|
params.push(archived ? 1 : 0);
|
|
2689
|
+
if (!include_dnc) {
|
|
2690
|
+
conditions.push("c.do_not_contact = 0");
|
|
2691
|
+
}
|
|
2629
2692
|
if (company_id) {
|
|
2630
2693
|
conditions.push("c.company_id = ?");
|
|
2631
2694
|
params.push(company_id);
|
|
@@ -2663,6 +2726,14 @@ function listContacts(opts = {}, db) {
|
|
|
2663
2726
|
conditions.push("c.last_contacted_at <= ?");
|
|
2664
2727
|
params.push(last_contacted_before);
|
|
2665
2728
|
}
|
|
2729
|
+
if (priority_min !== undefined) {
|
|
2730
|
+
conditions.push("c.priority >= ?");
|
|
2731
|
+
params.push(priority_min);
|
|
2732
|
+
}
|
|
2733
|
+
if (updated_since) {
|
|
2734
|
+
conditions.push("c.updated_at >= ?");
|
|
2735
|
+
params.push(updated_since);
|
|
2736
|
+
}
|
|
2666
2737
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2667
2738
|
const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
|
|
2668
2739
|
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
@@ -2746,6 +2817,18 @@ function updateContact(id, input, db) {
|
|
|
2746
2817
|
setClauses.push("project_id = ?");
|
|
2747
2818
|
params.push(input.project_id);
|
|
2748
2819
|
}
|
|
2820
|
+
if (input.do_not_contact !== undefined) {
|
|
2821
|
+
setClauses.push("do_not_contact = ?");
|
|
2822
|
+
params.push(input.do_not_contact ? 1 : 0);
|
|
2823
|
+
}
|
|
2824
|
+
if (input.priority !== undefined) {
|
|
2825
|
+
setClauses.push("priority = ?");
|
|
2826
|
+
params.push(input.priority ?? 3);
|
|
2827
|
+
}
|
|
2828
|
+
if (input.timezone !== undefined) {
|
|
2829
|
+
setClauses.push("timezone = ?");
|
|
2830
|
+
params.push(input.timezone);
|
|
2831
|
+
}
|
|
2749
2832
|
params.push(id);
|
|
2750
2833
|
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
2751
2834
|
if (input.emails_add?.length) {
|
|
@@ -2818,6 +2901,160 @@ function listRecentContacts(limit, db) {
|
|
|
2818
2901
|
const rows = d.query(`SELECT * FROM contacts ORDER BY updated_at DESC LIMIT ?`).all(limit);
|
|
2819
2902
|
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
2820
2903
|
}
|
|
2904
|
+
function mergeContacts(keepId, mergeId, db) {
|
|
2905
|
+
const d = db || getDatabase();
|
|
2906
|
+
const keepRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
2907
|
+
if (!keepRow)
|
|
2908
|
+
throw new ContactNotFoundError(keepId);
|
|
2909
|
+
const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
|
|
2910
|
+
if (!mergeRow)
|
|
2911
|
+
throw new ContactNotFoundError(mergeId);
|
|
2912
|
+
const mergeEmailRows = d.query(`SELECT * FROM emails WHERE contact_id = ?`).all(mergeId);
|
|
2913
|
+
for (const e of mergeEmailRows) {
|
|
2914
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(keepId, e.address);
|
|
2915
|
+
if (exists) {
|
|
2916
|
+
d.run(`DELETE FROM emails WHERE id = ?`, [e.id]);
|
|
2917
|
+
} else {
|
|
2918
|
+
d.run(`UPDATE emails SET contact_id = ? WHERE id = ?`, [keepId, e.id]);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const mergePhoneRows = d.query(`SELECT * FROM phones WHERE contact_id = ?`).all(mergeId);
|
|
2922
|
+
for (const p of mergePhoneRows) {
|
|
2923
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(keepId, p.number);
|
|
2924
|
+
if (exists) {
|
|
2925
|
+
d.run(`DELETE FROM phones WHERE id = ?`, [p.id]);
|
|
2926
|
+
} else {
|
|
2927
|
+
d.run(`UPDATE phones SET contact_id = ? WHERE id = ?`, [keepId, p.id]);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
2931
|
+
d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
2932
|
+
const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
|
|
2933
|
+
for (const { tag_id } of mergeTags) {
|
|
2934
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [keepId, tag_id]);
|
|
2935
|
+
}
|
|
2936
|
+
d.run(`UPDATE contact_relationships SET contact_a_id = ? WHERE contact_a_id = ?`, [keepId, mergeId]);
|
|
2937
|
+
d.run(`UPDATE contact_relationships SET contact_b_id = ? WHERE contact_b_id = ?`, [keepId, mergeId]);
|
|
2938
|
+
d.run(`UPDATE activity_log SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
2939
|
+
d.run(`DELETE FROM contacts WHERE id = ?`, [mergeId]);
|
|
2940
|
+
const updates = ["updated_at = ?"];
|
|
2941
|
+
const params = [now()];
|
|
2942
|
+
if (!keepRow.notes && mergeRow.notes) {
|
|
2943
|
+
updates.push("notes = ?");
|
|
2944
|
+
params.push(mergeRow.notes);
|
|
2945
|
+
}
|
|
2946
|
+
if (!keepRow.nickname && mergeRow.nickname) {
|
|
2947
|
+
updates.push("nickname = ?");
|
|
2948
|
+
params.push(mergeRow.nickname);
|
|
2949
|
+
}
|
|
2950
|
+
if (!keepRow.avatar_url && mergeRow.avatar_url) {
|
|
2951
|
+
updates.push("avatar_url = ?");
|
|
2952
|
+
params.push(mergeRow.avatar_url);
|
|
2953
|
+
}
|
|
2954
|
+
if (!keepRow.birthday && mergeRow.birthday) {
|
|
2955
|
+
updates.push("birthday = ?");
|
|
2956
|
+
params.push(mergeRow.birthday);
|
|
2957
|
+
}
|
|
2958
|
+
if (!keepRow.company_id && mergeRow.company_id) {
|
|
2959
|
+
updates.push("company_id = ?");
|
|
2960
|
+
params.push(mergeRow.company_id);
|
|
2961
|
+
}
|
|
2962
|
+
if (!keepRow.job_title && mergeRow.job_title) {
|
|
2963
|
+
updates.push("job_title = ?");
|
|
2964
|
+
params.push(mergeRow.job_title);
|
|
2965
|
+
}
|
|
2966
|
+
params.push(keepId);
|
|
2967
|
+
d.run(`UPDATE contacts SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
2968
|
+
logActivity(d, {
|
|
2969
|
+
contact_id: keepId,
|
|
2970
|
+
action: "contact.merged",
|
|
2971
|
+
details: `Merged contact ${mergeRow.display_name} (${mergeId}) into ${keepRow.display_name} (${keepId})`
|
|
2972
|
+
});
|
|
2973
|
+
const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
2974
|
+
return loadContactDetails(d, rowToContact(finalRow));
|
|
2975
|
+
}
|
|
2976
|
+
function getContactByEmail(email, db) {
|
|
2977
|
+
const d = db || getDatabase();
|
|
2978
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(email);
|
|
2979
|
+
if (!emailRow)
|
|
2980
|
+
return null;
|
|
2981
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(emailRow.contact_id);
|
|
2982
|
+
if (!row)
|
|
2983
|
+
return null;
|
|
2984
|
+
return loadContactDetails(d, rowToContact(row));
|
|
2985
|
+
}
|
|
2986
|
+
function addEmailToContact(contactId, email, db) {
|
|
2987
|
+
const d = db || getDatabase();
|
|
2988
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
2989
|
+
if (!row)
|
|
2990
|
+
throw new ContactNotFoundError(contactId);
|
|
2991
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(contactId, email.address);
|
|
2992
|
+
if (!exists) {
|
|
2993
|
+
d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), contactId, email.address, email.type ?? "work", email.is_primary ? 1 : 0]);
|
|
2994
|
+
}
|
|
2995
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
2996
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
2997
|
+
}
|
|
2998
|
+
function addPhoneToContact(contactId, phone, db) {
|
|
2999
|
+
const d = db || getDatabase();
|
|
3000
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
3001
|
+
if (!row)
|
|
3002
|
+
throw new ContactNotFoundError(contactId);
|
|
3003
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(contactId, phone.number);
|
|
3004
|
+
if (!exists) {
|
|
3005
|
+
d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), contactId, phone.number, phone.country_code ?? null, phone.type ?? "mobile", phone.is_primary ? 1 : 0]);
|
|
3006
|
+
}
|
|
3007
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
3008
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
3009
|
+
}
|
|
3010
|
+
function archiveContact(id, db) {
|
|
3011
|
+
const d = db || getDatabase();
|
|
3012
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
3013
|
+
if (!row)
|
|
3014
|
+
throw new ContactNotFoundError(id);
|
|
3015
|
+
d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
3016
|
+
logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
|
|
3017
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
3018
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
3019
|
+
}
|
|
3020
|
+
function unarchiveContact(id, db) {
|
|
3021
|
+
const d = db || getDatabase();
|
|
3022
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
3023
|
+
if (!row)
|
|
3024
|
+
throw new ContactNotFoundError(id);
|
|
3025
|
+
d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
3026
|
+
logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
|
|
3027
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
3028
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
3029
|
+
}
|
|
3030
|
+
function listColdContacts(days, db) {
|
|
3031
|
+
const d = db || getDatabase();
|
|
3032
|
+
const rows = d.query(`SELECT c.* FROM contacts c
|
|
3033
|
+
WHERE c.archived = 0 AND c.do_not_contact = 0
|
|
3034
|
+
AND (c.last_contacted_at IS NULL OR c.last_contacted_at < datetime('now', ? || ' days'))
|
|
3035
|
+
ORDER BY c.last_contacted_at ASC NULLS FIRST
|
|
3036
|
+
LIMIT 100`).all(`-${days}`);
|
|
3037
|
+
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
3038
|
+
}
|
|
3039
|
+
function autoLinkContactToCompany(contactId, db) {
|
|
3040
|
+
const d = db || getDatabase();
|
|
3041
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
3042
|
+
if (!row || row.company_id)
|
|
3043
|
+
return null;
|
|
3044
|
+
const emailRow = d.query(`SELECT address FROM emails WHERE contact_id = ? AND contact_id IS NOT NULL LIMIT 1`).get(contactId);
|
|
3045
|
+
if (!emailRow)
|
|
3046
|
+
return null;
|
|
3047
|
+
const domain = emailRow.address.split("@")[1];
|
|
3048
|
+
if (!domain)
|
|
3049
|
+
return null;
|
|
3050
|
+
const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
|
|
3051
|
+
if (!companyRow)
|
|
3052
|
+
return null;
|
|
3053
|
+
d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now(), contactId]);
|
|
3054
|
+
logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
|
|
3055
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
3056
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
3057
|
+
}
|
|
2821
3058
|
var init_contacts = __esm(() => {
|
|
2822
3059
|
init_types();
|
|
2823
3060
|
init_database();
|
|
@@ -3053,7 +3290,201 @@ var init_companies = __esm(() => {
|
|
|
3053
3290
|
init_activity();
|
|
3054
3291
|
});
|
|
3055
3292
|
|
|
3293
|
+
// src/db/relationships.ts
|
|
3294
|
+
function rowToCompanyRelationship(row) {
|
|
3295
|
+
return {
|
|
3296
|
+
...row,
|
|
3297
|
+
relationship_type: row.relationship_type,
|
|
3298
|
+
start_date: row.start_date ?? null,
|
|
3299
|
+
end_date: row.end_date ?? null,
|
|
3300
|
+
is_primary: !!row.is_primary,
|
|
3301
|
+
status: row.status ?? "active"
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
function createCompanyRelationship(input, db) {
|
|
3305
|
+
const d = db || getDatabase();
|
|
3306
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
|
|
3307
|
+
if (!contact)
|
|
3308
|
+
throw new ContactNotFoundError(input.contact_id);
|
|
3309
|
+
const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
|
|
3310
|
+
if (!company)
|
|
3311
|
+
throw new Error(`Company ${input.company_id} not found`);
|
|
3312
|
+
const id = uuid();
|
|
3313
|
+
d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3314
|
+
id,
|
|
3315
|
+
input.contact_id,
|
|
3316
|
+
input.company_id,
|
|
3317
|
+
input.relationship_type,
|
|
3318
|
+
input.notes ?? null,
|
|
3319
|
+
input.start_date ?? null,
|
|
3320
|
+
input.end_date ?? null,
|
|
3321
|
+
input.is_primary ? 1 : 0,
|
|
3322
|
+
input.status ?? "active"
|
|
3323
|
+
]);
|
|
3324
|
+
return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
|
|
3325
|
+
}
|
|
3326
|
+
function listCompanyRelationships(opts = {}, db) {
|
|
3327
|
+
const d = db || getDatabase();
|
|
3328
|
+
const conditions = [];
|
|
3329
|
+
const params = [];
|
|
3330
|
+
if (opts.contact_id) {
|
|
3331
|
+
conditions.push("contact_id = ?");
|
|
3332
|
+
params.push(opts.contact_id);
|
|
3333
|
+
}
|
|
3334
|
+
if (opts.company_id) {
|
|
3335
|
+
conditions.push("company_id = ?");
|
|
3336
|
+
params.push(opts.company_id);
|
|
3337
|
+
}
|
|
3338
|
+
if (opts.relationship_type) {
|
|
3339
|
+
conditions.push("relationship_type = ?");
|
|
3340
|
+
params.push(opts.relationship_type);
|
|
3341
|
+
}
|
|
3342
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3343
|
+
const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
|
|
3344
|
+
return rows.map(rowToCompanyRelationship);
|
|
3345
|
+
}
|
|
3346
|
+
var init_relationships = __esm(() => {
|
|
3347
|
+
init_types();
|
|
3348
|
+
init_database();
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
// src/db/contact-tasks.ts
|
|
3352
|
+
function rowToContactTask(row) {
|
|
3353
|
+
return {
|
|
3354
|
+
id: row.id,
|
|
3355
|
+
title: row.title,
|
|
3356
|
+
description: row.description,
|
|
3357
|
+
contact_id: row.contact_id,
|
|
3358
|
+
assigned_by: row.assigned_by,
|
|
3359
|
+
deadline: row.deadline,
|
|
3360
|
+
status: row.status,
|
|
3361
|
+
priority: row.priority,
|
|
3362
|
+
entity_id: row.entity_id,
|
|
3363
|
+
linked_todos_task_id: row.linked_todos_task_id,
|
|
3364
|
+
escalation_rules: JSON.parse(row.escalation_rules || "[]"),
|
|
3365
|
+
created_at: row.created_at,
|
|
3366
|
+
updated_at: row.updated_at
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
function createContactTask(input, db) {
|
|
3370
|
+
const d = db || getDatabase();
|
|
3371
|
+
const id = uuid();
|
|
3372
|
+
const timestamp = now();
|
|
3373
|
+
d.run(`INSERT INTO contact_tasks
|
|
3374
|
+
(id, title, description, contact_id, assigned_by, deadline, status, priority,
|
|
3375
|
+
entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
|
|
3376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3377
|
+
id,
|
|
3378
|
+
input.title,
|
|
3379
|
+
input.description ?? null,
|
|
3380
|
+
input.contact_id,
|
|
3381
|
+
input.assigned_by ?? null,
|
|
3382
|
+
input.deadline ?? null,
|
|
3383
|
+
input.status ?? "pending",
|
|
3384
|
+
input.priority ?? "medium",
|
|
3385
|
+
input.entity_id ?? null,
|
|
3386
|
+
input.linked_todos_task_id ?? null,
|
|
3387
|
+
JSON.stringify(input.escalation_rules ?? []),
|
|
3388
|
+
timestamp,
|
|
3389
|
+
timestamp
|
|
3390
|
+
]);
|
|
3391
|
+
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
3392
|
+
}
|
|
3393
|
+
function listContactTasks(opts = {}, db) {
|
|
3394
|
+
const d = db || getDatabase();
|
|
3395
|
+
const conditions = [];
|
|
3396
|
+
const params = [];
|
|
3397
|
+
if (opts.contact_id) {
|
|
3398
|
+
conditions.push("contact_id = ?");
|
|
3399
|
+
params.push(opts.contact_id);
|
|
3400
|
+
}
|
|
3401
|
+
if (opts.entity_id) {
|
|
3402
|
+
conditions.push("entity_id = ?");
|
|
3403
|
+
params.push(opts.entity_id);
|
|
3404
|
+
}
|
|
3405
|
+
if (opts.status) {
|
|
3406
|
+
conditions.push("status = ?");
|
|
3407
|
+
params.push(opts.status);
|
|
3408
|
+
}
|
|
3409
|
+
if (opts.priority) {
|
|
3410
|
+
conditions.push("priority = ?");
|
|
3411
|
+
params.push(opts.priority);
|
|
3412
|
+
}
|
|
3413
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3414
|
+
const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
|
|
3415
|
+
return rows.map(rowToContactTask);
|
|
3416
|
+
}
|
|
3417
|
+
function updateContactTask(id, input, db) {
|
|
3418
|
+
const d = db || getDatabase();
|
|
3419
|
+
const setClauses = ["updated_at = ?"];
|
|
3420
|
+
const params = [now()];
|
|
3421
|
+
if (input.title !== undefined) {
|
|
3422
|
+
setClauses.push("title = ?");
|
|
3423
|
+
params.push(input.title);
|
|
3424
|
+
}
|
|
3425
|
+
if ("description" in input) {
|
|
3426
|
+
setClauses.push("description = ?");
|
|
3427
|
+
params.push(input.description ?? null);
|
|
3428
|
+
}
|
|
3429
|
+
if ("assigned_by" in input) {
|
|
3430
|
+
setClauses.push("assigned_by = ?");
|
|
3431
|
+
params.push(input.assigned_by ?? null);
|
|
3432
|
+
}
|
|
3433
|
+
if ("deadline" in input) {
|
|
3434
|
+
setClauses.push("deadline = ?");
|
|
3435
|
+
params.push(input.deadline ?? null);
|
|
3436
|
+
}
|
|
3437
|
+
if (input.status !== undefined) {
|
|
3438
|
+
setClauses.push("status = ?");
|
|
3439
|
+
params.push(input.status);
|
|
3440
|
+
}
|
|
3441
|
+
if (input.priority !== undefined) {
|
|
3442
|
+
setClauses.push("priority = ?");
|
|
3443
|
+
params.push(input.priority);
|
|
3444
|
+
}
|
|
3445
|
+
if ("entity_id" in input) {
|
|
3446
|
+
setClauses.push("entity_id = ?");
|
|
3447
|
+
params.push(input.entity_id ?? null);
|
|
3448
|
+
}
|
|
3449
|
+
if ("linked_todos_task_id" in input) {
|
|
3450
|
+
setClauses.push("linked_todos_task_id = ?");
|
|
3451
|
+
params.push(input.linked_todos_task_id ?? null);
|
|
3452
|
+
}
|
|
3453
|
+
if (input.escalation_rules !== undefined) {
|
|
3454
|
+
setClauses.push("escalation_rules = ?");
|
|
3455
|
+
params.push(JSON.stringify(input.escalation_rules));
|
|
3456
|
+
}
|
|
3457
|
+
params.push(id);
|
|
3458
|
+
d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
3459
|
+
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
3460
|
+
}
|
|
3461
|
+
function listOverdueTasks(db) {
|
|
3462
|
+
const d = db || getDatabase();
|
|
3463
|
+
const now_iso = new Date().toISOString();
|
|
3464
|
+
const rows = d.query(`SELECT * FROM contact_tasks
|
|
3465
|
+
WHERE deadline < ? AND status NOT IN ('completed','cancelled')
|
|
3466
|
+
ORDER BY deadline ASC`).all(now_iso);
|
|
3467
|
+
return rows.map(rowToContactTask);
|
|
3468
|
+
}
|
|
3469
|
+
var init_contact_tasks = __esm(() => {
|
|
3470
|
+
init_database();
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3056
3473
|
// src/db/tags.ts
|
|
3474
|
+
var exports_tags = {};
|
|
3475
|
+
__export(exports_tags, {
|
|
3476
|
+
updateTag: () => updateTag,
|
|
3477
|
+
removeTagFromContact: () => removeTagFromContact,
|
|
3478
|
+
removeTagFromCompany: () => removeTagFromCompany,
|
|
3479
|
+
listTags: () => listTags,
|
|
3480
|
+
listContactsByTag: () => listContactsByTag,
|
|
3481
|
+
getTagByName: () => getTagByName,
|
|
3482
|
+
getTag: () => getTag,
|
|
3483
|
+
deleteTag: () => deleteTag,
|
|
3484
|
+
createTag: () => createTag,
|
|
3485
|
+
addTagToContact: () => addTagToContact,
|
|
3486
|
+
addTagToCompany: () => addTagToCompany
|
|
3487
|
+
});
|
|
3057
3488
|
function rowToTag2(row) {
|
|
3058
3489
|
return { ...row };
|
|
3059
3490
|
}
|
|
@@ -3066,10 +3497,52 @@ function createTag(input, db) {
|
|
|
3066
3497
|
d.run(`INSERT INTO tags (id, name, color, description) VALUES (?, ?, ?, ?)`, [id, input.name, input.color ?? "#6366f1", input.description ?? null]);
|
|
3067
3498
|
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
3068
3499
|
}
|
|
3069
|
-
function
|
|
3500
|
+
function getTag(id, db) {
|
|
3070
3501
|
const d = db || getDatabase();
|
|
3071
|
-
|
|
3072
|
-
|
|
3502
|
+
const row = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
|
|
3503
|
+
if (!row)
|
|
3504
|
+
throw new TagNotFoundError(id);
|
|
3505
|
+
return rowToTag2(row);
|
|
3506
|
+
}
|
|
3507
|
+
function getTagByName(name, db) {
|
|
3508
|
+
const d = db || getDatabase();
|
|
3509
|
+
const row = d.query(`SELECT * FROM tags WHERE name = ?`).get(name);
|
|
3510
|
+
return row ? rowToTag2(row) : null;
|
|
3511
|
+
}
|
|
3512
|
+
function listTags(db) {
|
|
3513
|
+
const d = db || getDatabase();
|
|
3514
|
+
return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
|
|
3515
|
+
}
|
|
3516
|
+
function updateTag(id, input, db) {
|
|
3517
|
+
const d = db || getDatabase();
|
|
3518
|
+
const existing = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
|
|
3519
|
+
if (!existing)
|
|
3520
|
+
throw new TagNotFoundError(id);
|
|
3521
|
+
if (input.name && input.name !== existing.name) {
|
|
3522
|
+
const dupe = d.query(`SELECT id FROM tags WHERE name = ? AND id != ?`).get(input.name, id);
|
|
3523
|
+
if (dupe)
|
|
3524
|
+
throw new DuplicateTagNameError(input.name);
|
|
3525
|
+
}
|
|
3526
|
+
const setClauses = [];
|
|
3527
|
+
const params = [];
|
|
3528
|
+
if (input.name !== undefined) {
|
|
3529
|
+
setClauses.push("name = ?");
|
|
3530
|
+
params.push(input.name);
|
|
3531
|
+
}
|
|
3532
|
+
if (input.color !== undefined) {
|
|
3533
|
+
setClauses.push("color = ?");
|
|
3534
|
+
params.push(input.color);
|
|
3535
|
+
}
|
|
3536
|
+
if (input.description !== undefined) {
|
|
3537
|
+
setClauses.push("description = ?");
|
|
3538
|
+
params.push(input.description);
|
|
3539
|
+
}
|
|
3540
|
+
if (setClauses.length > 0) {
|
|
3541
|
+
params.push(id);
|
|
3542
|
+
d.run(`UPDATE tags SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
3543
|
+
}
|
|
3544
|
+
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
3545
|
+
}
|
|
3073
3546
|
function deleteTag(id, db) {
|
|
3074
3547
|
const d = db || getDatabase();
|
|
3075
3548
|
const row = d.query(`SELECT id FROM tags WHERE id = ?`).get(id);
|
|
@@ -3077,6 +3550,59 @@ function deleteTag(id, db) {
|
|
|
3077
3550
|
throw new TagNotFoundError(id);
|
|
3078
3551
|
d.run(`DELETE FROM tags WHERE id = ?`, [id]);
|
|
3079
3552
|
}
|
|
3553
|
+
function addTagToContact(contactId, tagId, db) {
|
|
3554
|
+
const d = db || getDatabase();
|
|
3555
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
3556
|
+
if (!contact)
|
|
3557
|
+
throw new ContactNotFoundError(contactId);
|
|
3558
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
3559
|
+
if (!tag)
|
|
3560
|
+
throw new TagNotFoundError(tagId);
|
|
3561
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [contactId, tagId]);
|
|
3562
|
+
}
|
|
3563
|
+
function removeTagFromContact(contactId, tagId, db) {
|
|
3564
|
+
const d = db || getDatabase();
|
|
3565
|
+
d.run(`DELETE FROM contact_tags WHERE contact_id = ? AND tag_id = ?`, [contactId, tagId]);
|
|
3566
|
+
}
|
|
3567
|
+
function listContactsByTag(tagId, db) {
|
|
3568
|
+
const d = db || getDatabase();
|
|
3569
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
3570
|
+
if (!tag)
|
|
3571
|
+
throw new TagNotFoundError(tagId);
|
|
3572
|
+
const rows = d.query(`
|
|
3573
|
+
SELECT c.* FROM contacts c
|
|
3574
|
+
JOIN contact_tags ct ON ct.contact_id = c.id
|
|
3575
|
+
WHERE ct.tag_id = ?
|
|
3576
|
+
ORDER BY c.display_name ASC
|
|
3577
|
+
`).all(tagId);
|
|
3578
|
+
return rows.map((r) => ({
|
|
3579
|
+
...r,
|
|
3580
|
+
source: r.source,
|
|
3581
|
+
custom_fields: JSON.parse(r.custom_fields || "{}"),
|
|
3582
|
+
preferred_contact_method: r.preferred_contact_method ?? null,
|
|
3583
|
+
status: r.status ?? "active",
|
|
3584
|
+
follow_up_at: r.follow_up_at ?? null,
|
|
3585
|
+
archived: !!r.archived,
|
|
3586
|
+
project_id: r.project_id ?? null,
|
|
3587
|
+
do_not_contact: !!r.do_not_contact,
|
|
3588
|
+
priority: r.priority ?? 3,
|
|
3589
|
+
timezone: r.timezone ?? null
|
|
3590
|
+
}));
|
|
3591
|
+
}
|
|
3592
|
+
function addTagToCompany(companyId, tagId, db) {
|
|
3593
|
+
const d = db || getDatabase();
|
|
3594
|
+
const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
|
|
3595
|
+
if (!company)
|
|
3596
|
+
throw new CompanyNotFoundError(companyId);
|
|
3597
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
3598
|
+
if (!tag)
|
|
3599
|
+
throw new TagNotFoundError(tagId);
|
|
3600
|
+
d.run(`INSERT OR IGNORE INTO company_tags (company_id, tag_id) VALUES (?, ?)`, [companyId, tagId]);
|
|
3601
|
+
}
|
|
3602
|
+
function removeTagFromCompany(companyId, tagId, db) {
|
|
3603
|
+
const d = db || getDatabase();
|
|
3604
|
+
d.run(`DELETE FROM company_tags WHERE company_id = ? AND tag_id = ?`, [companyId, tagId]);
|
|
3605
|
+
}
|
|
3080
3606
|
var init_tags = __esm(() => {
|
|
3081
3607
|
init_types();
|
|
3082
3608
|
init_database();
|
|
@@ -3359,9 +3885,65 @@ function importFromJson(data) {
|
|
|
3359
3885
|
};
|
|
3360
3886
|
});
|
|
3361
3887
|
}
|
|
3888
|
+
function parseLinkedInCsvLine(line) {
|
|
3889
|
+
return parseCsvLine(line);
|
|
3890
|
+
}
|
|
3891
|
+
function parseLinkedIn(csv) {
|
|
3892
|
+
const lines = csv.split(`
|
|
3893
|
+
`).filter((l) => l.trim());
|
|
3894
|
+
if (!lines.length)
|
|
3895
|
+
return [];
|
|
3896
|
+
const headers = parseLinkedInCsvLine(lines[0]).map((h) => h.replace(/"/g, "").trim());
|
|
3897
|
+
const firstNameIdx = headers.findIndex((h) => h === "First Name");
|
|
3898
|
+
const lastNameIdx = headers.findIndex((h) => h === "Last Name");
|
|
3899
|
+
const emailIdx = headers.findIndex((h) => h === "Email Address");
|
|
3900
|
+
const companyIdx = headers.findIndex((h) => h === "Company");
|
|
3901
|
+
const positionIdx = headers.findIndex((h) => h === "Position");
|
|
3902
|
+
const urlIdx = headers.findIndex((h) => h === "URL");
|
|
3903
|
+
const connectedIdx = headers.findIndex((h) => h === "Connected On");
|
|
3904
|
+
const results = [];
|
|
3905
|
+
for (let i = 1;i < lines.length; i++) {
|
|
3906
|
+
const cols = parseLinkedInCsvLine(lines[i]);
|
|
3907
|
+
const firstName = firstNameIdx >= 0 ? (cols[firstNameIdx] ?? "").trim() : "";
|
|
3908
|
+
const lastName = lastNameIdx >= 0 ? (cols[lastNameIdx] ?? "").trim() : "";
|
|
3909
|
+
if (!firstName && !lastName)
|
|
3910
|
+
continue;
|
|
3911
|
+
const contact = {
|
|
3912
|
+
first_name: firstName,
|
|
3913
|
+
last_name: lastName,
|
|
3914
|
+
display_name: `${firstName} ${lastName}`.trim(),
|
|
3915
|
+
source: "import"
|
|
3916
|
+
};
|
|
3917
|
+
if (emailIdx >= 0 && cols[emailIdx]?.trim()) {
|
|
3918
|
+
contact.emails = [{ address: cols[emailIdx].trim(), type: "work", is_primary: true }];
|
|
3919
|
+
}
|
|
3920
|
+
if (positionIdx >= 0 && cols[positionIdx]?.trim()) {
|
|
3921
|
+
contact.job_title = cols[positionIdx].trim();
|
|
3922
|
+
}
|
|
3923
|
+
if (urlIdx >= 0 && cols[urlIdx]?.trim()) {
|
|
3924
|
+
contact.social_profiles = [{ platform: "linkedin", url: cols[urlIdx].trim(), is_primary: true }];
|
|
3925
|
+
}
|
|
3926
|
+
if (companyIdx >= 0 && cols[companyIdx]?.trim()) {
|
|
3927
|
+
const connectedNote = connectedIdx >= 0 && cols[connectedIdx]?.trim() ? ` Connected on LinkedIn: ${cols[connectedIdx].trim()}` : "";
|
|
3928
|
+
contact.notes = `Company: ${cols[companyIdx].trim()}${connectedNote}`;
|
|
3929
|
+
} else if (connectedIdx >= 0 && cols[connectedIdx]?.trim()) {
|
|
3930
|
+
contact.notes = `Connected on LinkedIn: ${cols[connectedIdx].trim()}`;
|
|
3931
|
+
}
|
|
3932
|
+
results.push(contact);
|
|
3933
|
+
}
|
|
3934
|
+
return results;
|
|
3935
|
+
}
|
|
3936
|
+
function isLinkedInFormat(data) {
|
|
3937
|
+
const firstLine = data.split(`
|
|
3938
|
+
`)[0] ?? "";
|
|
3939
|
+
const lower = firstLine.toLowerCase();
|
|
3940
|
+
return lower.includes("first name") && lower.includes("url") && lower.includes("connected on");
|
|
3941
|
+
}
|
|
3362
3942
|
async function importContacts(format, data) {
|
|
3363
3943
|
switch (format) {
|
|
3364
3944
|
case "csv":
|
|
3945
|
+
if (isLinkedInFormat(data))
|
|
3946
|
+
return parseLinkedIn(data);
|
|
3365
3947
|
return importFromCsv(data);
|
|
3366
3948
|
case "vcf":
|
|
3367
3949
|
return parseVcf(data);
|
|
@@ -3808,66 +4390,365 @@ var init_serve = __esm(() => {
|
|
|
3808
4390
|
DASHBOARD_DIST = join3(import.meta.dir, "../../dashboard/dist");
|
|
3809
4391
|
});
|
|
3810
4392
|
|
|
3811
|
-
//
|
|
3812
|
-
var
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
4393
|
+
// src/db/notes.ts
|
|
4394
|
+
var exports_notes = {};
|
|
4395
|
+
__export(exports_notes, {
|
|
4396
|
+
listNotesForContactAtCompany: () => listNotesForContactAtCompany,
|
|
4397
|
+
listNotes: () => listNotes,
|
|
4398
|
+
getNote: () => getNote,
|
|
4399
|
+
deleteNote: () => deleteNote,
|
|
4400
|
+
addNote: () => addNote
|
|
4401
|
+
});
|
|
4402
|
+
function addNote(contactId, body, createdBy, db, companyId) {
|
|
4403
|
+
const d = db || getDatabase();
|
|
4404
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
4405
|
+
if (!contact)
|
|
4406
|
+
throw new ContactNotFoundError(contactId);
|
|
4407
|
+
const id = uuid();
|
|
4408
|
+
d.run(`INSERT INTO contact_notes (id, contact_id, body, created_by, company_id) VALUES (?, ?, ?, ?, ?)`, [id, contactId, body, createdBy ?? null, companyId ?? null]);
|
|
4409
|
+
return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(id);
|
|
4410
|
+
}
|
|
4411
|
+
function listNotes(contactId, db) {
|
|
4412
|
+
const d = db || getDatabase();
|
|
4413
|
+
return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
|
|
4414
|
+
}
|
|
4415
|
+
function listNotesForContactAtCompany(contactId, companyId, db) {
|
|
4416
|
+
const d = db || getDatabase();
|
|
4417
|
+
return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? AND company_id = ? ORDER BY created_at ASC`).all(contactId, companyId);
|
|
4418
|
+
}
|
|
4419
|
+
function deleteNote(noteId, db) {
|
|
4420
|
+
const d = db || getDatabase();
|
|
4421
|
+
d.run(`DELETE FROM contact_notes WHERE id = ?`, [noteId]);
|
|
4422
|
+
}
|
|
4423
|
+
function getNote(noteId, db) {
|
|
4424
|
+
const d = db || getDatabase();
|
|
4425
|
+
return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(noteId);
|
|
4426
|
+
}
|
|
4427
|
+
var init_notes = __esm(() => {
|
|
4428
|
+
init_types();
|
|
4429
|
+
init_database();
|
|
4430
|
+
});
|
|
3826
4431
|
|
|
3827
|
-
// src/
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
4432
|
+
// src/lib/timeline.ts
|
|
4433
|
+
var exports_timeline = {};
|
|
4434
|
+
__export(exports_timeline, {
|
|
4435
|
+
getContactTimeline: () => getContactTimeline
|
|
4436
|
+
});
|
|
4437
|
+
function getContactTimeline(contactId, limit = 50, db) {
|
|
4438
|
+
const _db2 = db || getDatabase();
|
|
4439
|
+
const items = [];
|
|
4440
|
+
const notes = _db2.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at DESC LIMIT 50`).all(contactId);
|
|
4441
|
+
for (const n of notes) {
|
|
4442
|
+
items.push({ date: n.created_at, type: "note", title: "Note", body: n.body });
|
|
4443
|
+
}
|
|
4444
|
+
const events = _db2.query(`SELECT * FROM events WHERE contact_ids LIKE ? ORDER BY event_date DESC LIMIT 50`).all(`%${contactId}%`);
|
|
4445
|
+
for (const e of events) {
|
|
4446
|
+
items.push({ date: e.event_date, type: "event", title: `${e.type}: ${e.title}`, body: e.notes ?? undefined, metadata: { outcome: e.outcome, duration_min: e.duration_min } });
|
|
4447
|
+
}
|
|
4448
|
+
const tasks = _db2.query(`SELECT * FROM contact_tasks WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
|
|
4449
|
+
for (const t of tasks) {
|
|
4450
|
+
items.push({ date: t.created_at, type: "task_created", title: `Task created: ${t.title}`, metadata: { deadline: t.deadline, priority: t.priority } });
|
|
4451
|
+
if (t.status === "completed") {
|
|
4452
|
+
items.push({ date: t.updated_at, type: "task_completed", title: `Task completed: ${t.title}` });
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
const comms = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.contact_id = ? ORDER BY vc.comm_date DESC LIMIT 20`).all(contactId);
|
|
4456
|
+
for (const c of comms) {
|
|
4457
|
+
items.push({ date: c.comm_date, type: "vendor_comm", title: `${c.type} \u2014 ${c.company_name}`, body: c.subject ?? undefined });
|
|
4458
|
+
}
|
|
4459
|
+
const activity = _db2.query(`SELECT * FROM activity_log WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
|
|
4460
|
+
for (const a of activity) {
|
|
4461
|
+
items.push({ date: a.created_at, type: "interaction", title: a.action, body: a.details ?? undefined });
|
|
4462
|
+
}
|
|
4463
|
+
return items.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
|
|
4464
|
+
}
|
|
4465
|
+
var init_timeline = __esm(() => {
|
|
4466
|
+
init_database();
|
|
4467
|
+
});
|
|
3831
4468
|
|
|
3832
|
-
// src/
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4469
|
+
// src/lib/brief.ts
|
|
4470
|
+
var exports_brief = {};
|
|
4471
|
+
__export(exports_brief, {
|
|
4472
|
+
generateBrief: () => generateBrief
|
|
4473
|
+
});
|
|
4474
|
+
function generateBrief(contactId, db) {
|
|
4475
|
+
const _db2 = db || getDatabase();
|
|
4476
|
+
const contact = getContact(contactId, _db2);
|
|
4477
|
+
const notes = listNotes(contactId, _db2);
|
|
4478
|
+
const allTasks = listContactTasks({ contact_id: contactId }, _db2);
|
|
4479
|
+
const tasks = allTasks.filter((t) => !["completed", "cancelled"].includes(t.status));
|
|
4480
|
+
const overdueTasks = allTasks.filter((t) => t.deadline && t.deadline < new Date().toISOString() && !["completed", "cancelled"].includes(t.status));
|
|
4481
|
+
const companyRels = listCompanyRelationships({ contact_id: contactId }, _db2);
|
|
4482
|
+
const recentTimeline = getContactTimeline(contactId, 5, _db2);
|
|
4483
|
+
const daysSince = contact.last_contacted_at ? Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000) : null;
|
|
4484
|
+
const lines = [];
|
|
4485
|
+
lines.push(`# ${contact.display_name}`);
|
|
4486
|
+
if (contact.job_title)
|
|
4487
|
+
lines.push(`**Role:** ${contact.job_title}${contact.company_id ? ` (linked to company)` : ""}`);
|
|
4488
|
+
if (contact.emails?.length) {
|
|
4489
|
+
const primary = contact.emails.find((e) => e.is_primary) || contact.emails[0];
|
|
4490
|
+
if (primary)
|
|
4491
|
+
lines.push(`**Email:** ${primary.address}`);
|
|
4492
|
+
}
|
|
4493
|
+
if (contact.phones?.length) {
|
|
4494
|
+
const primary = contact.phones.find((p) => p.is_primary) || contact.phones[0];
|
|
4495
|
+
if (primary)
|
|
4496
|
+
lines.push(`**Phone:** ${primary.number}`);
|
|
4497
|
+
}
|
|
4498
|
+
if (contact.preferred_contact_method)
|
|
4499
|
+
lines.push(`**Preferred contact:** ${contact.preferred_contact_method}`);
|
|
4500
|
+
lines.push("");
|
|
4501
|
+
lines.push(`## Status`);
|
|
4502
|
+
lines.push(`- Last contacted: ${daysSince !== null ? `${daysSince} days ago` : "never"}`);
|
|
4503
|
+
lines.push(`- Status: ${contact.status || "active"}`);
|
|
4504
|
+
if (contact.follow_up_at)
|
|
4505
|
+
lines.push(`- Follow-up scheduled: ${contact.follow_up_at}`);
|
|
4506
|
+
if (overdueTasks.length)
|
|
4507
|
+
lines.push(`- OVERDUE TASKS: ${overdueTasks.length}`);
|
|
4508
|
+
if (companyRels.length) {
|
|
4509
|
+
lines.push("");
|
|
4510
|
+
lines.push(`## Entity Relationships`);
|
|
4511
|
+
for (const r of companyRels)
|
|
4512
|
+
lines.push(`- ${r.relationship_type} \u2014 ${r.notes || ""}`);
|
|
4513
|
+
}
|
|
4514
|
+
if (tasks.length) {
|
|
4515
|
+
lines.push("");
|
|
4516
|
+
lines.push(`## Open Tasks`);
|
|
4517
|
+
for (const t of tasks)
|
|
4518
|
+
lines.push(`- [${t.priority}] ${t.title}${t.deadline ? ` (due ${t.deadline})` : ""}`);
|
|
4519
|
+
}
|
|
4520
|
+
if (notes.length) {
|
|
4521
|
+
lines.push("");
|
|
4522
|
+
lines.push(`## Recent Notes`);
|
|
4523
|
+
for (const n of notes.slice(0, 3))
|
|
4524
|
+
lines.push(`**${n.created_at.slice(0, 10)}:** ${n.body}`);
|
|
4525
|
+
}
|
|
4526
|
+
if (recentTimeline.length) {
|
|
4527
|
+
lines.push("");
|
|
4528
|
+
lines.push(`## Recent Activity`);
|
|
4529
|
+
for (const item of recentTimeline)
|
|
4530
|
+
lines.push(`- ${item.date.slice(0, 10)} ${item.title}`);
|
|
4531
|
+
}
|
|
4532
|
+
if (contact.notes) {
|
|
4533
|
+
lines.push("");
|
|
4534
|
+
lines.push(`## Background Notes`);
|
|
4535
|
+
lines.push(contact.notes);
|
|
4536
|
+
}
|
|
4537
|
+
return lines.join(`
|
|
4538
|
+
`);
|
|
4539
|
+
}
|
|
4540
|
+
var init_brief = __esm(() => {
|
|
4541
|
+
init_database();
|
|
4542
|
+
init_contacts();
|
|
4543
|
+
init_notes();
|
|
4544
|
+
init_contact_tasks();
|
|
4545
|
+
init_relationships();
|
|
4546
|
+
init_timeline();
|
|
4547
|
+
});
|
|
4548
|
+
|
|
4549
|
+
// src/lib/upcoming.ts
|
|
4550
|
+
var exports_upcoming = {};
|
|
4551
|
+
__export(exports_upcoming, {
|
|
4552
|
+
getUpcomingItems: () => getUpcomingItems
|
|
4553
|
+
});
|
|
4554
|
+
function getUpcomingItems(days = 7, db) {
|
|
4555
|
+
const _db2 = db || getDatabase();
|
|
4556
|
+
const items = [];
|
|
4557
|
+
const now3 = new Date;
|
|
4558
|
+
const future = new Date(now3.getTime() + days * 86400000);
|
|
4559
|
+
const todayStr = now3.toISOString().slice(0, 10);
|
|
4560
|
+
const futureStr = future.toISOString().slice(0, 10);
|
|
4561
|
+
const followUps = _db2.query(`SELECT c.id, c.display_name, c.follow_up_at FROM contacts c WHERE c.follow_up_at IS NOT NULL AND c.follow_up_at <= ? AND c.do_not_contact = 0`).all(futureStr);
|
|
4562
|
+
for (const r of followUps) {
|
|
4563
|
+
items.push({ date: r.follow_up_at, type: "follow_up", contact_id: r.id, contact_name: r.display_name, title: `Follow up with ${r.display_name}`, urgency: r.follow_up_at < todayStr ? "overdue" : r.follow_up_at === todayStr ? "today" : "upcoming" });
|
|
4564
|
+
}
|
|
4565
|
+
const tasks = _db2.query(`SELECT ct.*, c.display_name FROM contact_tasks ct JOIN contacts c ON ct.contact_id = c.id WHERE ct.deadline IS NOT NULL AND ct.deadline <= ? AND ct.status NOT IN ('completed','cancelled')`).all(futureStr);
|
|
4566
|
+
for (const t of tasks) {
|
|
4567
|
+
items.push({ date: t.deadline, type: "task_deadline", contact_id: t.contact_id, contact_name: t.display_name, title: t.title, urgency: t.deadline < todayStr ? "overdue" : t.deadline === todayStr ? "today" : "upcoming" });
|
|
4568
|
+
}
|
|
4569
|
+
const apps = _db2.query(`SELECT a.*, c.display_name as contact_name FROM applications a LEFT JOIN contacts c ON a.primary_contact_id = c.id WHERE a.follow_up_date IS NOT NULL AND a.follow_up_date <= ?`).all(futureStr);
|
|
4570
|
+
for (const a of apps) {
|
|
4571
|
+
items.push({ date: a.follow_up_date, type: "application_followup", contact_name: a.contact_name ?? undefined, title: `Follow up: ${a.program_name}`, urgency: a.follow_up_date < todayStr ? "overdue" : a.follow_up_date === todayStr ? "today" : "upcoming" });
|
|
4572
|
+
}
|
|
4573
|
+
const vendorFU = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.follow_up_date IS NOT NULL AND vc.follow_up_date <= ? AND vc.follow_up_done = 0`).all(futureStr);
|
|
4574
|
+
for (const v of vendorFU) {
|
|
4575
|
+
items.push({ date: v.follow_up_date, type: "vendor_followup", company_id: v.company_id, company_name: v.company_name, title: `Follow up with ${v.company_name}: ${v.subject || v.type}`, urgency: v.follow_up_date < todayStr ? "overdue" : v.follow_up_date === todayStr ? "today" : "upcoming" });
|
|
4576
|
+
}
|
|
4577
|
+
const contacts = _db2.query(`SELECT id, display_name, birthday FROM contacts WHERE birthday IS NOT NULL AND do_not_contact = 0`).all();
|
|
4578
|
+
for (const c of contacts) {
|
|
4579
|
+
const bday = new Date(c.birthday);
|
|
4580
|
+
const thisYear = new Date(now3.getFullYear(), bday.getMonth(), bday.getDate());
|
|
4581
|
+
const nextBday = thisYear >= now3 ? thisYear : new Date(now3.getFullYear() + 1, bday.getMonth(), bday.getDate());
|
|
4582
|
+
const nextStr = nextBday.toISOString().slice(0, 10);
|
|
4583
|
+
if (nextStr <= futureStr) {
|
|
4584
|
+
items.push({ date: nextStr, type: "birthday", contact_id: c.id, contact_name: c.display_name, title: `Birthday: ${c.display_name}`, urgency: nextStr === todayStr ? "today" : "upcoming" });
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
return items.sort((a, b) => a.date.localeCompare(b.date));
|
|
4588
|
+
}
|
|
4589
|
+
var init_upcoming = __esm(() => {
|
|
4590
|
+
init_database();
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4593
|
+
// src/lib/stats.ts
|
|
4594
|
+
var exports_stats = {};
|
|
4595
|
+
__export(exports_stats, {
|
|
4596
|
+
getNetworkStats: () => getNetworkStats
|
|
4597
|
+
});
|
|
4598
|
+
function getNetworkStats(db) {
|
|
4599
|
+
const _db2 = db || getDatabase();
|
|
4600
|
+
const q = (sql) => _db2.query(sql).get();
|
|
4601
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
4602
|
+
const d30 = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
|
|
4603
|
+
const d60 = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
|
|
3836
4604
|
return {
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
4605
|
+
total_contacts: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0`).c,
|
|
4606
|
+
total_companies: q(`SELECT COUNT(*) c FROM companies WHERE archived=0`).c,
|
|
4607
|
+
owned_entities: q(`SELECT COUNT(*) c FROM companies WHERE is_owned_entity=1`).c,
|
|
4608
|
+
total_tags: q(`SELECT COUNT(*) c FROM tags`).c,
|
|
4609
|
+
total_groups: q(`SELECT COUNT(*) c FROM groups`).c,
|
|
4610
|
+
total_deals: q(`SELECT COUNT(*) c FROM deals WHERE stage NOT IN ('won','lost','cancelled')`).c,
|
|
4611
|
+
total_events: q(`SELECT COUNT(*) c FROM events`).c,
|
|
4612
|
+
cold_30d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d30}')`).c,
|
|
4613
|
+
cold_60d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d60}')`).c,
|
|
4614
|
+
cold_never: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND last_contacted_at IS NULL`).c,
|
|
4615
|
+
contacts_with_email: q(`SELECT COUNT(DISTINCT contact_id) c FROM emails WHERE contact_id IS NOT NULL`).c,
|
|
4616
|
+
contacts_with_phone: q(`SELECT COUNT(DISTINCT contact_id) c FROM phones WHERE contact_id IS NOT NULL`).c,
|
|
4617
|
+
contacts_no_company: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND company_id IS NULL`).c,
|
|
4618
|
+
overdue_tasks: q(`SELECT COUNT(*) c FROM contact_tasks WHERE deadline < '${today}' AND status NOT IN ('completed','cancelled')`).c,
|
|
4619
|
+
pending_applications: q(`SELECT COUNT(*) c FROM applications WHERE status IN ('submitted','pending','follow_up_needed')`).c,
|
|
4620
|
+
missing_invoices: q(`SELECT COUNT(*) c FROM vendor_communications WHERE type='invoice_request' AND status IN ('awaiting_response','no_response')`).c,
|
|
4621
|
+
upcoming_7d: q(`SELECT COUNT(*) c FROM contacts WHERE follow_up_at BETWEEN '${today}' AND date('${today}','+7 days')`).c,
|
|
4622
|
+
notes_count: q(`SELECT COUNT(*) c FROM contact_notes`).c,
|
|
4623
|
+
active_deals_value: q(`SELECT COALESCE(SUM(value_usd),0) c FROM deals WHERE stage NOT IN ('won','lost','cancelled') AND currency='USD'`).c
|
|
3843
4624
|
};
|
|
3844
4625
|
}
|
|
3845
|
-
|
|
4626
|
+
var init_stats = __esm(() => {
|
|
4627
|
+
init_database();
|
|
4628
|
+
});
|
|
4629
|
+
|
|
4630
|
+
// src/lib/audit.ts
|
|
4631
|
+
var exports_audit = {};
|
|
4632
|
+
__export(exports_audit, {
|
|
4633
|
+
listContactAudit: () => listContactAudit,
|
|
4634
|
+
auditContact: () => auditContact
|
|
4635
|
+
});
|
|
4636
|
+
function auditContact(contact) {
|
|
4637
|
+
const missing = [];
|
|
4638
|
+
const suggestions = [];
|
|
4639
|
+
let score = 0;
|
|
4640
|
+
if (contact.emails?.length)
|
|
4641
|
+
score += 20;
|
|
4642
|
+
else {
|
|
4643
|
+
missing.push("email");
|
|
4644
|
+
suggestions.push("Add an email address");
|
|
4645
|
+
}
|
|
4646
|
+
if (contact.phones?.length)
|
|
4647
|
+
score += 15;
|
|
4648
|
+
else {
|
|
4649
|
+
missing.push("phone");
|
|
4650
|
+
suggestions.push("Add a phone number");
|
|
4651
|
+
}
|
|
4652
|
+
if (contact.company_id)
|
|
4653
|
+
score += 15;
|
|
4654
|
+
else {
|
|
4655
|
+
missing.push("company");
|
|
4656
|
+
suggestions.push("Link to a company");
|
|
4657
|
+
}
|
|
4658
|
+
if (contact.last_contacted_at)
|
|
4659
|
+
score += 20;
|
|
4660
|
+
else {
|
|
4661
|
+
missing.push("last_contacted_at");
|
|
4662
|
+
suggestions.push("Log a contact interaction");
|
|
4663
|
+
}
|
|
4664
|
+
if (contact.tags?.length)
|
|
4665
|
+
score += 10;
|
|
4666
|
+
else {
|
|
4667
|
+
missing.push("tags");
|
|
4668
|
+
suggestions.push("Add at least one tag");
|
|
4669
|
+
}
|
|
4670
|
+
if (contact.notes)
|
|
4671
|
+
score += 10;
|
|
4672
|
+
else {
|
|
4673
|
+
missing.push("notes");
|
|
4674
|
+
suggestions.push("Add notes");
|
|
4675
|
+
}
|
|
4676
|
+
if (contact.job_title)
|
|
4677
|
+
score += 10;
|
|
4678
|
+
else {
|
|
4679
|
+
missing.push("job_title");
|
|
4680
|
+
suggestions.push("Add a job title");
|
|
4681
|
+
}
|
|
4682
|
+
return { contact_id: contact.id, display_name: contact.display_name, score, missing, suggestions };
|
|
4683
|
+
}
|
|
4684
|
+
async function listContactAudit(db) {
|
|
4685
|
+
const _db2 = db || getDatabase();
|
|
4686
|
+
const { listContacts: listContacts2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4687
|
+
const { contacts } = listContacts2({ limit: 500, include_dnc: true }, _db2);
|
|
4688
|
+
return contacts.map(auditContact).sort((a, b) => a.score - b.score);
|
|
4689
|
+
}
|
|
4690
|
+
var init_audit = __esm(() => {
|
|
4691
|
+
init_database();
|
|
4692
|
+
});
|
|
4693
|
+
|
|
4694
|
+
// src/db/deals.ts
|
|
4695
|
+
var exports_deals = {};
|
|
4696
|
+
__export(exports_deals, {
|
|
4697
|
+
updateDeal: () => updateDeal,
|
|
4698
|
+
listDeals: () => listDeals,
|
|
4699
|
+
getDealsByStage: () => getDealsByStage,
|
|
4700
|
+
getDeal: () => getDeal,
|
|
4701
|
+
deleteDeal: () => deleteDeal,
|
|
4702
|
+
createDeal: () => createDeal
|
|
4703
|
+
});
|
|
4704
|
+
function rowToDeal(row) {
|
|
4705
|
+
return {
|
|
4706
|
+
id: row.id,
|
|
4707
|
+
title: row.title,
|
|
4708
|
+
contact_id: row.contact_id,
|
|
4709
|
+
company_id: row.company_id,
|
|
4710
|
+
stage: row.stage,
|
|
4711
|
+
value_usd: row.value_usd,
|
|
4712
|
+
currency: row.currency,
|
|
4713
|
+
close_date: row.close_date,
|
|
4714
|
+
notes: row.notes,
|
|
4715
|
+
created_at: row.created_at,
|
|
4716
|
+
updated_at: row.updated_at
|
|
4717
|
+
};
|
|
4718
|
+
}
|
|
4719
|
+
function createDeal(input, db) {
|
|
3846
4720
|
const d = db || getDatabase();
|
|
3847
|
-
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
|
|
3848
|
-
if (!contact)
|
|
3849
|
-
throw new ContactNotFoundError(input.contact_id);
|
|
3850
|
-
const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
|
|
3851
|
-
if (!company)
|
|
3852
|
-
throw new Error(`Company ${input.company_id} not found`);
|
|
3853
4721
|
const id = uuid();
|
|
3854
|
-
|
|
4722
|
+
const timestamp = now();
|
|
4723
|
+
d.run(`INSERT INTO deals (id, title, contact_id, company_id, stage, value_usd, currency, close_date, notes, created_at, updated_at)
|
|
4724
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3855
4725
|
id,
|
|
3856
|
-
input.
|
|
3857
|
-
input.
|
|
3858
|
-
input.
|
|
4726
|
+
input.title,
|
|
4727
|
+
input.contact_id ?? null,
|
|
4728
|
+
input.company_id ?? null,
|
|
4729
|
+
input.stage ?? "lead",
|
|
4730
|
+
input.value_usd ?? null,
|
|
4731
|
+
input.currency ?? "USD",
|
|
4732
|
+
input.close_date ?? null,
|
|
3859
4733
|
input.notes ?? null,
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
input.is_primary ? 1 : 0,
|
|
3863
|
-
input.status ?? "active"
|
|
4734
|
+
timestamp,
|
|
4735
|
+
timestamp
|
|
3864
4736
|
]);
|
|
3865
|
-
return
|
|
4737
|
+
return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
|
|
3866
4738
|
}
|
|
3867
|
-
function
|
|
4739
|
+
function getDeal(id, db) {
|
|
4740
|
+
const d = db || getDatabase();
|
|
4741
|
+
const row = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
|
|
4742
|
+
return row ? rowToDeal(row) : null;
|
|
4743
|
+
}
|
|
4744
|
+
function listDeals(opts = {}, db) {
|
|
3868
4745
|
const d = db || getDatabase();
|
|
3869
4746
|
const conditions = [];
|
|
3870
4747
|
const params = [];
|
|
4748
|
+
if (opts.stage) {
|
|
4749
|
+
conditions.push("stage = ?");
|
|
4750
|
+
params.push(opts.stage);
|
|
4751
|
+
}
|
|
3871
4752
|
if (opts.contact_id) {
|
|
3872
4753
|
conditions.push("contact_id = ?");
|
|
3873
4754
|
params.push(opts.contact_id);
|
|
@@ -3876,14 +4757,183 @@ function listCompanyRelationships(opts = {}, db) {
|
|
|
3876
4757
|
conditions.push("company_id = ?");
|
|
3877
4758
|
params.push(opts.company_id);
|
|
3878
4759
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4760
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4761
|
+
const rows = d.query(`SELECT * FROM deals ${where} ORDER BY created_at DESC`).all(...params);
|
|
4762
|
+
return rows.map(rowToDeal);
|
|
4763
|
+
}
|
|
4764
|
+
function updateDeal(id, input, db) {
|
|
4765
|
+
const d = db || getDatabase();
|
|
4766
|
+
const existing = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
|
|
4767
|
+
if (!existing)
|
|
4768
|
+
return null;
|
|
4769
|
+
const setClauses = ["updated_at = ?"];
|
|
4770
|
+
const params = [now()];
|
|
4771
|
+
if (input.title !== undefined) {
|
|
4772
|
+
setClauses.push("title = ?");
|
|
4773
|
+
params.push(input.title);
|
|
4774
|
+
}
|
|
4775
|
+
if (input.contact_id !== undefined) {
|
|
4776
|
+
setClauses.push("contact_id = ?");
|
|
4777
|
+
params.push(input.contact_id ?? null);
|
|
4778
|
+
}
|
|
4779
|
+
if (input.company_id !== undefined) {
|
|
4780
|
+
setClauses.push("company_id = ?");
|
|
4781
|
+
params.push(input.company_id ?? null);
|
|
4782
|
+
}
|
|
4783
|
+
if (input.stage !== undefined) {
|
|
4784
|
+
setClauses.push("stage = ?");
|
|
4785
|
+
params.push(input.stage);
|
|
4786
|
+
}
|
|
4787
|
+
if (input.value_usd !== undefined) {
|
|
4788
|
+
setClauses.push("value_usd = ?");
|
|
4789
|
+
params.push(input.value_usd ?? null);
|
|
4790
|
+
}
|
|
4791
|
+
if (input.currency !== undefined) {
|
|
4792
|
+
setClauses.push("currency = ?");
|
|
4793
|
+
params.push(input.currency);
|
|
4794
|
+
}
|
|
4795
|
+
if (input.close_date !== undefined) {
|
|
4796
|
+
setClauses.push("close_date = ?");
|
|
4797
|
+
params.push(input.close_date ?? null);
|
|
4798
|
+
}
|
|
4799
|
+
if (input.notes !== undefined) {
|
|
4800
|
+
setClauses.push("notes = ?");
|
|
4801
|
+
params.push(input.notes ?? null);
|
|
4802
|
+
}
|
|
4803
|
+
params.push(id);
|
|
4804
|
+
d.run(`UPDATE deals SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
4805
|
+
return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
|
|
4806
|
+
}
|
|
4807
|
+
function deleteDeal(id, db) {
|
|
4808
|
+
const d = db || getDatabase();
|
|
4809
|
+
d.run(`DELETE FROM deals WHERE id = ?`, [id]);
|
|
4810
|
+
}
|
|
4811
|
+
function getDealsByStage(db) {
|
|
4812
|
+
const d = db || getDatabase();
|
|
4813
|
+
const rows = d.query(`SELECT * FROM deals ORDER BY stage, created_at DESC`).all();
|
|
4814
|
+
const result = {};
|
|
4815
|
+
for (const row of rows) {
|
|
4816
|
+
if (!result[row.stage])
|
|
4817
|
+
result[row.stage] = [];
|
|
4818
|
+
result[row.stage].push(rowToDeal(row));
|
|
4819
|
+
}
|
|
4820
|
+
return result;
|
|
4821
|
+
}
|
|
4822
|
+
var init_deals = __esm(() => {
|
|
4823
|
+
init_database();
|
|
4824
|
+
});
|
|
4825
|
+
|
|
4826
|
+
// src/db/events.ts
|
|
4827
|
+
var exports_events = {};
|
|
4828
|
+
__export(exports_events, {
|
|
4829
|
+
logEvent: () => logEvent,
|
|
4830
|
+
listEvents: () => listEvents,
|
|
4831
|
+
getEvent: () => getEvent,
|
|
4832
|
+
deleteEvent: () => deleteEvent
|
|
4833
|
+
});
|
|
4834
|
+
function rowToEvent(row) {
|
|
4835
|
+
let contact_ids = [];
|
|
4836
|
+
try {
|
|
4837
|
+
contact_ids = JSON.parse(row.contact_ids);
|
|
4838
|
+
} catch {
|
|
4839
|
+
contact_ids = [];
|
|
4840
|
+
}
|
|
4841
|
+
return {
|
|
4842
|
+
id: row.id,
|
|
4843
|
+
title: row.title,
|
|
4844
|
+
type: row.type,
|
|
4845
|
+
event_date: row.event_date,
|
|
4846
|
+
duration_min: row.duration_min,
|
|
4847
|
+
contact_ids,
|
|
4848
|
+
company_id: row.company_id,
|
|
4849
|
+
notes: row.notes,
|
|
4850
|
+
outcome: row.outcome,
|
|
4851
|
+
deal_id: row.deal_id,
|
|
4852
|
+
created_at: row.created_at
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
function logEvent(input, db) {
|
|
4856
|
+
const d = db || getDatabase();
|
|
4857
|
+
const id = uuid();
|
|
4858
|
+
const timestamp = now();
|
|
4859
|
+
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
4860
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
4861
|
+
id,
|
|
4862
|
+
input.title,
|
|
4863
|
+
input.type ?? "meeting",
|
|
4864
|
+
input.event_date,
|
|
4865
|
+
input.duration_min ?? null,
|
|
4866
|
+
JSON.stringify(input.contact_ids ?? []),
|
|
4867
|
+
input.company_id ?? null,
|
|
4868
|
+
input.notes ?? null,
|
|
4869
|
+
input.outcome ?? null,
|
|
4870
|
+
input.deal_id ?? null,
|
|
4871
|
+
timestamp
|
|
4872
|
+
]);
|
|
4873
|
+
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
4874
|
+
}
|
|
4875
|
+
function getEvent(id, db) {
|
|
4876
|
+
const d = db || getDatabase();
|
|
4877
|
+
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
4878
|
+
return row ? rowToEvent(row) : null;
|
|
4879
|
+
}
|
|
4880
|
+
function listEvents(opts = {}, db) {
|
|
4881
|
+
const d = db || getDatabase();
|
|
4882
|
+
const conditions = [];
|
|
4883
|
+
const params = [];
|
|
4884
|
+
if (opts.contact_id) {
|
|
4885
|
+
conditions.push("contact_ids LIKE ?");
|
|
4886
|
+
params.push(`%${opts.contact_id}%`);
|
|
4887
|
+
}
|
|
4888
|
+
if (opts.company_id) {
|
|
4889
|
+
conditions.push("company_id = ?");
|
|
4890
|
+
params.push(opts.company_id);
|
|
4891
|
+
}
|
|
4892
|
+
if (opts.type) {
|
|
4893
|
+
conditions.push("type = ?");
|
|
4894
|
+
params.push(opts.type);
|
|
4895
|
+
}
|
|
4896
|
+
if (opts.date_from) {
|
|
4897
|
+
conditions.push("event_date >= ?");
|
|
4898
|
+
params.push(opts.date_from);
|
|
4899
|
+
}
|
|
4900
|
+
if (opts.date_to) {
|
|
4901
|
+
conditions.push("event_date <= ?");
|
|
4902
|
+
params.push(opts.date_to);
|
|
3882
4903
|
}
|
|
3883
4904
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3884
|
-
const rows = d.query(`SELECT * FROM
|
|
3885
|
-
return rows.map(
|
|
4905
|
+
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
4906
|
+
return rows.map(rowToEvent);
|
|
3886
4907
|
}
|
|
4908
|
+
function deleteEvent(id, db) {
|
|
4909
|
+
const d = db || getDatabase();
|
|
4910
|
+
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
4911
|
+
}
|
|
4912
|
+
var init_events = __esm(() => {
|
|
4913
|
+
init_database();
|
|
4914
|
+
});
|
|
4915
|
+
|
|
4916
|
+
// node_modules/commander/esm.mjs
|
|
4917
|
+
var import__ = __toESM(require_commander(), 1);
|
|
4918
|
+
var {
|
|
4919
|
+
program,
|
|
4920
|
+
createCommand,
|
|
4921
|
+
createArgument,
|
|
4922
|
+
createOption,
|
|
4923
|
+
CommanderError,
|
|
4924
|
+
InvalidArgumentError,
|
|
4925
|
+
InvalidOptionArgumentError,
|
|
4926
|
+
Command,
|
|
4927
|
+
Argument,
|
|
4928
|
+
Option,
|
|
4929
|
+
Help
|
|
4930
|
+
} = import__.default;
|
|
4931
|
+
|
|
4932
|
+
// src/cli/index.tsx
|
|
4933
|
+
init_contacts();
|
|
4934
|
+
init_companies();
|
|
4935
|
+
init_relationships();
|
|
4936
|
+
import chalk from "chalk";
|
|
3887
4937
|
|
|
3888
4938
|
// src/db/vendor-comms.ts
|
|
3889
4939
|
init_database();
|
|
@@ -3926,166 +4976,49 @@ function logVendorCommunication(input, db) {
|
|
|
3926
4976
|
input.invoice_currency ?? null,
|
|
3927
4977
|
input.invoice_ref ?? null,
|
|
3928
4978
|
input.follow_up_date ?? null,
|
|
3929
|
-
input.follow_up_done ? 1 : 0
|
|
3930
|
-
]);
|
|
3931
|
-
return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
|
|
3932
|
-
}
|
|
3933
|
-
function listVendorCommunications(companyId, opts = {}, db) {
|
|
3934
|
-
const d = db || getDatabase();
|
|
3935
|
-
const conditions = ["company_id = ?"];
|
|
3936
|
-
const params = [companyId];
|
|
3937
|
-
if (opts.type) {
|
|
3938
|
-
conditions.push("type = ?");
|
|
3939
|
-
params.push(opts.type);
|
|
3940
|
-
}
|
|
3941
|
-
if (opts.status) {
|
|
3942
|
-
conditions.push("status = ?");
|
|
3943
|
-
params.push(opts.status);
|
|
3944
|
-
}
|
|
3945
|
-
if (opts.direction) {
|
|
3946
|
-
conditions.push("direction = ?");
|
|
3947
|
-
params.push(opts.direction);
|
|
3948
|
-
}
|
|
3949
|
-
const where = conditions.join(" AND ");
|
|
3950
|
-
const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
|
|
3951
|
-
return rows.map(rowToVendorComm);
|
|
3952
|
-
}
|
|
3953
|
-
function listPendingFollowUps(db) {
|
|
3954
|
-
const d = db || getDatabase();
|
|
3955
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
3956
|
-
const rows = d.query(`SELECT * FROM vendor_communications
|
|
3957
|
-
WHERE follow_up_date <= ? AND follow_up_done = 0
|
|
3958
|
-
ORDER BY follow_up_date ASC`).all(today);
|
|
3959
|
-
return rows.map(rowToVendorComm);
|
|
3960
|
-
}
|
|
3961
|
-
function listMissingInvoices(db) {
|
|
3962
|
-
const d = db || getDatabase();
|
|
3963
|
-
const rows = d.query(`SELECT * FROM vendor_communications
|
|
3964
|
-
WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
|
|
3965
|
-
ORDER BY comm_date ASC`).all();
|
|
3966
|
-
return rows.map(rowToVendorComm);
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
// src/db/contact-tasks.ts
|
|
3970
|
-
init_database();
|
|
3971
|
-
function rowToContactTask(row) {
|
|
3972
|
-
return {
|
|
3973
|
-
id: row.id,
|
|
3974
|
-
title: row.title,
|
|
3975
|
-
description: row.description,
|
|
3976
|
-
contact_id: row.contact_id,
|
|
3977
|
-
assigned_by: row.assigned_by,
|
|
3978
|
-
deadline: row.deadline,
|
|
3979
|
-
status: row.status,
|
|
3980
|
-
priority: row.priority,
|
|
3981
|
-
entity_id: row.entity_id,
|
|
3982
|
-
linked_todos_task_id: row.linked_todos_task_id,
|
|
3983
|
-
escalation_rules: JSON.parse(row.escalation_rules || "[]"),
|
|
3984
|
-
created_at: row.created_at,
|
|
3985
|
-
updated_at: row.updated_at
|
|
3986
|
-
};
|
|
3987
|
-
}
|
|
3988
|
-
function createContactTask(input, db) {
|
|
3989
|
-
const d = db || getDatabase();
|
|
3990
|
-
const id = uuid();
|
|
3991
|
-
const timestamp = now();
|
|
3992
|
-
d.run(`INSERT INTO contact_tasks
|
|
3993
|
-
(id, title, description, contact_id, assigned_by, deadline, status, priority,
|
|
3994
|
-
entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
|
|
3995
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3996
|
-
id,
|
|
3997
|
-
input.title,
|
|
3998
|
-
input.description ?? null,
|
|
3999
|
-
input.contact_id,
|
|
4000
|
-
input.assigned_by ?? null,
|
|
4001
|
-
input.deadline ?? null,
|
|
4002
|
-
input.status ?? "pending",
|
|
4003
|
-
input.priority ?? "medium",
|
|
4004
|
-
input.entity_id ?? null,
|
|
4005
|
-
input.linked_todos_task_id ?? null,
|
|
4006
|
-
JSON.stringify(input.escalation_rules ?? []),
|
|
4007
|
-
timestamp,
|
|
4008
|
-
timestamp
|
|
4009
|
-
]);
|
|
4010
|
-
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
4011
|
-
}
|
|
4012
|
-
function listContactTasks(opts = {}, db) {
|
|
4013
|
-
const d = db || getDatabase();
|
|
4014
|
-
const conditions = [];
|
|
4015
|
-
const params = [];
|
|
4016
|
-
if (opts.contact_id) {
|
|
4017
|
-
conditions.push("contact_id = ?");
|
|
4018
|
-
params.push(opts.contact_id);
|
|
4019
|
-
}
|
|
4020
|
-
if (opts.entity_id) {
|
|
4021
|
-
conditions.push("entity_id = ?");
|
|
4022
|
-
params.push(opts.entity_id);
|
|
4979
|
+
input.follow_up_done ? 1 : 0
|
|
4980
|
+
]);
|
|
4981
|
+
return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
|
|
4982
|
+
}
|
|
4983
|
+
function listVendorCommunications(companyId, opts = {}, db) {
|
|
4984
|
+
const d = db || getDatabase();
|
|
4985
|
+
const conditions = ["company_id = ?"];
|
|
4986
|
+
const params = [companyId];
|
|
4987
|
+
if (opts.type) {
|
|
4988
|
+
conditions.push("type = ?");
|
|
4989
|
+
params.push(opts.type);
|
|
4023
4990
|
}
|
|
4024
4991
|
if (opts.status) {
|
|
4025
4992
|
conditions.push("status = ?");
|
|
4026
4993
|
params.push(opts.status);
|
|
4027
4994
|
}
|
|
4028
|
-
if (opts.
|
|
4029
|
-
conditions.push("
|
|
4030
|
-
params.push(opts.
|
|
4995
|
+
if (opts.direction) {
|
|
4996
|
+
conditions.push("direction = ?");
|
|
4997
|
+
params.push(opts.direction);
|
|
4031
4998
|
}
|
|
4032
|
-
const where = conditions.
|
|
4033
|
-
const rows = d.query(`SELECT * FROM
|
|
4034
|
-
return rows.map(
|
|
4999
|
+
const where = conditions.join(" AND ");
|
|
5000
|
+
const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
|
|
5001
|
+
return rows.map(rowToVendorComm);
|
|
4035
5002
|
}
|
|
4036
|
-
function
|
|
5003
|
+
function listPendingFollowUps(db) {
|
|
4037
5004
|
const d = db || getDatabase();
|
|
4038
|
-
const
|
|
4039
|
-
const
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
}
|
|
4044
|
-
if ("description" in input) {
|
|
4045
|
-
setClauses.push("description = ?");
|
|
4046
|
-
params.push(input.description ?? null);
|
|
4047
|
-
}
|
|
4048
|
-
if ("assigned_by" in input) {
|
|
4049
|
-
setClauses.push("assigned_by = ?");
|
|
4050
|
-
params.push(input.assigned_by ?? null);
|
|
4051
|
-
}
|
|
4052
|
-
if ("deadline" in input) {
|
|
4053
|
-
setClauses.push("deadline = ?");
|
|
4054
|
-
params.push(input.deadline ?? null);
|
|
4055
|
-
}
|
|
4056
|
-
if (input.status !== undefined) {
|
|
4057
|
-
setClauses.push("status = ?");
|
|
4058
|
-
params.push(input.status);
|
|
4059
|
-
}
|
|
4060
|
-
if (input.priority !== undefined) {
|
|
4061
|
-
setClauses.push("priority = ?");
|
|
4062
|
-
params.push(input.priority);
|
|
4063
|
-
}
|
|
4064
|
-
if ("entity_id" in input) {
|
|
4065
|
-
setClauses.push("entity_id = ?");
|
|
4066
|
-
params.push(input.entity_id ?? null);
|
|
4067
|
-
}
|
|
4068
|
-
if ("linked_todos_task_id" in input) {
|
|
4069
|
-
setClauses.push("linked_todos_task_id = ?");
|
|
4070
|
-
params.push(input.linked_todos_task_id ?? null);
|
|
4071
|
-
}
|
|
4072
|
-
if (input.escalation_rules !== undefined) {
|
|
4073
|
-
setClauses.push("escalation_rules = ?");
|
|
4074
|
-
params.push(JSON.stringify(input.escalation_rules));
|
|
4075
|
-
}
|
|
4076
|
-
params.push(id);
|
|
4077
|
-
d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
4078
|
-
return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
|
|
5005
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
5006
|
+
const rows = d.query(`SELECT * FROM vendor_communications
|
|
5007
|
+
WHERE follow_up_date <= ? AND follow_up_done = 0
|
|
5008
|
+
ORDER BY follow_up_date ASC`).all(today);
|
|
5009
|
+
return rows.map(rowToVendorComm);
|
|
4079
5010
|
}
|
|
4080
|
-
function
|
|
5011
|
+
function listMissingInvoices(db) {
|
|
4081
5012
|
const d = db || getDatabase();
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
return rows.map(rowToContactTask);
|
|
5013
|
+
const rows = d.query(`SELECT * FROM vendor_communications
|
|
5014
|
+
WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
|
|
5015
|
+
ORDER BY comm_date ASC`).all();
|
|
5016
|
+
return rows.map(rowToVendorComm);
|
|
4087
5017
|
}
|
|
4088
5018
|
|
|
5019
|
+
// src/cli/index.tsx
|
|
5020
|
+
init_contact_tasks();
|
|
5021
|
+
|
|
4089
5022
|
// src/db/applications.ts
|
|
4090
5023
|
init_database();
|
|
4091
5024
|
function rowToApplication(row) {
|
|
@@ -5532,4 +6465,433 @@ program.command("seed").description("Seed demo contacts, companies, and relation
|
|
|
5532
6465
|
console.log(chalk.gray(" Try: contacts entities list"));
|
|
5533
6466
|
console.log(chalk.gray(" Try: contacts workload " + alina.id));
|
|
5534
6467
|
});
|
|
6468
|
+
program.command("brief <id>").description("Generate pre-meeting briefing for a contact").action(async (id) => {
|
|
6469
|
+
const { generateBrief: generateBrief2 } = await Promise.resolve().then(() => (init_brief(), exports_brief));
|
|
6470
|
+
const db = getDatabase();
|
|
6471
|
+
const brief = generateBrief2(id, db);
|
|
6472
|
+
console.log(brief);
|
|
6473
|
+
});
|
|
6474
|
+
program.command("cold").description("Show contacts you haven't reached out to recently").option("--days <n>", "Days threshold", "30").action(async (opts) => {
|
|
6475
|
+
const { listColdContacts: listColdContacts2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
6476
|
+
const db = getDatabase();
|
|
6477
|
+
const contacts = listColdContacts2(parseInt(opts.days, 10), db);
|
|
6478
|
+
if (!contacts.length) {
|
|
6479
|
+
console.log(chalk.green(`
|
|
6480
|
+
No cold contacts!
|
|
6481
|
+
`));
|
|
6482
|
+
return;
|
|
6483
|
+
}
|
|
6484
|
+
console.log();
|
|
6485
|
+
const rows = contacts.map((c) => {
|
|
6486
|
+
const daysCold = c.days_cold ?? null;
|
|
6487
|
+
const lastContact = c.last_contacted_at ? c.last_contacted_at.slice(0, 10) : "never";
|
|
6488
|
+
const dayStr = daysCold === null ? chalk.red("never") : daysCold > 60 ? chalk.red(String(daysCold) + "d") : chalk.yellow(String(daysCold) + "d");
|
|
6489
|
+
return {
|
|
6490
|
+
Name: c.display_name,
|
|
6491
|
+
Company: c.company?.name ?? "",
|
|
6492
|
+
"Last Contact": lastContact,
|
|
6493
|
+
"Days Cold": dayStr
|
|
6494
|
+
};
|
|
6495
|
+
});
|
|
6496
|
+
renderTable(["Name", "Company", "Last Contact", "Days Cold"], rows);
|
|
6497
|
+
console.log(chalk.gray(`
|
|
6498
|
+
${contacts.length} cold contact(s) (${opts.days}+ days)
|
|
6499
|
+
`));
|
|
6500
|
+
});
|
|
6501
|
+
program.command("upcoming").option("--days <n>", "Days ahead to show", "7").description("Show upcoming follow-ups, birthdays, and deadlines").action(async (opts) => {
|
|
6502
|
+
const { getUpcomingItems: getUpcomingItems2 } = await Promise.resolve().then(() => (init_upcoming(), exports_upcoming));
|
|
6503
|
+
const db = getDatabase();
|
|
6504
|
+
const items = getUpcomingItems2(parseInt(opts.days, 10), db);
|
|
6505
|
+
if (!items.length) {
|
|
6506
|
+
console.log(chalk.green(`
|
|
6507
|
+
Nothing upcoming!
|
|
6508
|
+
`));
|
|
6509
|
+
return;
|
|
6510
|
+
}
|
|
6511
|
+
console.log();
|
|
6512
|
+
const iconMap = {
|
|
6513
|
+
follow_up: "\uD83D\uDCC5",
|
|
6514
|
+
birthday: "\uD83C\uDF82",
|
|
6515
|
+
task_deadline: "\u26A0\uFE0F",
|
|
6516
|
+
application_followup: "\uD83D\uDCCB",
|
|
6517
|
+
vendor_followup: "\uD83D\uDCBC"
|
|
6518
|
+
};
|
|
6519
|
+
for (const item of items) {
|
|
6520
|
+
const icon = iconMap[item.type] ?? "\u2022";
|
|
6521
|
+
const urgencyColor = item.urgency === "overdue" ? chalk.red : item.urgency === "today" ? chalk.yellow : chalk.white;
|
|
6522
|
+
console.log(` ${icon} ${urgencyColor(item.date?.slice(0, 10) ?? "")} ${chalk.bold(item.title ?? "")} ${chalk.gray(item.type ?? "")}`);
|
|
6523
|
+
}
|
|
6524
|
+
console.log(chalk.gray(`
|
|
6525
|
+
${items.length} upcoming item(s) in next ${opts.days} days
|
|
6526
|
+
`));
|
|
6527
|
+
});
|
|
6528
|
+
program.command("stats").description("Network health dashboard").action(async () => {
|
|
6529
|
+
const { getNetworkStats: getNetworkStats2 } = await Promise.resolve().then(() => (init_stats(), exports_stats));
|
|
6530
|
+
const db = getDatabase();
|
|
6531
|
+
const stats = getNetworkStats2(db);
|
|
6532
|
+
console.log(chalk.bold.blue(`
|
|
6533
|
+
\u2501\u2501\u2501 Network Health Dashboard \u2501\u2501\u2501
|
|
6534
|
+
`));
|
|
6535
|
+
const size = stats["network_size"];
|
|
6536
|
+
if (size) {
|
|
6537
|
+
console.log(chalk.bold(" Network Size:"));
|
|
6538
|
+
console.log(` ${chalk.cyan(String(size["contacts"] ?? 0))} contacts ${chalk.cyan(String(size["companies"] ?? 0))} companies ${chalk.cyan(String(size["tags"] ?? 0))} tags`);
|
|
6539
|
+
}
|
|
6540
|
+
const cold = stats["cold_contacts"];
|
|
6541
|
+
if (cold) {
|
|
6542
|
+
console.log(chalk.bold(`
|
|
6543
|
+
Cold Contacts:`));
|
|
6544
|
+
const c30 = cold["30d"] ?? 0;
|
|
6545
|
+
const c60 = cold["60d"] ?? 0;
|
|
6546
|
+
const cnever = cold["never"] ?? 0;
|
|
6547
|
+
console.log(` ${c30 > 5 ? chalk.red(String(c30)) : c30 > 0 ? chalk.yellow(String(c30)) : chalk.green(String(c30))} not contacted in 30d`);
|
|
6548
|
+
console.log(` ${c60 > 5 ? chalk.red(String(c60)) : c60 > 0 ? chalk.yellow(String(c60)) : chalk.green(String(c60))} not contacted in 60d`);
|
|
6549
|
+
console.log(` ${cnever > 5 ? chalk.red(String(cnever)) : cnever > 0 ? chalk.yellow(String(cnever)) : chalk.green(String(cnever))} never contacted`);
|
|
6550
|
+
}
|
|
6551
|
+
const action = stats["action_required"];
|
|
6552
|
+
if (action) {
|
|
6553
|
+
console.log(chalk.bold(`
|
|
6554
|
+
Action Required:`));
|
|
6555
|
+
const overdue = action["overdue_tasks"] ?? 0;
|
|
6556
|
+
const pending = action["pending_applications"] ?? 0;
|
|
6557
|
+
const missing = action["missing_invoices"] ?? 0;
|
|
6558
|
+
const upcoming = action["upcoming_7d"] ?? 0;
|
|
6559
|
+
console.log(` ${overdue > 0 ? chalk.red(String(overdue)) : chalk.green("0")} overdue tasks`);
|
|
6560
|
+
console.log(` ${pending > 0 ? chalk.yellow(String(pending)) : chalk.green("0")} pending applications`);
|
|
6561
|
+
console.log(` ${missing > 0 ? chalk.yellow(String(missing)) : chalk.green("0")} missing invoices`);
|
|
6562
|
+
console.log(` ${upcoming > 0 ? chalk.yellow(String(upcoming)) : chalk.green("0")} upcoming in 7d`);
|
|
6563
|
+
}
|
|
6564
|
+
const pipeline = stats["pipeline_value_usd"];
|
|
6565
|
+
if (pipeline !== undefined) {
|
|
6566
|
+
console.log(chalk.bold(`
|
|
6567
|
+
Deal Pipeline:`));
|
|
6568
|
+
console.log(` ${chalk.cyan("$" + pipeline.toLocaleString())} active pipeline`);
|
|
6569
|
+
}
|
|
6570
|
+
console.log();
|
|
6571
|
+
});
|
|
6572
|
+
program.command("audit").description("Score contacts for data completeness").option("--limit <n>", "Number to show", "20").action(async (opts) => {
|
|
6573
|
+
const { listContactAudit: listContactAudit2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
|
|
6574
|
+
const db = getDatabase();
|
|
6575
|
+
const results = (await listContactAudit2(db)).slice(0, parseInt(opts.limit, 10));
|
|
6576
|
+
if (!results.length) {
|
|
6577
|
+
console.log(chalk.gray(`
|
|
6578
|
+
No contacts found.
|
|
6579
|
+
`));
|
|
6580
|
+
return;
|
|
6581
|
+
}
|
|
6582
|
+
console.log();
|
|
6583
|
+
for (const r of results) {
|
|
6584
|
+
const score = r.score;
|
|
6585
|
+
const name = r.display_name;
|
|
6586
|
+
const missing = r.missing_fields ?? [];
|
|
6587
|
+
const filled = Math.round(score / 10);
|
|
6588
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(10 - filled));
|
|
6589
|
+
const scoreColor = score < 40 ? chalk.red : score < 70 ? chalk.yellow : chalk.green;
|
|
6590
|
+
console.log(` ${bar} ${scoreColor(String(score).padStart(3) + "%")} ${chalk.bold(name)} ${chalk.gray(missing.join(", "))}`);
|
|
6591
|
+
}
|
|
6592
|
+
console.log(chalk.gray(`
|
|
6593
|
+
${results.length} contact(s) shown (sorted by completeness ascending)
|
|
6594
|
+
`));
|
|
6595
|
+
});
|
|
6596
|
+
var dealsCmd = program.command("deals").description("Manage deals and opportunities");
|
|
6597
|
+
dealsCmd.command("list").description("List deals").option("--stage <s>", "Filter by stage").action(async (opts) => {
|
|
6598
|
+
const { listDeals: listDeals2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
|
|
6599
|
+
const db = getDatabase();
|
|
6600
|
+
const deals = listDeals2({ stage: opts.stage }, db);
|
|
6601
|
+
if (!deals.length) {
|
|
6602
|
+
console.log(chalk.gray(`
|
|
6603
|
+
No deals found.
|
|
6604
|
+
`));
|
|
6605
|
+
return;
|
|
6606
|
+
}
|
|
6607
|
+
console.log();
|
|
6608
|
+
renderTable(["Title", "Stage", "Value", "Close Date", "Contact"], deals.map((d) => ({
|
|
6609
|
+
Title: d.title,
|
|
6610
|
+
Stage: d.stage,
|
|
6611
|
+
Value: d.value_usd ? "$" + d.value_usd.toLocaleString() : "",
|
|
6612
|
+
"Close Date": d.close_date ? d.close_date.slice(0, 10) : "",
|
|
6613
|
+
Contact: d.contact_id ?? ""
|
|
6614
|
+
})));
|
|
6615
|
+
console.log(chalk.gray(`
|
|
6616
|
+
${deals.length} deal(s)
|
|
6617
|
+
`));
|
|
6618
|
+
});
|
|
6619
|
+
dealsCmd.command("add").description("Add a new deal").option("--title <title>", "Deal title (required)").option("--stage <stage>", "Stage: prospecting|qualified|proposal|negotiation|won|lost", "prospecting").option("--value <usd>", "Value in USD").option("--contact <id>", "Contact ID").option("--company <id>", "Company ID").option("--close-date <date>", "Expected close date (YYYY-MM-DD)").option("--notes <text>", "Notes").action(async (opts) => {
|
|
6620
|
+
const { createDeal: createDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
|
|
6621
|
+
const db = getDatabase();
|
|
6622
|
+
let title = opts.title;
|
|
6623
|
+
if (!title) {
|
|
6624
|
+
title = await prompt("Deal title (required):");
|
|
6625
|
+
if (!title) {
|
|
6626
|
+
console.error(chalk.red("Title is required."));
|
|
6627
|
+
process.exit(1);
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
const deal = createDeal2({
|
|
6631
|
+
title,
|
|
6632
|
+
stage: opts.stage,
|
|
6633
|
+
value_usd: opts.value ? parseFloat(opts.value) : undefined,
|
|
6634
|
+
contact_id: opts.contact,
|
|
6635
|
+
company_id: opts.company,
|
|
6636
|
+
close_date: opts.closeDate,
|
|
6637
|
+
notes: opts.notes
|
|
6638
|
+
}, db);
|
|
6639
|
+
console.log(chalk.green(`
|
|
6640
|
+
\u2713 Deal created: ${deal.title} (${deal.id})
|
|
6641
|
+
`));
|
|
6642
|
+
});
|
|
6643
|
+
dealsCmd.command("show <id>").description("Show deal details").action(async (id) => {
|
|
6644
|
+
const { getDeal: getDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
|
|
6645
|
+
const db = getDatabase();
|
|
6646
|
+
const deal = getDeal2(id, db);
|
|
6647
|
+
console.log();
|
|
6648
|
+
for (const [k, v] of Object.entries(deal)) {
|
|
6649
|
+
if (v !== null && v !== undefined)
|
|
6650
|
+
console.log(` ${chalk.gray(k.padEnd(15))} ${v}`);
|
|
6651
|
+
}
|
|
6652
|
+
console.log();
|
|
6653
|
+
});
|
|
6654
|
+
dealsCmd.command("won <id>").description("Mark a deal as won").action(async (id) => {
|
|
6655
|
+
const { updateDeal: updateDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
|
|
6656
|
+
const db = getDatabase();
|
|
6657
|
+
const deal = updateDeal2(id, { stage: "won" }, db);
|
|
6658
|
+
console.log(chalk.green(`
|
|
6659
|
+
\u2713 Deal won: ${deal.title}
|
|
6660
|
+
`));
|
|
6661
|
+
});
|
|
6662
|
+
dealsCmd.command("lost <id>").description("Mark a deal as lost").action(async (id) => {
|
|
6663
|
+
const { updateDeal: updateDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
|
|
6664
|
+
const db = getDatabase();
|
|
6665
|
+
const deal = updateDeal2(id, { stage: "lost" }, db);
|
|
6666
|
+
console.log(chalk.yellow(`
|
|
6667
|
+
Deal lost: ${deal.title}
|
|
6668
|
+
`));
|
|
6669
|
+
});
|
|
6670
|
+
var eventsCmd = program.command("events").description("Log meetings and interactions");
|
|
6671
|
+
eventsCmd.command("log").description("Log an event/meeting").option("--title <title>", "Event title (required)").option("--type <type>", "Type: meeting|call|email|lunch|conference|demo|other", "meeting").option("--date <date>", "Date (YYYY-MM-DD, default today)").option("--contact <id>", "Contact ID (can repeat)", collect, []).option("--duration <min>", "Duration in minutes").option("--notes <text>", "Notes").option("--outcome <text>", "Outcome").action(async (opts) => {
|
|
6672
|
+
const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
6673
|
+
const db = getDatabase();
|
|
6674
|
+
let title = opts.title;
|
|
6675
|
+
if (!title) {
|
|
6676
|
+
title = await prompt("Event title (required):");
|
|
6677
|
+
if (!title) {
|
|
6678
|
+
console.error(chalk.red("Title is required."));
|
|
6679
|
+
process.exit(1);
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
const eventDate = opts.date ?? new Date().toISOString().slice(0, 10);
|
|
6683
|
+
const event = logEvent2({
|
|
6684
|
+
title,
|
|
6685
|
+
type: opts.type,
|
|
6686
|
+
event_date: eventDate,
|
|
6687
|
+
duration_min: opts.duration ? parseInt(opts.duration, 10) : undefined,
|
|
6688
|
+
contact_ids: opts.contact.length ? opts.contact : undefined,
|
|
6689
|
+
notes: opts.notes,
|
|
6690
|
+
outcome: opts.outcome
|
|
6691
|
+
}, db);
|
|
6692
|
+
console.log(chalk.green(`
|
|
6693
|
+
\u2713 Event logged: ${event.title} on ${eventDate}
|
|
6694
|
+
`));
|
|
6695
|
+
});
|
|
6696
|
+
eventsCmd.command("list [contact-id]").description("List events, optionally for a specific contact").action(async (contactId) => {
|
|
6697
|
+
const { listEvents: listEvents2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
6698
|
+
const db = getDatabase();
|
|
6699
|
+
const events = listEvents2({ contact_id: contactId }, db);
|
|
6700
|
+
if (!events.length) {
|
|
6701
|
+
console.log(chalk.gray(`
|
|
6702
|
+
No events found.
|
|
6703
|
+
`));
|
|
6704
|
+
return;
|
|
6705
|
+
}
|
|
6706
|
+
console.log();
|
|
6707
|
+
renderTable(["Title", "Type", "Date", "Duration"], events.map((e) => ({
|
|
6708
|
+
Title: e.title,
|
|
6709
|
+
Type: e.type,
|
|
6710
|
+
Date: e.event_date.slice(0, 10),
|
|
6711
|
+
Duration: e.duration_min ? `${e.duration_min}m` : ""
|
|
6712
|
+
})));
|
|
6713
|
+
console.log(chalk.gray(`
|
|
6714
|
+
${events.length} event(s)
|
|
6715
|
+
`));
|
|
6716
|
+
});
|
|
6717
|
+
program.command("timeline <id>").description("Full chronological activity history for a contact").option("--limit <n>", "Items to show", "20").action(async (id, opts) => {
|
|
6718
|
+
const { getContactTimeline: getContactTimeline2 } = await Promise.resolve().then(() => (init_timeline(), exports_timeline));
|
|
6719
|
+
const db = getDatabase();
|
|
6720
|
+
const items = getContactTimeline2(id, parseInt(opts.limit, 10), db);
|
|
6721
|
+
if (!items.length) {
|
|
6722
|
+
console.log(chalk.gray(`
|
|
6723
|
+
No timeline items found.
|
|
6724
|
+
`));
|
|
6725
|
+
return;
|
|
6726
|
+
}
|
|
6727
|
+
const contact = getContact(id);
|
|
6728
|
+
console.log(chalk.bold(`
|
|
6729
|
+
Timeline: ${contact.display_name}
|
|
6730
|
+
`));
|
|
6731
|
+
const iconMap = {
|
|
6732
|
+
note: "\uD83D\uDCDD",
|
|
6733
|
+
event: "\uD83D\uDCC5",
|
|
6734
|
+
task: "\u2705",
|
|
6735
|
+
vendor_comm: "\uD83D\uDCE7",
|
|
6736
|
+
interaction: "\uD83D\uDCAC",
|
|
6737
|
+
deal: "\uD83D\uDCB0"
|
|
6738
|
+
};
|
|
6739
|
+
for (const item of items) {
|
|
6740
|
+
const icon = iconMap[item.type] ?? "\u2022";
|
|
6741
|
+
console.log(` ${icon} ${chalk.gray(item.date?.slice(0, 10) ?? "")} ${chalk.bold(item.title)} ${chalk.gray(item.body ? item.body.slice(0, 60) : "")}`);
|
|
6742
|
+
}
|
|
6743
|
+
console.log();
|
|
6744
|
+
});
|
|
6745
|
+
program.command("enrich <id>").description("Auto-fill missing contact data via web search (requires EXA_API_KEY)").action(async (id) => {
|
|
6746
|
+
const contact = getContact(id);
|
|
6747
|
+
const exaKey = process.env["EXA_API_KEY"];
|
|
6748
|
+
if (!exaKey) {
|
|
6749
|
+
console.error(chalk.red(`
|
|
6750
|
+
Set EXA_API_KEY environment variable to use enrichment.
|
|
6751
|
+
`));
|
|
6752
|
+
process.exit(1);
|
|
6753
|
+
}
|
|
6754
|
+
console.log(chalk.blue(`
|
|
6755
|
+
Searching for: ${contact.display_name}...
|
|
6756
|
+
`));
|
|
6757
|
+
const primaryEmail = contact.emails?.[0]?.address ?? "";
|
|
6758
|
+
const query = `${contact.display_name} ${primaryEmail} site:linkedin.com OR site:twitter.com OR site:github.com`;
|
|
6759
|
+
const res = await fetch("https://api.exa.ai/search", {
|
|
6760
|
+
method: "POST",
|
|
6761
|
+
headers: { "x-api-key": exaKey, "content-type": "application/json" },
|
|
6762
|
+
body: JSON.stringify({ query, num_results: 5 })
|
|
6763
|
+
});
|
|
6764
|
+
const data = await res.json();
|
|
6765
|
+
const results = data.results ?? [];
|
|
6766
|
+
if (!results.length) {
|
|
6767
|
+
console.log(chalk.gray(`No results found.
|
|
6768
|
+
`));
|
|
6769
|
+
return;
|
|
6770
|
+
}
|
|
6771
|
+
const socialProfiles = contact.social_profiles;
|
|
6772
|
+
const suggestions = [];
|
|
6773
|
+
for (const r of results) {
|
|
6774
|
+
if (r.url?.includes("linkedin.com") && !socialProfiles?.find((s) => s.platform === "linkedin"))
|
|
6775
|
+
suggestions.push({ field: "linkedin", value: r.url });
|
|
6776
|
+
if (r.url?.includes("twitter.com") && !socialProfiles?.find((s) => s.platform === "twitter"))
|
|
6777
|
+
suggestions.push({ field: "twitter", value: r.url });
|
|
6778
|
+
if (r.url?.includes("github.com") && !socialProfiles?.find((s) => s.platform === "github"))
|
|
6779
|
+
suggestions.push({ field: "github", value: r.url });
|
|
6780
|
+
}
|
|
6781
|
+
if (!suggestions.length) {
|
|
6782
|
+
console.log(chalk.green(`No new data found to enrich.
|
|
6783
|
+
`));
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
console.log(chalk.yellow(`Suggestions (review before applying):
|
|
6787
|
+
`));
|
|
6788
|
+
for (const s of suggestions) {
|
|
6789
|
+
console.log(` ${chalk.cyan(s.field.padEnd(10))} ${s.value}`);
|
|
6790
|
+
}
|
|
6791
|
+
console.log(chalk.gray("\nUse `contacts edit <id>` to apply these manually.\n"));
|
|
6792
|
+
});
|
|
6793
|
+
program.command("remind <id>").option("--in <duration>", "Duration (e.g. 7d, 2w, 1m)").option("--on <date>", "Specific date YYYY-MM-DD").option("--note <text>", "Reminder note").description("Schedule a follow-up reminder").action(async (id, opts) => {
|
|
6794
|
+
let date;
|
|
6795
|
+
if (opts.on) {
|
|
6796
|
+
date = opts.on;
|
|
6797
|
+
} else if (opts.in) {
|
|
6798
|
+
const raw = opts.in;
|
|
6799
|
+
const n = parseInt(raw, 10);
|
|
6800
|
+
const unit = raw.slice(-1);
|
|
6801
|
+
const ms = unit === "w" ? n * 7 * 86400000 : unit === "m" ? n * 30 * 86400000 : n * 86400000;
|
|
6802
|
+
date = new Date(Date.now() + ms).toISOString().slice(0, 10);
|
|
6803
|
+
} else {
|
|
6804
|
+
console.error(chalk.red("Provide --in (e.g. 7d) or --on (YYYY-MM-DD)"));
|
|
6805
|
+
process.exit(1);
|
|
6806
|
+
}
|
|
6807
|
+
const db = getDatabase();
|
|
6808
|
+
updateContact(id, { follow_up_at: date });
|
|
6809
|
+
if (opts.note) {
|
|
6810
|
+
const { addNote: addNote2 } = await Promise.resolve().then(() => (init_notes(), exports_notes));
|
|
6811
|
+
addNote2(id, `Reminder (${date}): ${opts.note}`, undefined, db);
|
|
6812
|
+
}
|
|
6813
|
+
const contact = getContact(id);
|
|
6814
|
+
console.log(chalk.green(`
|
|
6815
|
+
\u2713 Reminder set for ${contact.display_name} on ${date}
|
|
6816
|
+
`));
|
|
6817
|
+
});
|
|
6818
|
+
var bulkTag = tagsCmd.command("bulk").description("Bulk tag operations");
|
|
6819
|
+
bulkTag.command("add <tag>").description("Apply a tag to multiple contacts").option("--query <q>", "Apply to all contacts matching search query").option("--all", "Apply to all contacts").option("--contact-ids <ids>", "Comma-separated contact IDs").action(async (tag, opts) => {
|
|
6820
|
+
const { addTagToContact: addTag } = await Promise.resolve().then(() => (init_tags(), exports_tags));
|
|
6821
|
+
const { getTagByName: getTagByNameFn } = await Promise.resolve().then(() => (init_tags(), exports_tags));
|
|
6822
|
+
const db = getDatabase();
|
|
6823
|
+
const tagRecord = getTagByNameFn(tag, db);
|
|
6824
|
+
if (!tagRecord) {
|
|
6825
|
+
console.error(chalk.red(`Tag not found: ${tag}`));
|
|
6826
|
+
process.exit(1);
|
|
6827
|
+
}
|
|
6828
|
+
let contactIds = [];
|
|
6829
|
+
if (opts.contactIds)
|
|
6830
|
+
contactIds = opts.contactIds.split(",").map((s) => s.trim());
|
|
6831
|
+
if (opts.query) {
|
|
6832
|
+
const found = searchContacts(opts.query);
|
|
6833
|
+
contactIds = [...contactIds, ...found.map((c) => c.id)];
|
|
6834
|
+
}
|
|
6835
|
+
if (opts.all) {
|
|
6836
|
+
const all = listContacts({ limit: 1e4 });
|
|
6837
|
+
contactIds = [...contactIds, ...all.contacts.map((c) => c.id)];
|
|
6838
|
+
}
|
|
6839
|
+
contactIds = [...new Set(contactIds)];
|
|
6840
|
+
let count = 0;
|
|
6841
|
+
for (const cid of contactIds) {
|
|
6842
|
+
try {
|
|
6843
|
+
addTag(cid, tagRecord.id);
|
|
6844
|
+
count++;
|
|
6845
|
+
} catch {}
|
|
6846
|
+
}
|
|
6847
|
+
console.log(chalk.green(`
|
|
6848
|
+
\u2713 Tagged ${count} contact(s) with #${tagRecord.name}
|
|
6849
|
+
`));
|
|
6850
|
+
});
|
|
6851
|
+
bulkTag.command("remove <tag>").description("Remove a tag from matching contacts").option("--query <q>", "Remove from all contacts matching search query").option("--contact-ids <ids>", "Comma-separated contact IDs").action(async (tag, opts) => {
|
|
6852
|
+
const { removeTagFromContact: removeTag } = await Promise.resolve().then(() => (init_tags(), exports_tags));
|
|
6853
|
+
const { getTagByName: getTagByNameFn } = await Promise.resolve().then(() => (init_tags(), exports_tags));
|
|
6854
|
+
const db = getDatabase();
|
|
6855
|
+
const tagRecord = getTagByNameFn(tag, db);
|
|
6856
|
+
if (!tagRecord) {
|
|
6857
|
+
console.error(chalk.red(`Tag not found: ${tag}`));
|
|
6858
|
+
process.exit(1);
|
|
6859
|
+
}
|
|
6860
|
+
let contactIds = [];
|
|
6861
|
+
if (opts.contactIds)
|
|
6862
|
+
contactIds = opts.contactIds.split(",").map((s) => s.trim());
|
|
6863
|
+
if (opts.query) {
|
|
6864
|
+
const found = searchContacts(opts.query);
|
|
6865
|
+
contactIds = [...contactIds, ...found.map((c) => c.id)];
|
|
6866
|
+
}
|
|
6867
|
+
contactIds = [...new Set(contactIds)];
|
|
6868
|
+
let count = 0;
|
|
6869
|
+
for (const cid of contactIds) {
|
|
6870
|
+
try {
|
|
6871
|
+
removeTag(cid, tagRecord.id);
|
|
6872
|
+
count++;
|
|
6873
|
+
} catch {}
|
|
6874
|
+
}
|
|
6875
|
+
console.log(chalk.green(`
|
|
6876
|
+
\u2713 Removed #${tagRecord.name} from ${count} contact(s)
|
|
6877
|
+
`));
|
|
6878
|
+
});
|
|
6879
|
+
program.command("dnc <id>").description("Mark contact as do-not-contact").option("--remove", "Remove DNC flag").option("--reason <text>", "Reason for DNC").action(async (id, opts) => {
|
|
6880
|
+
const contact = getContact(id);
|
|
6881
|
+
const db = getDatabase();
|
|
6882
|
+
updateContact(id, { do_not_contact: !opts.remove });
|
|
6883
|
+
if (opts.reason && !opts.remove) {
|
|
6884
|
+
const { addNote: addNote2 } = await Promise.resolve().then(() => (init_notes(), exports_notes));
|
|
6885
|
+
addNote2(id, `DNC: ${opts.reason}`, undefined, db);
|
|
6886
|
+
}
|
|
6887
|
+
if (opts.remove) {
|
|
6888
|
+
console.log(chalk.green(`
|
|
6889
|
+
\u2713 DNC flag removed for ${contact.display_name}
|
|
6890
|
+
`));
|
|
6891
|
+
} else {
|
|
6892
|
+
console.log(chalk.yellow(`
|
|
6893
|
+
\u26A0 ${contact.display_name} marked as do-not-contact
|
|
6894
|
+
`));
|
|
6895
|
+
}
|
|
6896
|
+
});
|
|
5535
6897
|
program.parse(process.argv);
|