@hasna/contacts 0.2.1 → 0.2.3
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 +146 -18
- package/dist/db/companies.d.ts +2 -0
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts +7 -1
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/groups.d.ts +11 -1
- package/dist/db/groups.d.ts.map +1 -1
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +275 -19
- package/dist/mcp/index.js +666 -34
- package/dist/server/index.js +116 -13
- package/dist/types/index.d.ts +41 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +21 -30
package/dist/index.js
CHANGED
|
@@ -150,9 +150,7 @@ var MIGRATIONS = [
|
|
|
150
150
|
last_name,
|
|
151
151
|
nickname,
|
|
152
152
|
notes,
|
|
153
|
-
job_title
|
|
154
|
-
content='contacts',
|
|
155
|
-
content_rowid='rowid'
|
|
153
|
+
job_title
|
|
156
154
|
);
|
|
157
155
|
|
|
158
156
|
CREATE TRIGGER IF NOT EXISTS contacts_fts_insert AFTER INSERT ON contacts BEGIN
|
|
@@ -190,6 +188,20 @@ var MIGRATIONS = [
|
|
|
190
188
|
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
191
189
|
PRIMARY KEY (contact_id, group_id)
|
|
192
190
|
);
|
|
191
|
+
`,
|
|
192
|
+
`
|
|
193
|
+
ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT 'active';
|
|
194
|
+
ALTER TABLE contacts ADD COLUMN follow_up_at TEXT;
|
|
195
|
+
ALTER TABLE contacts ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
196
|
+
ALTER TABLE contacts ADD COLUMN project_id TEXT;
|
|
197
|
+
ALTER TABLE companies ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
198
|
+
ALTER TABLE companies ADD COLUMN project_id TEXT;
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS company_groups (
|
|
201
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
202
|
+
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
203
|
+
PRIMARY KEY (company_id, group_id)
|
|
204
|
+
);
|
|
193
205
|
`
|
|
194
206
|
];
|
|
195
207
|
var _db = null;
|
|
@@ -301,7 +313,11 @@ function rowToContact(row) {
|
|
|
301
313
|
...row,
|
|
302
314
|
source: row.source,
|
|
303
315
|
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
304
|
-
preferred_contact_method: row.preferred_contact_method ?? null
|
|
316
|
+
preferred_contact_method: row.preferred_contact_method ?? null,
|
|
317
|
+
status: row.status ?? "active",
|
|
318
|
+
follow_up_at: row.follow_up_at ?? null,
|
|
319
|
+
archived: !!row.archived,
|
|
320
|
+
project_id: row.project_id ?? null
|
|
305
321
|
};
|
|
306
322
|
}
|
|
307
323
|
function rowToEmail(row) {
|
|
@@ -338,7 +354,9 @@ function rowToTag(row) {
|
|
|
338
354
|
function rowToCompany(row) {
|
|
339
355
|
return {
|
|
340
356
|
...row,
|
|
341
|
-
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
357
|
+
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
358
|
+
archived: !!row.archived,
|
|
359
|
+
project_id: row.project_id ?? null
|
|
342
360
|
};
|
|
343
361
|
}
|
|
344
362
|
function insertEmails(db, contactId, companyId, emails) {
|
|
@@ -382,8 +400,8 @@ function createContact(input, db) {
|
|
|
382
400
|
const firstName = input.first_name ?? "";
|
|
383
401
|
const lastName = input.last_name ?? "";
|
|
384
402
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
385
|
-
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, created_at, updated_at)
|
|
386
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
403
|
+
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)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
387
405
|
id,
|
|
388
406
|
firstName,
|
|
389
407
|
lastName,
|
|
@@ -399,6 +417,9 @@ function createContact(input, db) {
|
|
|
399
417
|
input.last_contacted_at ?? null,
|
|
400
418
|
input.website ?? null,
|
|
401
419
|
input.preferred_contact_method ?? null,
|
|
420
|
+
input.status ?? "active",
|
|
421
|
+
input.follow_up_at ?? null,
|
|
422
|
+
input.project_id ?? null,
|
|
402
423
|
timestamp,
|
|
403
424
|
timestamp
|
|
404
425
|
]);
|
|
@@ -433,12 +454,21 @@ function listContacts(opts = {}, db) {
|
|
|
433
454
|
offset = 0,
|
|
434
455
|
company_id,
|
|
435
456
|
tag_id,
|
|
457
|
+
tag_ids,
|
|
436
458
|
source,
|
|
459
|
+
status,
|
|
460
|
+
project_id,
|
|
461
|
+
archived = false,
|
|
462
|
+
follow_up_due,
|
|
463
|
+
last_contacted_after,
|
|
464
|
+
last_contacted_before,
|
|
437
465
|
order_by = "display_name",
|
|
438
466
|
order_dir = "asc"
|
|
439
467
|
} = opts;
|
|
440
468
|
const conditions = [];
|
|
441
469
|
const params = [];
|
|
470
|
+
conditions.push("c.archived = ?");
|
|
471
|
+
params.push(archived ? 1 : 0);
|
|
442
472
|
if (company_id) {
|
|
443
473
|
conditions.push("c.company_id = ?");
|
|
444
474
|
params.push(company_id);
|
|
@@ -447,12 +477,37 @@ function listContacts(opts = {}, db) {
|
|
|
447
477
|
conditions.push("c.source = ?");
|
|
448
478
|
params.push(source);
|
|
449
479
|
}
|
|
480
|
+
if (status) {
|
|
481
|
+
conditions.push("c.status = ?");
|
|
482
|
+
params.push(status);
|
|
483
|
+
}
|
|
484
|
+
if (project_id) {
|
|
485
|
+
conditions.push("c.project_id = ?");
|
|
486
|
+
params.push(project_id);
|
|
487
|
+
}
|
|
450
488
|
if (tag_id) {
|
|
451
489
|
conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
|
|
452
490
|
params.push(tag_id);
|
|
453
491
|
}
|
|
492
|
+
if (tag_ids?.length) {
|
|
493
|
+
for (const tid of tag_ids) {
|
|
494
|
+
conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
|
|
495
|
+
params.push(tid);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (follow_up_due) {
|
|
499
|
+
conditions.push("c.follow_up_at IS NOT NULL AND c.follow_up_at <= datetime('now')");
|
|
500
|
+
}
|
|
501
|
+
if (last_contacted_after) {
|
|
502
|
+
conditions.push("c.last_contacted_at >= ?");
|
|
503
|
+
params.push(last_contacted_after);
|
|
504
|
+
}
|
|
505
|
+
if (last_contacted_before) {
|
|
506
|
+
conditions.push("c.last_contacted_at <= ?");
|
|
507
|
+
params.push(last_contacted_before);
|
|
508
|
+
}
|
|
454
509
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
455
|
-
const validOrderBy = ["display_name", "created_at", "updated_at"].includes(order_by) ? order_by : "display_name";
|
|
510
|
+
const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
|
|
456
511
|
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
457
512
|
const totalRow = d.query(`SELECT COUNT(*) as total FROM contacts c ${where}`).get(...params);
|
|
458
513
|
const rows = d.query(`SELECT c.* FROM contacts c ${where} ORDER BY c.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
@@ -522,8 +577,36 @@ function updateContact(id, input, db) {
|
|
|
522
577
|
setClauses.push("preferred_contact_method = ?");
|
|
523
578
|
params.push(input.preferred_contact_method);
|
|
524
579
|
}
|
|
580
|
+
if (input.status !== undefined) {
|
|
581
|
+
setClauses.push("status = ?");
|
|
582
|
+
params.push(input.status);
|
|
583
|
+
}
|
|
584
|
+
if (input.follow_up_at !== undefined) {
|
|
585
|
+
setClauses.push("follow_up_at = ?");
|
|
586
|
+
params.push(input.follow_up_at);
|
|
587
|
+
}
|
|
588
|
+
if (input.project_id !== undefined) {
|
|
589
|
+
setClauses.push("project_id = ?");
|
|
590
|
+
params.push(input.project_id);
|
|
591
|
+
}
|
|
525
592
|
params.push(id);
|
|
526
593
|
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
594
|
+
if (input.emails_add?.length) {
|
|
595
|
+
for (const e of input.emails_add) {
|
|
596
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(id, e.address);
|
|
597
|
+
if (!exists) {
|
|
598
|
+
d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), id, e.address, e.type ?? "work", e.is_primary ? 1 : 0]);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (input.phones_add?.length) {
|
|
603
|
+
for (const p of input.phones_add) {
|
|
604
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(id, p.number);
|
|
605
|
+
if (!exists) {
|
|
606
|
+
d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), id, p.number, p.country_code ?? null, p.type ?? "mobile", p.is_primary ? 1 : 0]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
527
610
|
logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
|
|
528
611
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
529
612
|
return loadContactDetails(d, rowToContact(row));
|
|
@@ -541,25 +624,31 @@ function searchContacts(query, db) {
|
|
|
541
624
|
const ftsRows = d.query(`
|
|
542
625
|
SELECT c.* FROM contacts c
|
|
543
626
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
544
|
-
WHERE contacts_fts MATCH ?
|
|
627
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
545
628
|
ORDER BY rank
|
|
546
629
|
LIMIT 50
|
|
547
630
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
548
631
|
const emailRows = d.query(`
|
|
549
632
|
SELECT DISTINCT c.* FROM contacts c
|
|
550
633
|
JOIN emails e ON e.contact_id = c.id
|
|
551
|
-
WHERE e.address LIKE ?
|
|
634
|
+
WHERE e.address LIKE ? AND c.archived = 0
|
|
552
635
|
LIMIT 20
|
|
553
636
|
`).all(`%${query}%`);
|
|
554
637
|
const phoneRows = d.query(`
|
|
555
638
|
SELECT DISTINCT c.* FROM contacts c
|
|
556
639
|
JOIN phones p ON p.contact_id = c.id
|
|
557
|
-
WHERE p.number LIKE ?
|
|
640
|
+
WHERE p.number LIKE ? AND c.archived = 0
|
|
641
|
+
LIMIT 20
|
|
642
|
+
`).all(`%${query}%`);
|
|
643
|
+
const companyRows = d.query(`
|
|
644
|
+
SELECT DISTINCT c.* FROM contacts c
|
|
645
|
+
JOIN companies co ON co.id = c.company_id
|
|
646
|
+
WHERE co.name LIKE ? AND c.archived = 0
|
|
558
647
|
LIMIT 20
|
|
559
648
|
`).all(`%${query}%`);
|
|
560
649
|
const seen = new Set;
|
|
561
650
|
const allRows = [];
|
|
562
|
-
for (const row of [...ftsRows, ...emailRows, ...phoneRows]) {
|
|
651
|
+
for (const row of [...ftsRows, ...emailRows, ...phoneRows, ...companyRows]) {
|
|
563
652
|
if (!seen.has(row.id)) {
|
|
564
653
|
seen.add(row.id);
|
|
565
654
|
allRows.push(row);
|
|
@@ -580,8 +669,24 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
580
669
|
const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
|
|
581
670
|
if (!mergeRow)
|
|
582
671
|
throw new ContactNotFoundError(mergeId);
|
|
583
|
-
d.
|
|
584
|
-
|
|
672
|
+
const mergeEmailRows = d.query(`SELECT * FROM emails WHERE contact_id = ?`).all(mergeId);
|
|
673
|
+
for (const e of mergeEmailRows) {
|
|
674
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(keepId, e.address);
|
|
675
|
+
if (exists) {
|
|
676
|
+
d.run(`DELETE FROM emails WHERE id = ?`, [e.id]);
|
|
677
|
+
} else {
|
|
678
|
+
d.run(`UPDATE emails SET contact_id = ? WHERE id = ?`, [keepId, e.id]);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const mergePhoneRows = d.query(`SELECT * FROM phones WHERE contact_id = ?`).all(mergeId);
|
|
682
|
+
for (const p of mergePhoneRows) {
|
|
683
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(keepId, p.number);
|
|
684
|
+
if (exists) {
|
|
685
|
+
d.run(`DELETE FROM phones WHERE id = ?`, [p.id]);
|
|
686
|
+
} else {
|
|
687
|
+
d.run(`UPDATE phones SET contact_id = ? WHERE id = ?`, [keepId, p.id]);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
585
690
|
d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
586
691
|
d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
587
692
|
const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
|
|
@@ -628,11 +733,86 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
628
733
|
const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
629
734
|
return loadContactDetails(d, rowToContact(finalRow));
|
|
630
735
|
}
|
|
736
|
+
function getContactByEmail(email, db) {
|
|
737
|
+
const d = db || getDatabase();
|
|
738
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(email);
|
|
739
|
+
if (!emailRow)
|
|
740
|
+
return null;
|
|
741
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(emailRow.contact_id);
|
|
742
|
+
if (!row)
|
|
743
|
+
return null;
|
|
744
|
+
return loadContactDetails(d, rowToContact(row));
|
|
745
|
+
}
|
|
746
|
+
function addEmailToContact(contactId, email, db) {
|
|
747
|
+
const d = db || getDatabase();
|
|
748
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
749
|
+
if (!row)
|
|
750
|
+
throw new ContactNotFoundError(contactId);
|
|
751
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(contactId, email.address);
|
|
752
|
+
if (!exists) {
|
|
753
|
+
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]);
|
|
754
|
+
}
|
|
755
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
756
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
757
|
+
}
|
|
758
|
+
function addPhoneToContact(contactId, phone, db) {
|
|
759
|
+
const d = db || getDatabase();
|
|
760
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
761
|
+
if (!row)
|
|
762
|
+
throw new ContactNotFoundError(contactId);
|
|
763
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(contactId, phone.number);
|
|
764
|
+
if (!exists) {
|
|
765
|
+
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]);
|
|
766
|
+
}
|
|
767
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
768
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
769
|
+
}
|
|
770
|
+
function archiveContact(id, db) {
|
|
771
|
+
const d = db || getDatabase();
|
|
772
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
773
|
+
if (!row)
|
|
774
|
+
throw new ContactNotFoundError(id);
|
|
775
|
+
d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
776
|
+
logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
|
|
777
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
778
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
779
|
+
}
|
|
780
|
+
function unarchiveContact(id, db) {
|
|
781
|
+
const d = db || getDatabase();
|
|
782
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
783
|
+
if (!row)
|
|
784
|
+
throw new ContactNotFoundError(id);
|
|
785
|
+
d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
786
|
+
logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
|
|
787
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
788
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
789
|
+
}
|
|
790
|
+
function autoLinkContactToCompany(contactId, db) {
|
|
791
|
+
const d = db || getDatabase();
|
|
792
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
793
|
+
if (!row || row.company_id)
|
|
794
|
+
return null;
|
|
795
|
+
const emailRow = d.query(`SELECT address FROM emails WHERE contact_id = ? AND contact_id IS NOT NULL LIMIT 1`).get(contactId);
|
|
796
|
+
if (!emailRow)
|
|
797
|
+
return null;
|
|
798
|
+
const domain = emailRow.address.split("@")[1];
|
|
799
|
+
if (!domain)
|
|
800
|
+
return null;
|
|
801
|
+
const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
|
|
802
|
+
if (!companyRow)
|
|
803
|
+
return null;
|
|
804
|
+
d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now(), contactId]);
|
|
805
|
+
logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
|
|
806
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
807
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
808
|
+
}
|
|
631
809
|
// src/db/companies.ts
|
|
632
810
|
function rowToCompany2(row) {
|
|
633
811
|
return {
|
|
634
812
|
...row,
|
|
635
|
-
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
813
|
+
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
814
|
+
archived: !!row.archived,
|
|
815
|
+
project_id: row.project_id ?? null
|
|
636
816
|
};
|
|
637
817
|
}
|
|
638
818
|
function insertEmails2(db, companyId, emails) {
|
|
@@ -742,15 +922,23 @@ function listCompanies(opts = {}, db) {
|
|
|
742
922
|
offset = 0,
|
|
743
923
|
industry,
|
|
744
924
|
tag_id,
|
|
925
|
+
project_id,
|
|
926
|
+
archived = false,
|
|
745
927
|
order_by = "name",
|
|
746
928
|
order_dir = "asc"
|
|
747
929
|
} = opts;
|
|
748
930
|
const conditions = [];
|
|
749
931
|
const params = [];
|
|
932
|
+
conditions.push("co.archived = ?");
|
|
933
|
+
params.push(archived ? 1 : 0);
|
|
750
934
|
if (industry) {
|
|
751
935
|
conditions.push("co.industry = ?");
|
|
752
936
|
params.push(industry);
|
|
753
937
|
}
|
|
938
|
+
if (project_id) {
|
|
939
|
+
conditions.push("co.project_id = ?");
|
|
940
|
+
params.push(project_id);
|
|
941
|
+
}
|
|
754
942
|
if (tag_id) {
|
|
755
943
|
conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
|
|
756
944
|
params.push(tag_id);
|
|
@@ -806,6 +994,10 @@ function updateCompany(id, input, db) {
|
|
|
806
994
|
setClauses.push("custom_fields = ?");
|
|
807
995
|
params.push(JSON.stringify(input.custom_fields));
|
|
808
996
|
}
|
|
997
|
+
if ("project_id" in input && input.project_id !== undefined) {
|
|
998
|
+
setClauses.push("project_id = ?");
|
|
999
|
+
params.push(input.project_id);
|
|
1000
|
+
}
|
|
809
1001
|
params.push(id);
|
|
810
1002
|
d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
811
1003
|
logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
|
|
@@ -839,9 +1031,33 @@ function listCompanyEmployees(companyId, db) {
|
|
|
839
1031
|
...r,
|
|
840
1032
|
source: r.source,
|
|
841
1033
|
custom_fields: JSON.parse(r.custom_fields || "{}"),
|
|
842
|
-
preferred_contact_method: r.preferred_contact_method ?? null
|
|
1034
|
+
preferred_contact_method: r.preferred_contact_method ?? null,
|
|
1035
|
+
status: r.status ?? "active",
|
|
1036
|
+
follow_up_at: r.follow_up_at ?? null,
|
|
1037
|
+
archived: !!r.archived,
|
|
1038
|
+
project_id: r.project_id ?? null
|
|
843
1039
|
}));
|
|
844
1040
|
}
|
|
1041
|
+
function archiveCompany(id, db) {
|
|
1042
|
+
const d = db || getDatabase();
|
|
1043
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1044
|
+
if (!row)
|
|
1045
|
+
throw new CompanyNotFoundError(id);
|
|
1046
|
+
d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
1047
|
+
logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
|
|
1048
|
+
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1049
|
+
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
1050
|
+
}
|
|
1051
|
+
function unarchiveCompany(id, db) {
|
|
1052
|
+
const d = db || getDatabase();
|
|
1053
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1054
|
+
if (!row)
|
|
1055
|
+
throw new CompanyNotFoundError(id);
|
|
1056
|
+
d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
1057
|
+
logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
|
|
1058
|
+
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1059
|
+
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
1060
|
+
}
|
|
845
1061
|
// src/db/tags.ts
|
|
846
1062
|
function rowToTag2(row) {
|
|
847
1063
|
return { ...row };
|
|
@@ -937,7 +1153,11 @@ function listContactsByTag(tagId, db) {
|
|
|
937
1153
|
...r,
|
|
938
1154
|
source: r.source,
|
|
939
1155
|
custom_fields: JSON.parse(r.custom_fields || "{}"),
|
|
940
|
-
preferred_contact_method: r.preferred_contact_method ?? null
|
|
1156
|
+
preferred_contact_method: r.preferred_contact_method ?? null,
|
|
1157
|
+
status: r.status ?? "active",
|
|
1158
|
+
follow_up_at: r.follow_up_at ?? null,
|
|
1159
|
+
archived: !!r.archived,
|
|
1160
|
+
project_id: r.project_id ?? null
|
|
941
1161
|
}));
|
|
942
1162
|
}
|
|
943
1163
|
function addTagToCompany(companyId, tagId, db) {
|
|
@@ -1009,7 +1229,10 @@ function getGroup(db, id) {
|
|
|
1009
1229
|
return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
|
|
1010
1230
|
}
|
|
1011
1231
|
function listGroups(db) {
|
|
1012
|
-
return db.query(`SELECT g.*,
|
|
1232
|
+
return db.query(`SELECT g.*,
|
|
1233
|
+
(SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) as member_count,
|
|
1234
|
+
(SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) as company_count
|
|
1235
|
+
FROM groups g ORDER BY g.name`).all();
|
|
1013
1236
|
}
|
|
1014
1237
|
function updateGroup(db, id, input) {
|
|
1015
1238
|
const fields = [];
|
|
@@ -1032,7 +1255,11 @@ function deleteGroup(db, id) {
|
|
|
1032
1255
|
db.query(`DELETE FROM groups WHERE id = ?`).run(id);
|
|
1033
1256
|
}
|
|
1034
1257
|
function addContactToGroup(db, contactId, groupId) {
|
|
1035
|
-
db.query(`
|
|
1258
|
+
const existing = db.query(`SELECT 1 FROM contact_groups WHERE contact_id = ? AND group_id = ?`).get(contactId, groupId);
|
|
1259
|
+
if (existing)
|
|
1260
|
+
return { added: false, already_member: true };
|
|
1261
|
+
db.query(`INSERT INTO contact_groups(contact_id, group_id) VALUES(?,?)`).run(contactId, groupId);
|
|
1262
|
+
return { added: true, already_member: false };
|
|
1036
1263
|
}
|
|
1037
1264
|
function removeContactFromGroup(db, contactId, groupId) {
|
|
1038
1265
|
db.query(`DELETE FROM contact_groups WHERE contact_id = ? AND group_id = ?`).run(contactId, groupId);
|
|
@@ -1044,28 +1271,50 @@ function listContactsInGroup(db, groupId) {
|
|
|
1044
1271
|
function listGroupsForContact(db, contactId) {
|
|
1045
1272
|
return db.query(`SELECT g.* FROM groups g JOIN contact_groups cg ON g.id = cg.group_id WHERE cg.contact_id = ? ORDER BY g.name`).all(contactId);
|
|
1046
1273
|
}
|
|
1274
|
+
function addCompanyToGroup(db, companyId, groupId) {
|
|
1275
|
+
const existing = db.query(`SELECT 1 FROM company_groups WHERE company_id = ? AND group_id = ?`).get(companyId, groupId);
|
|
1276
|
+
if (existing)
|
|
1277
|
+
return { added: false, already_member: true };
|
|
1278
|
+
db.query(`INSERT INTO company_groups(company_id, group_id) VALUES(?,?)`).run(companyId, groupId);
|
|
1279
|
+
return { added: true, already_member: false };
|
|
1280
|
+
}
|
|
1281
|
+
function removeCompanyFromGroup(db, companyId, groupId) {
|
|
1282
|
+
db.query(`DELETE FROM company_groups WHERE company_id = ? AND group_id = ?`).run(companyId, groupId);
|
|
1283
|
+
}
|
|
1284
|
+
function listCompaniesInGroup(db, groupId) {
|
|
1285
|
+
const rows = db.query(`SELECT company_id FROM company_groups WHERE group_id = ?`).all(groupId);
|
|
1286
|
+
return rows.map((r) => r.company_id);
|
|
1287
|
+
}
|
|
1288
|
+
function listGroupsForCompany(db, companyId) {
|
|
1289
|
+
return db.query(`SELECT g.* FROM groups g JOIN company_groups cog ON g.id = cog.group_id WHERE cog.company_id = ? ORDER BY g.name`).all(companyId);
|
|
1290
|
+
}
|
|
1047
1291
|
export {
|
|
1048
1292
|
updateTag,
|
|
1049
1293
|
updateGroup,
|
|
1050
1294
|
updateContact,
|
|
1051
1295
|
updateCompany,
|
|
1296
|
+
unarchiveContact,
|
|
1297
|
+
unarchiveCompany,
|
|
1052
1298
|
searchContacts,
|
|
1053
1299
|
searchCompanies,
|
|
1054
1300
|
resetDatabase,
|
|
1055
1301
|
removeTagFromContact,
|
|
1056
1302
|
removeTagFromCompany,
|
|
1057
1303
|
removeContactFromGroup,
|
|
1304
|
+
removeCompanyFromGroup,
|
|
1058
1305
|
mergeContacts,
|
|
1059
1306
|
logActivity,
|
|
1060
1307
|
listTags,
|
|
1061
1308
|
listRelationships,
|
|
1062
1309
|
listRecentContacts,
|
|
1063
1310
|
listGroupsForContact,
|
|
1311
|
+
listGroupsForCompany,
|
|
1064
1312
|
listGroups,
|
|
1065
1313
|
listContactsInGroup,
|
|
1066
1314
|
listContactsByTag,
|
|
1067
1315
|
listContacts,
|
|
1068
1316
|
listCompanyEmployees,
|
|
1317
|
+
listCompaniesInGroup,
|
|
1069
1318
|
listCompanies,
|
|
1070
1319
|
listActivity,
|
|
1071
1320
|
getTagByName,
|
|
@@ -1073,6 +1322,7 @@ export {
|
|
|
1073
1322
|
getRelationship,
|
|
1074
1323
|
getGroup,
|
|
1075
1324
|
getDatabase,
|
|
1325
|
+
getContactByEmail,
|
|
1076
1326
|
getContact,
|
|
1077
1327
|
getCompany,
|
|
1078
1328
|
getActivity,
|
|
@@ -1086,9 +1336,15 @@ export {
|
|
|
1086
1336
|
createGroup,
|
|
1087
1337
|
createContact,
|
|
1088
1338
|
createCompany,
|
|
1339
|
+
autoLinkContactToCompany,
|
|
1340
|
+
archiveContact,
|
|
1341
|
+
archiveCompany,
|
|
1089
1342
|
addTagToContact,
|
|
1090
1343
|
addTagToCompany,
|
|
1344
|
+
addPhoneToContact,
|
|
1345
|
+
addEmailToContact,
|
|
1091
1346
|
addContactToGroup,
|
|
1347
|
+
addCompanyToGroup,
|
|
1092
1348
|
TagNotFoundError,
|
|
1093
1349
|
DuplicateTagNameError,
|
|
1094
1350
|
ContactNotFoundError,
|