@voyantjs/crm 0.2.0 → 0.3.1
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/routes/accounts.d.ts +1 -1
- package/dist/routes/index.d.ts +2 -2
- package/dist/routes/opportunities.d.ts +1 -1
- package/dist/schema-accounts.d.ts +1175 -0
- package/dist/schema-accounts.d.ts.map +1 -0
- package/dist/schema-accounts.js +112 -0
- package/dist/schema-activities.d.ts +821 -0
- package/dist/schema-activities.d.ts.map +1 -0
- package/dist/schema-activities.js +83 -0
- package/dist/schema-relations.d.ts +81 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +123 -0
- package/dist/schema-sales.d.ts +1392 -0
- package/dist/schema-sales.d.ts.map +1 -0
- package/dist/schema-sales.js +142 -0
- package/dist/schema-shared.d.ts +13 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +63 -0
- package/dist/schema.d.ts +5 -3477
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -515
- package/dist/service/accounts-organizations.d.ts +93 -0
- package/dist/service/accounts-organizations.d.ts.map +1 -0
- package/dist/service/accounts-organizations.js +49 -0
- package/dist/service/accounts-people.d.ts +868 -0
- package/dist/service/accounts-people.d.ts.map +1 -0
- package/dist/service/accounts-people.js +311 -0
- package/dist/service/accounts-shared.d.ts +54 -0
- package/dist/service/accounts-shared.d.ts.map +1 -0
- package/dist/service/accounts-shared.js +152 -0
- package/dist/service/accounts.d.ts +121 -149
- package/dist/service/accounts.d.ts.map +1 -1
- package/dist/service/accounts.js +4 -507
- package/dist/service/index.d.ts +106 -297
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/opportunities.d.ts +1 -1
- package/package.json +5 -5
package/dist/service/accounts.js
CHANGED
|
@@ -1,509 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { and, desc, eq, gte, ilike, inArray, lte, or, sql } from "drizzle-orm";
|
|
4
|
-
import { communicationLog, organizationNotes, organizations, people, personNotes, segments, } from "../schema.js";
|
|
5
|
-
import { formatAddress, isManagedBySource, normalizeContactValue, paginate, toNullableTrimmed, } from "./helpers.js";
|
|
6
|
-
const organizationEntityType = "organization";
|
|
7
|
-
const personEntityType = "person";
|
|
8
|
-
const personBaseIdentitySource = "crm.person.base";
|
|
9
|
-
function personBaseFields(data) {
|
|
10
|
-
return {
|
|
11
|
-
organizationId: data.organizationId,
|
|
12
|
-
firstName: data.firstName,
|
|
13
|
-
lastName: data.lastName,
|
|
14
|
-
jobTitle: data.jobTitle,
|
|
15
|
-
relation: data.relation,
|
|
16
|
-
preferredLanguage: data.preferredLanguage,
|
|
17
|
-
preferredCurrency: data.preferredCurrency,
|
|
18
|
-
ownerId: data.ownerId,
|
|
19
|
-
status: data.status,
|
|
20
|
-
source: data.source,
|
|
21
|
-
sourceRef: data.sourceRef,
|
|
22
|
-
tags: data.tags,
|
|
23
|
-
birthday: data.birthday,
|
|
24
|
-
notes: data.notes,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
async function syncPersonIdentity(db, personId, data) {
|
|
28
|
-
const existingContactPoints = await identityService.listContactPointsForEntity(db, personEntityType, personId);
|
|
29
|
-
const existingAddresses = await identityService.listAddressesForEntity(db, personEntityType, personId);
|
|
30
|
-
const managedContactPoints = existingContactPoints.filter((point) => isManagedBySource(point.metadata, personBaseIdentitySource));
|
|
31
|
-
const managedAddress = existingAddresses.find((address) => isManagedBySource(address.metadata, personBaseIdentitySource));
|
|
32
|
-
for (const [kind, rawValue] of Object.entries({
|
|
33
|
-
email: data.email,
|
|
34
|
-
phone: data.phone,
|
|
35
|
-
website: data.website,
|
|
36
|
-
})) {
|
|
37
|
-
const value = toNullableTrimmed(rawValue);
|
|
38
|
-
const existing = managedContactPoints.find((point) => point.kind === kind) ??
|
|
39
|
-
existingContactPoints.find((point) => point.kind === kind && point.isPrimary);
|
|
40
|
-
if (!value) {
|
|
41
|
-
if (existing) {
|
|
42
|
-
await identityService.deleteContactPoint(db, existing.id);
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const payload = {
|
|
47
|
-
entityType: personEntityType,
|
|
48
|
-
entityId: personId,
|
|
49
|
-
kind,
|
|
50
|
-
label: kind === "website" ? "website" : "primary",
|
|
51
|
-
value,
|
|
52
|
-
normalizedValue: normalizeContactValue(kind, value),
|
|
53
|
-
isPrimary: true,
|
|
54
|
-
metadata: { managedBy: personBaseIdentitySource },
|
|
55
|
-
};
|
|
56
|
-
if (existing) {
|
|
57
|
-
await identityService.updateContactPoint(db, existing.id, payload);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
await identityService.createContactPoint(db, payload);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const addressLine = toNullableTrimmed(data.address);
|
|
64
|
-
const city = toNullableTrimmed(data.city);
|
|
65
|
-
const country = toNullableTrimmed(data.country);
|
|
66
|
-
const hasAddress = Boolean(addressLine || city || country);
|
|
67
|
-
if (!hasAddress) {
|
|
68
|
-
if (managedAddress) {
|
|
69
|
-
await identityService.deleteAddress(db, managedAddress.id);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const addressPayload = {
|
|
74
|
-
entityType: personEntityType,
|
|
75
|
-
entityId: personId,
|
|
76
|
-
label: "primary",
|
|
77
|
-
fullText: addressLine,
|
|
78
|
-
line1: addressLine,
|
|
79
|
-
city,
|
|
80
|
-
country,
|
|
81
|
-
isPrimary: true,
|
|
82
|
-
metadata: { managedBy: personBaseIdentitySource },
|
|
83
|
-
};
|
|
84
|
-
if (managedAddress) {
|
|
85
|
-
await identityService.updateAddress(db, managedAddress.id, addressPayload);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
await identityService.createAddress(db, addressPayload);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function deletePersonIdentity(db, personId) {
|
|
92
|
-
const [contactPoints, addresses] = await Promise.all([
|
|
93
|
-
identityService.listContactPointsForEntity(db, personEntityType, personId),
|
|
94
|
-
identityService.listAddressesForEntity(db, personEntityType, personId),
|
|
95
|
-
]);
|
|
96
|
-
await Promise.all([
|
|
97
|
-
...contactPoints.map((point) => identityService.deleteContactPoint(db, point.id)),
|
|
98
|
-
...addresses.map((address) => identityService.deleteAddress(db, address.id)),
|
|
99
|
-
]);
|
|
100
|
-
}
|
|
101
|
-
async function hydratePeople(db, rows) {
|
|
102
|
-
if (rows.length === 0) {
|
|
103
|
-
return rows.map((row) => ({
|
|
104
|
-
...row,
|
|
105
|
-
email: null,
|
|
106
|
-
phone: null,
|
|
107
|
-
website: null,
|
|
108
|
-
address: null,
|
|
109
|
-
city: null,
|
|
110
|
-
country: null,
|
|
111
|
-
}));
|
|
112
|
-
}
|
|
113
|
-
const ids = rows.map((row) => row.id);
|
|
114
|
-
const [contactPoints, addresses] = await Promise.all([
|
|
115
|
-
db
|
|
116
|
-
.select()
|
|
117
|
-
.from(identityContactPoints)
|
|
118
|
-
.where(and(eq(identityContactPoints.entityType, personEntityType), inArray(identityContactPoints.entityId, ids))),
|
|
119
|
-
db
|
|
120
|
-
.select()
|
|
121
|
-
.from(identityAddresses)
|
|
122
|
-
.where(and(eq(identityAddresses.entityType, personEntityType), inArray(identityAddresses.entityId, ids))),
|
|
123
|
-
]);
|
|
124
|
-
const contactPointMap = new Map();
|
|
125
|
-
const addressMap = new Map();
|
|
126
|
-
for (const point of contactPoints) {
|
|
127
|
-
const bucket = contactPointMap.get(point.entityId) ?? [];
|
|
128
|
-
bucket.push(point);
|
|
129
|
-
contactPointMap.set(point.entityId, bucket);
|
|
130
|
-
}
|
|
131
|
-
for (const address of addresses) {
|
|
132
|
-
const bucket = addressMap.get(address.entityId) ?? [];
|
|
133
|
-
bucket.push(address);
|
|
134
|
-
addressMap.set(address.entityId, bucket);
|
|
135
|
-
}
|
|
136
|
-
return rows.map((row) => {
|
|
137
|
-
const entityContactPoints = contactPointMap.get(row.id) ?? [];
|
|
138
|
-
const entityAddresses = addressMap.get(row.id) ?? [];
|
|
139
|
-
const findPrimaryContactPoint = (kind) => entityContactPoints.find((point) => point.kind === kind && point.isPrimary) ??
|
|
140
|
-
entityContactPoints.find((point) => point.kind === kind) ??
|
|
141
|
-
null;
|
|
142
|
-
const primaryAddress = entityAddresses.find((address) => address.isPrimary) ?? entityAddresses[0] ?? null;
|
|
143
|
-
return {
|
|
144
|
-
...row,
|
|
145
|
-
email: findPrimaryContactPoint("email")?.value ?? null,
|
|
146
|
-
phone: findPrimaryContactPoint("phone")?.value ?? null,
|
|
147
|
-
website: findPrimaryContactPoint("website")?.value ?? null,
|
|
148
|
-
address: primaryAddress ? formatAddress(primaryAddress) : null,
|
|
149
|
-
city: primaryAddress?.city ?? null,
|
|
150
|
-
country: primaryAddress?.country ?? null,
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
}
|
|
1
|
+
import { organizationAccountsService } from "./accounts-organizations.js";
|
|
2
|
+
import { peopleAccountsService } from "./accounts-people.js";
|
|
154
3
|
export const accountsService = {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (query.ownerId)
|
|
158
|
-
conditions.push(eq(organizations.ownerId, query.ownerId));
|
|
159
|
-
if (query.relation)
|
|
160
|
-
conditions.push(eq(organizations.relation, query.relation));
|
|
161
|
-
if (query.status)
|
|
162
|
-
conditions.push(eq(organizations.status, query.status));
|
|
163
|
-
if (query.search) {
|
|
164
|
-
const term = `%${query.search}%`;
|
|
165
|
-
conditions.push(or(ilike(organizations.name, term), ilike(organizations.legalName, term), ilike(organizations.website, term)));
|
|
166
|
-
}
|
|
167
|
-
const where = conditions.length ? and(...conditions) : undefined;
|
|
168
|
-
return paginate(db
|
|
169
|
-
.select()
|
|
170
|
-
.from(organizations)
|
|
171
|
-
.where(where)
|
|
172
|
-
.limit(query.limit)
|
|
173
|
-
.offset(query.offset)
|
|
174
|
-
.orderBy(desc(organizations.updatedAt)), db.select({ count: sql `count(*)::int` }).from(organizations).where(where), query.limit, query.offset);
|
|
175
|
-
},
|
|
176
|
-
async getOrganizationById(db, id) {
|
|
177
|
-
const [row] = await db.select().from(organizations).where(eq(organizations.id, id)).limit(1);
|
|
178
|
-
return row ?? null;
|
|
179
|
-
},
|
|
180
|
-
async createOrganization(db, data) {
|
|
181
|
-
const [row] = await db.insert(organizations).values(data).returning();
|
|
182
|
-
return row;
|
|
183
|
-
},
|
|
184
|
-
async updateOrganization(db, id, data) {
|
|
185
|
-
const [row] = await db
|
|
186
|
-
.update(organizations)
|
|
187
|
-
.set({ ...data, updatedAt: new Date() })
|
|
188
|
-
.where(eq(organizations.id, id))
|
|
189
|
-
.returning();
|
|
190
|
-
return row ?? null;
|
|
191
|
-
},
|
|
192
|
-
async deleteOrganization(db, id) {
|
|
193
|
-
const [row] = await db
|
|
194
|
-
.delete(organizations)
|
|
195
|
-
.where(eq(organizations.id, id))
|
|
196
|
-
.returning({ id: organizations.id });
|
|
197
|
-
return row ?? null;
|
|
198
|
-
},
|
|
199
|
-
async listPeople(db, query) {
|
|
200
|
-
const conditions = [];
|
|
201
|
-
if (query.organizationId)
|
|
202
|
-
conditions.push(eq(people.organizationId, query.organizationId));
|
|
203
|
-
if (query.ownerId)
|
|
204
|
-
conditions.push(eq(people.ownerId, query.ownerId));
|
|
205
|
-
if (query.relation)
|
|
206
|
-
conditions.push(eq(people.relation, query.relation));
|
|
207
|
-
if (query.status)
|
|
208
|
-
conditions.push(eq(people.status, query.status));
|
|
209
|
-
if (query.search) {
|
|
210
|
-
const term = `%${query.search}%`;
|
|
211
|
-
conditions.push(or(ilike(people.firstName, term), ilike(people.lastName, term), ilike(people.jobTitle, term)));
|
|
212
|
-
}
|
|
213
|
-
const where = conditions.length ? and(...conditions) : undefined;
|
|
214
|
-
const result = await paginate(db
|
|
215
|
-
.select()
|
|
216
|
-
.from(people)
|
|
217
|
-
.where(where)
|
|
218
|
-
.limit(query.limit)
|
|
219
|
-
.offset(query.offset)
|
|
220
|
-
.orderBy(desc(people.updatedAt)), db.select({ count: sql `count(*)::int` }).from(people).where(where), query.limit, query.offset);
|
|
221
|
-
return {
|
|
222
|
-
...result,
|
|
223
|
-
data: await hydratePeople(db, result.data),
|
|
224
|
-
};
|
|
225
|
-
},
|
|
226
|
-
async getPersonById(db, id) {
|
|
227
|
-
const [row] = await db.select().from(people).where(eq(people.id, id)).limit(1);
|
|
228
|
-
if (!row)
|
|
229
|
-
return null;
|
|
230
|
-
const [hydrated] = await hydratePeople(db, [row]);
|
|
231
|
-
return hydrated ?? null;
|
|
232
|
-
},
|
|
233
|
-
async createPerson(db, data) {
|
|
234
|
-
const [row] = await db
|
|
235
|
-
.insert(people)
|
|
236
|
-
.values({
|
|
237
|
-
...personBaseFields(data),
|
|
238
|
-
firstName: data.firstName,
|
|
239
|
-
lastName: data.lastName,
|
|
240
|
-
})
|
|
241
|
-
.returning();
|
|
242
|
-
if (!row) {
|
|
243
|
-
throw new Error("Failed to create person");
|
|
244
|
-
}
|
|
245
|
-
await syncPersonIdentity(db, row.id, data);
|
|
246
|
-
return this.getPersonById(db, row.id);
|
|
247
|
-
},
|
|
248
|
-
async updatePerson(db, id, data) {
|
|
249
|
-
const existing = await this.getPersonById(db, id);
|
|
250
|
-
if (!existing)
|
|
251
|
-
return null;
|
|
252
|
-
await db
|
|
253
|
-
.update(people)
|
|
254
|
-
.set({ ...personBaseFields(data), updatedAt: new Date() })
|
|
255
|
-
.where(eq(people.id, id));
|
|
256
|
-
await syncPersonIdentity(db, id, {
|
|
257
|
-
email: data.email === undefined ? existing.email : data.email,
|
|
258
|
-
phone: data.phone === undefined ? existing.phone : data.phone,
|
|
259
|
-
website: data.website === undefined ? existing.website : data.website,
|
|
260
|
-
address: data.address === undefined ? existing.address : data.address,
|
|
261
|
-
city: data.city === undefined ? existing.city : data.city,
|
|
262
|
-
country: data.country === undefined ? existing.country : data.country,
|
|
263
|
-
});
|
|
264
|
-
return this.getPersonById(db, id);
|
|
265
|
-
},
|
|
266
|
-
async deletePerson(db, id) {
|
|
267
|
-
await deletePersonIdentity(db, id);
|
|
268
|
-
const [row] = await db.delete(people).where(eq(people.id, id)).returning({ id: people.id });
|
|
269
|
-
return row ?? null;
|
|
270
|
-
},
|
|
271
|
-
// --- Contact methods & addresses (explicit CRUD via identity) ---
|
|
272
|
-
listContactMethods(db, entityType, entityId) {
|
|
273
|
-
return identityService.listContactPointsForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
|
|
274
|
-
},
|
|
275
|
-
async createContactMethod(db, entityType, entityId, data) {
|
|
276
|
-
return identityService.createContactPoint(db, {
|
|
277
|
-
...data,
|
|
278
|
-
entityType: entityType === "organization" ? organizationEntityType : personEntityType,
|
|
279
|
-
entityId,
|
|
280
|
-
});
|
|
281
|
-
},
|
|
282
|
-
async updateContactMethod(db, id, data) {
|
|
283
|
-
return identityService.updateContactPoint(db, id, data);
|
|
284
|
-
},
|
|
285
|
-
async deleteContactMethod(db, id) {
|
|
286
|
-
return identityService.deleteContactPoint(db, id);
|
|
287
|
-
},
|
|
288
|
-
listAddresses(db, entityType, entityId) {
|
|
289
|
-
return identityService.listAddressesForEntity(db, entityType === "organization" ? organizationEntityType : personEntityType, entityId);
|
|
290
|
-
},
|
|
291
|
-
async createAddress(db, entityType, entityId, data) {
|
|
292
|
-
return identityService.createAddress(db, {
|
|
293
|
-
...data,
|
|
294
|
-
entityType: entityType === "organization" ? organizationEntityType : personEntityType,
|
|
295
|
-
entityId,
|
|
296
|
-
});
|
|
297
|
-
},
|
|
298
|
-
async updateAddress(db, id, data) {
|
|
299
|
-
return identityService.updateAddress(db, id, data);
|
|
300
|
-
},
|
|
301
|
-
async deleteAddress(db, id) {
|
|
302
|
-
return identityService.deleteAddress(db, id);
|
|
303
|
-
},
|
|
304
|
-
// --- Person notes ---
|
|
305
|
-
listPersonNotes(db, personId) {
|
|
306
|
-
return db
|
|
307
|
-
.select()
|
|
308
|
-
.from(personNotes)
|
|
309
|
-
.where(eq(personNotes.personId, personId))
|
|
310
|
-
.orderBy(personNotes.createdAt);
|
|
311
|
-
},
|
|
312
|
-
async createPersonNote(db, personId, userId, data) {
|
|
313
|
-
const [existing] = await db
|
|
314
|
-
.select({ id: people.id })
|
|
315
|
-
.from(people)
|
|
316
|
-
.where(eq(people.id, personId))
|
|
317
|
-
.limit(1);
|
|
318
|
-
if (!existing)
|
|
319
|
-
return null;
|
|
320
|
-
const [row] = await db
|
|
321
|
-
.insert(personNotes)
|
|
322
|
-
.values({ personId, authorId: userId, content: data.content })
|
|
323
|
-
.returning();
|
|
324
|
-
return row;
|
|
325
|
-
},
|
|
326
|
-
// --- Organization notes ---
|
|
327
|
-
listOrganizationNotes(db, organizationId) {
|
|
328
|
-
return db
|
|
329
|
-
.select()
|
|
330
|
-
.from(organizationNotes)
|
|
331
|
-
.where(eq(organizationNotes.organizationId, organizationId))
|
|
332
|
-
.orderBy(organizationNotes.createdAt);
|
|
333
|
-
},
|
|
334
|
-
async createOrganizationNote(db, organizationId, userId, data) {
|
|
335
|
-
const [existing] = await db
|
|
336
|
-
.select({ id: organizations.id })
|
|
337
|
-
.from(organizations)
|
|
338
|
-
.where(eq(organizations.id, organizationId))
|
|
339
|
-
.limit(1);
|
|
340
|
-
if (!existing)
|
|
341
|
-
return null;
|
|
342
|
-
const [row] = await db
|
|
343
|
-
.insert(organizationNotes)
|
|
344
|
-
.values({ organizationId, authorId: userId, content: data.content })
|
|
345
|
-
.returning();
|
|
346
|
-
return row;
|
|
347
|
-
},
|
|
348
|
-
async updatePersonNote(db, id, content) {
|
|
349
|
-
const [row] = await db
|
|
350
|
-
.update(personNotes)
|
|
351
|
-
.set({ content })
|
|
352
|
-
.where(eq(personNotes.id, id))
|
|
353
|
-
.returning();
|
|
354
|
-
return row ?? null;
|
|
355
|
-
},
|
|
356
|
-
async deletePersonNote(db, id) {
|
|
357
|
-
const [row] = await db.delete(personNotes).where(eq(personNotes.id, id)).returning();
|
|
358
|
-
return row ?? null;
|
|
359
|
-
},
|
|
360
|
-
async updateOrganizationNote(db, id, content) {
|
|
361
|
-
const [row] = await db
|
|
362
|
-
.update(organizationNotes)
|
|
363
|
-
.set({ content })
|
|
364
|
-
.where(eq(organizationNotes.id, id))
|
|
365
|
-
.returning();
|
|
366
|
-
return row ?? null;
|
|
367
|
-
},
|
|
368
|
-
async deleteOrganizationNote(db, id) {
|
|
369
|
-
const [row] = await db.delete(organizationNotes).where(eq(organizationNotes.id, id)).returning();
|
|
370
|
-
return row ?? null;
|
|
371
|
-
},
|
|
372
|
-
// --- Communication log ---
|
|
373
|
-
async listCommunications(db, personId, query) {
|
|
374
|
-
const conditions = [eq(communicationLog.personId, personId)];
|
|
375
|
-
if (query.channel)
|
|
376
|
-
conditions.push(eq(communicationLog.channel, query.channel));
|
|
377
|
-
if (query.direction)
|
|
378
|
-
conditions.push(eq(communicationLog.direction, query.direction));
|
|
379
|
-
if (query.dateFrom)
|
|
380
|
-
conditions.push(gte(communicationLog.createdAt, new Date(query.dateFrom)));
|
|
381
|
-
if (query.dateTo)
|
|
382
|
-
conditions.push(lte(communicationLog.createdAt, new Date(query.dateTo)));
|
|
383
|
-
return db
|
|
384
|
-
.select()
|
|
385
|
-
.from(communicationLog)
|
|
386
|
-
.where(and(...conditions))
|
|
387
|
-
.limit(query.limit)
|
|
388
|
-
.offset(query.offset)
|
|
389
|
-
.orderBy(desc(communicationLog.createdAt));
|
|
390
|
-
},
|
|
391
|
-
async createCommunication(db, personId, data) {
|
|
392
|
-
const [existing] = await db
|
|
393
|
-
.select({ id: people.id })
|
|
394
|
-
.from(people)
|
|
395
|
-
.where(eq(people.id, personId))
|
|
396
|
-
.limit(1);
|
|
397
|
-
if (!existing)
|
|
398
|
-
return null;
|
|
399
|
-
const [row] = await db
|
|
400
|
-
.insert(communicationLog)
|
|
401
|
-
.values({
|
|
402
|
-
personId,
|
|
403
|
-
organizationId: data.organizationId ?? null,
|
|
404
|
-
channel: data.channel,
|
|
405
|
-
direction: data.direction,
|
|
406
|
-
subject: data.subject ?? null,
|
|
407
|
-
content: data.content ?? null,
|
|
408
|
-
sentAt: data.sentAt ? new Date(data.sentAt) : null,
|
|
409
|
-
})
|
|
410
|
-
.returning();
|
|
411
|
-
return row;
|
|
412
|
-
},
|
|
413
|
-
// --- Segments ---
|
|
414
|
-
listSegments(db) {
|
|
415
|
-
return db.select().from(segments).orderBy(segments.createdAt);
|
|
416
|
-
},
|
|
417
|
-
async createSegment(db, data) {
|
|
418
|
-
const [row] = await db.insert(segments).values(data).returning();
|
|
419
|
-
return row;
|
|
420
|
-
},
|
|
421
|
-
async deleteSegment(db, segmentId) {
|
|
422
|
-
const [row] = await db
|
|
423
|
-
.delete(segments)
|
|
424
|
-
.where(eq(segments.id, segmentId))
|
|
425
|
-
.returning({ id: segments.id });
|
|
426
|
-
return row ?? null;
|
|
427
|
-
},
|
|
428
|
-
// --- CSV export/import ---
|
|
429
|
-
async exportPeopleCsv(db) {
|
|
430
|
-
const rows = await hydratePeople(db, await db.select().from(people).orderBy(people.createdAt));
|
|
431
|
-
const headers = [
|
|
432
|
-
"id",
|
|
433
|
-
"firstName",
|
|
434
|
-
"lastName",
|
|
435
|
-
"jobTitle",
|
|
436
|
-
"relation",
|
|
437
|
-
"preferredLanguage",
|
|
438
|
-
"preferredCurrency",
|
|
439
|
-
"email",
|
|
440
|
-
"phone",
|
|
441
|
-
"website",
|
|
442
|
-
"address",
|
|
443
|
-
"city",
|
|
444
|
-
"country",
|
|
445
|
-
"organizationId",
|
|
446
|
-
];
|
|
447
|
-
const csvLines = [headers.join(",")];
|
|
448
|
-
for (const row of rows) {
|
|
449
|
-
const values = headers.map((header) => {
|
|
450
|
-
const value = row[header];
|
|
451
|
-
if (value === null || value === undefined)
|
|
452
|
-
return "";
|
|
453
|
-
const stringValue = String(value);
|
|
454
|
-
return stringValue.includes(",") || stringValue.includes('"') || stringValue.includes("\n")
|
|
455
|
-
? `"${stringValue.replace(/"/g, '""')}"`
|
|
456
|
-
: stringValue;
|
|
457
|
-
});
|
|
458
|
-
csvLines.push(values.join(","));
|
|
459
|
-
}
|
|
460
|
-
return csvLines.join("\n");
|
|
461
|
-
},
|
|
462
|
-
async importPeopleCsv(db, csvText) {
|
|
463
|
-
const lines = csvText.split("\n").filter((line) => line.trim());
|
|
464
|
-
if (lines.length < 2) {
|
|
465
|
-
return { error: "CSV must have a header row and at least one data row" };
|
|
466
|
-
}
|
|
467
|
-
const headers = lines[0].split(",").map((header) => header.trim());
|
|
468
|
-
const rows = [];
|
|
469
|
-
for (let i = 1; i < lines.length; i++) {
|
|
470
|
-
const values = lines[i].split(",").map((value) => value.trim());
|
|
471
|
-
const row = {};
|
|
472
|
-
for (let j = 0; j < headers.length; j++) {
|
|
473
|
-
const header = headers[j];
|
|
474
|
-
const value = values[j];
|
|
475
|
-
if (header && value) {
|
|
476
|
-
row[header] = value;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
rows.push(row);
|
|
480
|
-
}
|
|
481
|
-
const imported = [];
|
|
482
|
-
const errors = [];
|
|
483
|
-
for (let i = 0; i < rows.length; i++) {
|
|
484
|
-
const row = rows[i];
|
|
485
|
-
const result = (await import("../validation.js")).insertPersonSchema.safeParse({
|
|
486
|
-
firstName: row.firstName || "",
|
|
487
|
-
lastName: row.lastName || "",
|
|
488
|
-
jobTitle: row.jobTitle || null,
|
|
489
|
-
relation: row.relation || null,
|
|
490
|
-
preferredLanguage: row.preferredLanguage || null,
|
|
491
|
-
preferredCurrency: row.preferredCurrency || null,
|
|
492
|
-
email: row.email || null,
|
|
493
|
-
phone: row.phone || null,
|
|
494
|
-
website: row.website || null,
|
|
495
|
-
address: row.address || null,
|
|
496
|
-
city: row.city || null,
|
|
497
|
-
country: row.country || null,
|
|
498
|
-
organizationId: row.organizationId || null,
|
|
499
|
-
tags: [],
|
|
500
|
-
});
|
|
501
|
-
if (!result.success) {
|
|
502
|
-
errors.push({ row: i + 2, error: result.error.message });
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
imported.push(await this.createPerson(db, result.data));
|
|
506
|
-
}
|
|
507
|
-
return { imported: imported.length, errors };
|
|
508
|
-
},
|
|
4
|
+
...organizationAccountsService,
|
|
5
|
+
...peopleAccountsService,
|
|
509
6
|
};
|