@hed-hog/contact 0.0.294 → 0.0.295
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/person/dto/account.dto.d.ts +28 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -0
- package/dist/person/dto/account.dto.js +123 -0
- package/dist/person/dto/account.dto.js.map +1 -0
- package/dist/person/dto/activity.dto.d.ts +15 -0
- package/dist/person/dto/activity.dto.d.ts.map +1 -0
- package/dist/person/dto/activity.dto.js +65 -0
- package/dist/person/dto/activity.dto.js.map +1 -0
- package/dist/person/dto/dashboard-query.dto.d.ts +9 -0
- package/dist/person/dto/dashboard-query.dto.d.ts.map +1 -0
- package/dist/person/dto/dashboard-query.dto.js +40 -0
- package/dist/person/dto/dashboard-query.dto.js.map +1 -0
- package/dist/person/dto/followup-query.dto.d.ts +10 -0
- package/dist/person/dto/followup-query.dto.d.ts.map +1 -0
- package/dist/person/dto/followup-query.dto.js +45 -0
- package/dist/person/dto/followup-query.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +204 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +138 -0
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +234 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +1367 -0
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/data/menu.yaml +163 -163
- package/hedhog/data/route.yaml +41 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +210 -114
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +3 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +323 -245
- package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -0
- package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -0
- package/hedhog/frontend/app/activities/page.tsx.ejs +165 -517
- package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +504 -356
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +242 -153
- package/hedhog/frontend/messages/en.json +91 -6
- package/hedhog/frontend/messages/pt.json +91 -6
- package/hedhog/table/crm_activity.yaml +68 -0
- package/hedhog/table/person_company.yaml +22 -0
- package/package.json +4 -4
- package/src/person/dto/account.dto.ts +100 -0
- package/src/person/dto/activity.dto.ts +54 -0
- package/src/person/dto/dashboard-query.dto.ts +25 -0
- package/src/person/dto/followup-query.dto.ts +25 -0
- package/src/person/person.controller.ts +116 -0
- package/src/person/person.service.ts +2139 -77
|
@@ -18,6 +18,7 @@ const api_pagination_1 = require("@hed-hog/api-pagination");
|
|
|
18
18
|
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
19
19
|
const core_1 = require("@hed-hog/core");
|
|
20
20
|
const common_1 = require("@nestjs/common");
|
|
21
|
+
const account_dto_1 = require("./dto/account.dto");
|
|
21
22
|
const create_interaction_dto_1 = require("./dto/create-interaction.dto");
|
|
22
23
|
const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING = 'contact-allow-company-registration';
|
|
23
24
|
const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
|
|
@@ -38,6 +39,23 @@ const DEAL_VALUE_METADATA_KEY = 'deal_value';
|
|
|
38
39
|
const TAGS_METADATA_KEY = 'tags';
|
|
39
40
|
const LAST_INTERACTION_AT_METADATA_KEY = 'last_interaction_at';
|
|
40
41
|
const INTERACTIONS_METADATA_KEY = 'interactions';
|
|
42
|
+
const CRM_DASHBOARD_STAGE_ORDER = [
|
|
43
|
+
'new',
|
|
44
|
+
'contacted',
|
|
45
|
+
'qualified',
|
|
46
|
+
'proposal',
|
|
47
|
+
'negotiation',
|
|
48
|
+
'customer',
|
|
49
|
+
'lost',
|
|
50
|
+
];
|
|
51
|
+
const CRM_DASHBOARD_SOURCE_ORDER = [
|
|
52
|
+
'website',
|
|
53
|
+
'referral',
|
|
54
|
+
'social',
|
|
55
|
+
'inbound',
|
|
56
|
+
'outbound',
|
|
57
|
+
'other',
|
|
58
|
+
];
|
|
41
59
|
let PersonService = class PersonService {
|
|
42
60
|
constructor(prismaService, paginationService, fileService, settingService) {
|
|
43
61
|
this.prismaService = prismaService;
|
|
@@ -80,6 +98,105 @@ let PersonService = class PersonService {
|
|
|
80
98
|
inactive,
|
|
81
99
|
};
|
|
82
100
|
}
|
|
101
|
+
async getDashboard(query, locale) {
|
|
102
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
103
|
+
const ownerUserId = this.coerceNumber(query.owner_user_id);
|
|
104
|
+
const ranges = this.resolveDashboardRanges(query, locale);
|
|
105
|
+
const where = {};
|
|
106
|
+
if (!allowCompanyRegistration) {
|
|
107
|
+
where.type = 'individual';
|
|
108
|
+
}
|
|
109
|
+
if (ownerUserId > 0) {
|
|
110
|
+
where.AND = [
|
|
111
|
+
{
|
|
112
|
+
person_metadata: {
|
|
113
|
+
some: {
|
|
114
|
+
key: OWNER_USER_METADATA_KEY,
|
|
115
|
+
value: ownerUserId,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
const people = await this.prismaService.person.findMany({
|
|
122
|
+
where,
|
|
123
|
+
select: {
|
|
124
|
+
id: true,
|
|
125
|
+
name: true,
|
|
126
|
+
type: true,
|
|
127
|
+
status: true,
|
|
128
|
+
avatar_id: true,
|
|
129
|
+
created_at: true,
|
|
130
|
+
updated_at: true,
|
|
131
|
+
person_metadata: true,
|
|
132
|
+
},
|
|
133
|
+
orderBy: {
|
|
134
|
+
created_at: 'desc',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const enriched = await this.enrichPeople(people, allowCompanyRegistration);
|
|
138
|
+
const createdScoped = enriched.filter((person) => this.isDateWithinRange(person.created_at, ranges.created));
|
|
139
|
+
const nextActionScoped = enriched.filter((person) => !!person.next_action_at &&
|
|
140
|
+
this.isDateWithinRange(person.next_action_at, ranges.operational));
|
|
141
|
+
const kpis = {
|
|
142
|
+
total_leads: createdScoped.length,
|
|
143
|
+
qualified: createdScoped.filter((person) => {
|
|
144
|
+
var _a;
|
|
145
|
+
return ['qualified', 'proposal', 'negotiation', 'customer'].includes((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new');
|
|
146
|
+
}).length,
|
|
147
|
+
proposal: createdScoped.filter((person) => person.lifecycle_stage === 'proposal').length,
|
|
148
|
+
customers: createdScoped.filter((person) => person.lifecycle_stage === 'customer').length,
|
|
149
|
+
lost: createdScoped.filter((person) => person.lifecycle_stage === 'lost')
|
|
150
|
+
.length,
|
|
151
|
+
unassigned: createdScoped.filter((person) => !person.owner_user_id).length,
|
|
152
|
+
overdue: nextActionScoped.filter((person) => {
|
|
153
|
+
const nextActionAt = this.parseDateOrNull(person.next_action_at);
|
|
154
|
+
return !!nextActionAt && nextActionAt.getTime() < Date.now();
|
|
155
|
+
}).length,
|
|
156
|
+
next_actions: nextActionScoped.length,
|
|
157
|
+
};
|
|
158
|
+
const charts = {
|
|
159
|
+
stage: CRM_DASHBOARD_STAGE_ORDER.map((key) => ({
|
|
160
|
+
key,
|
|
161
|
+
total: createdScoped.filter((person) => { var _a; return ((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new') === key; }).length,
|
|
162
|
+
})),
|
|
163
|
+
source: CRM_DASHBOARD_SOURCE_ORDER.map((key) => ({
|
|
164
|
+
key,
|
|
165
|
+
total: createdScoped.filter((person) => { var _a; return ((_a = person.source) !== null && _a !== void 0 ? _a : 'other') === key; }).length,
|
|
166
|
+
})),
|
|
167
|
+
owner_performance: this.buildDashboardOwnerPerformance(createdScoped),
|
|
168
|
+
};
|
|
169
|
+
const lists = {
|
|
170
|
+
next_actions: [...nextActionScoped]
|
|
171
|
+
.sort((left, right) => {
|
|
172
|
+
var _a, _b;
|
|
173
|
+
return new Date((_a = left.next_action_at) !== null && _a !== void 0 ? _a : 0).getTime() -
|
|
174
|
+
new Date((_b = right.next_action_at) !== null && _b !== void 0 ? _b : 0).getTime();
|
|
175
|
+
})
|
|
176
|
+
.slice(0, 5)
|
|
177
|
+
.map((person) => this.mapDashboardListItem(person, {
|
|
178
|
+
includeCreatedAt: false,
|
|
179
|
+
includeNextActionAt: true,
|
|
180
|
+
})),
|
|
181
|
+
unattended: [...createdScoped]
|
|
182
|
+
.filter((person) => { var _a; return !person.owner_user_id || ((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new') === 'new'; })
|
|
183
|
+
.sort((left, right) => {
|
|
184
|
+
var _a, _b;
|
|
185
|
+
return new Date((_a = right.created_at) !== null && _a !== void 0 ? _a : 0).getTime() -
|
|
186
|
+
new Date((_b = left.created_at) !== null && _b !== void 0 ? _b : 0).getTime();
|
|
187
|
+
})
|
|
188
|
+
.slice(0, 5)
|
|
189
|
+
.map((person) => this.mapDashboardListItem(person, {
|
|
190
|
+
includeCreatedAt: true,
|
|
191
|
+
includeNextActionAt: false,
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
kpis,
|
|
196
|
+
charts,
|
|
197
|
+
lists,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
83
200
|
async getOwnerOptions(currentUserId) {
|
|
84
201
|
const where = {
|
|
85
202
|
OR: [
|
|
@@ -347,6 +464,517 @@ let PersonService = class PersonService {
|
|
|
347
464
|
const enriched = await this.enrichPeople(result.data, allowCompanyRegistration);
|
|
348
465
|
return Object.assign(Object.assign({}, result), { data: enriched });
|
|
349
466
|
}
|
|
467
|
+
async listAccounts(paginationParams) {
|
|
468
|
+
var _a;
|
|
469
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
470
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
471
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
472
|
+
const skip = (page - 1) * pageSize;
|
|
473
|
+
if (!allowCompanyRegistration) {
|
|
474
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
475
|
+
}
|
|
476
|
+
const search = this.normalizeTextOrNull(paginationParams.search);
|
|
477
|
+
const filters = this.buildAccountSqlFilters({
|
|
478
|
+
search,
|
|
479
|
+
status: paginationParams.status,
|
|
480
|
+
lifecycleStage: paginationParams.lifecycle_stage,
|
|
481
|
+
});
|
|
482
|
+
const orderBy = this.getAccountOrderBySql(paginationParams.sortField, paginationParams.sortOrder);
|
|
483
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
484
|
+
SELECT COUNT(*) AS total
|
|
485
|
+
FROM person p
|
|
486
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
487
|
+
WHERE p.type = 'company'
|
|
488
|
+
${filters}
|
|
489
|
+
`);
|
|
490
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
491
|
+
if (total === 0) {
|
|
492
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
493
|
+
}
|
|
494
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
495
|
+
SELECT p.id AS person_id
|
|
496
|
+
FROM person p
|
|
497
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
498
|
+
WHERE p.type = 'company'
|
|
499
|
+
${filters}
|
|
500
|
+
ORDER BY ${orderBy}, p.id ASC
|
|
501
|
+
LIMIT ${pageSize}
|
|
502
|
+
OFFSET ${skip}
|
|
503
|
+
`);
|
|
504
|
+
const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
|
|
505
|
+
if (personIds.length === 0) {
|
|
506
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
507
|
+
}
|
|
508
|
+
const people = await this.loadAccountPeopleByIds(personIds);
|
|
509
|
+
const companies = await this.prismaService.person_company.findMany({
|
|
510
|
+
where: {
|
|
511
|
+
id: {
|
|
512
|
+
in: personIds,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
const personById = new Map(people.map((item) => [item.id, item]));
|
|
517
|
+
const companyById = new Map(companies.map((item) => [item.id, item]));
|
|
518
|
+
const data = rows
|
|
519
|
+
.map((row) => {
|
|
520
|
+
const person = personById.get(row.person_id);
|
|
521
|
+
const company = companyById.get(row.person_id);
|
|
522
|
+
if (!person || !company) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
return this.mapAccountFromPerson(person, company);
|
|
526
|
+
})
|
|
527
|
+
.filter((item) => item != null);
|
|
528
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
529
|
+
return {
|
|
530
|
+
total,
|
|
531
|
+
lastPage,
|
|
532
|
+
page,
|
|
533
|
+
pageSize,
|
|
534
|
+
prev: page > 1 ? page - 1 : null,
|
|
535
|
+
next: page < lastPage ? page + 1 : null,
|
|
536
|
+
data,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async getAccountStats() {
|
|
540
|
+
var _a, _b, _c, _d;
|
|
541
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
542
|
+
if (!allowCompanyRegistration) {
|
|
543
|
+
return {
|
|
544
|
+
total: 0,
|
|
545
|
+
active: 0,
|
|
546
|
+
customers: 0,
|
|
547
|
+
prospects: 0,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
551
|
+
SELECT
|
|
552
|
+
COUNT(*) AS total,
|
|
553
|
+
COUNT(*) FILTER (WHERE p.status = 'active') AS active,
|
|
554
|
+
COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'customer') AS customers,
|
|
555
|
+
COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'prospect') AS prospects
|
|
556
|
+
FROM person p
|
|
557
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
558
|
+
WHERE p.type = 'company'
|
|
559
|
+
`);
|
|
560
|
+
return {
|
|
561
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
562
|
+
active: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.active),
|
|
563
|
+
customers: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.customers),
|
|
564
|
+
prospects: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.prospects),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
async createAccount(data, locale) {
|
|
568
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
569
|
+
nextType: 'company',
|
|
570
|
+
locale,
|
|
571
|
+
});
|
|
572
|
+
const normalizedName = this.normalizeTextOrNull(data.name);
|
|
573
|
+
if (!normalizedName) {
|
|
574
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.nameMustBeString', locale, 'Name is required.'));
|
|
575
|
+
}
|
|
576
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
577
|
+
const person = await tx.person.create({
|
|
578
|
+
data: {
|
|
579
|
+
name: normalizedName,
|
|
580
|
+
type: 'company',
|
|
581
|
+
status: data.status,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
await this.syncPersonSubtypeData(tx, person.id, null, Object.assign(Object.assign({}, data), { type: 'company', name: normalizedName }), locale);
|
|
585
|
+
await this.syncPersonMetadata(tx, person.id, {
|
|
586
|
+
owner_user_id: data.owner_user_id,
|
|
587
|
+
});
|
|
588
|
+
await this.upsertPrimaryAccountContact(tx, person.id, 'EMAIL', data.email);
|
|
589
|
+
await this.upsertPrimaryAccountContact(tx, person.id, 'PHONE', data.phone);
|
|
590
|
+
return {
|
|
591
|
+
success: true,
|
|
592
|
+
id: person.id,
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async updateAccount(id, data, locale) {
|
|
597
|
+
var _a;
|
|
598
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
599
|
+
currentType: 'company',
|
|
600
|
+
nextType: 'company',
|
|
601
|
+
locale,
|
|
602
|
+
});
|
|
603
|
+
const person = await this.ensureCompanyAccountAccessible(id, locale);
|
|
604
|
+
const nextName = (_a = this.normalizeTextOrNull(data.name)) !== null && _a !== void 0 ? _a : person.name;
|
|
605
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
606
|
+
var _a;
|
|
607
|
+
await tx.person.update({
|
|
608
|
+
where: { id },
|
|
609
|
+
data: {
|
|
610
|
+
name: nextName,
|
|
611
|
+
type: 'company',
|
|
612
|
+
status: (_a = data.status) !== null && _a !== void 0 ? _a : person.status,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
await this.syncPersonSubtypeData(tx, id, 'company', Object.assign(Object.assign({}, data), { type: 'company', name: nextName }), locale);
|
|
616
|
+
await this.syncPersonMetadata(tx, id, {
|
|
617
|
+
owner_user_id: data.owner_user_id,
|
|
618
|
+
});
|
|
619
|
+
await this.upsertPrimaryAccountContact(tx, id, 'EMAIL', data.email);
|
|
620
|
+
await this.upsertPrimaryAccountContact(tx, id, 'PHONE', data.phone);
|
|
621
|
+
return {
|
|
622
|
+
success: true,
|
|
623
|
+
id,
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async deleteAccounts({ ids }, locale) {
|
|
628
|
+
if (ids == undefined || ids == null) {
|
|
629
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('deleteItemsRequired', locale, 'You must select at least one item to delete.'));
|
|
630
|
+
}
|
|
631
|
+
const companies = await this.prismaService.person.findMany({
|
|
632
|
+
where: {
|
|
633
|
+
id: {
|
|
634
|
+
in: ids,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
select: {
|
|
638
|
+
id: true,
|
|
639
|
+
type: true,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
const existingIds = companies.map((item) => item.id);
|
|
643
|
+
const missingIds = ids.filter((itemId) => !existingIds.includes(itemId));
|
|
644
|
+
if (missingIds.length > 0) {
|
|
645
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person(s) with ID(s) ${missingIds.join(', ')} not found.`));
|
|
646
|
+
}
|
|
647
|
+
const invalidIds = companies
|
|
648
|
+
.filter((item) => item.type !== 'company')
|
|
649
|
+
.map((item) => item.id);
|
|
650
|
+
if (invalidIds.length > 0) {
|
|
651
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRelationInvalidTarget', locale, 'Only company records can be managed as accounts.'));
|
|
652
|
+
}
|
|
653
|
+
return this.delete({ ids }, locale);
|
|
654
|
+
}
|
|
655
|
+
async listActivities(paginationParams) {
|
|
656
|
+
var _a;
|
|
657
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
658
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
659
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
660
|
+
const skip = (page - 1) * pageSize;
|
|
661
|
+
const filters = this.buildCrmActivitySqlFilters({
|
|
662
|
+
allowCompanyRegistration,
|
|
663
|
+
search: this.normalizeTextOrNull(paginationParams.search),
|
|
664
|
+
status: paginationParams.status,
|
|
665
|
+
type: paginationParams.type,
|
|
666
|
+
priority: paginationParams.priority,
|
|
667
|
+
});
|
|
668
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
669
|
+
SELECT COUNT(*) AS total
|
|
670
|
+
FROM crm_activity a
|
|
671
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
672
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
673
|
+
WHERE 1 = 1
|
|
674
|
+
${filters}
|
|
675
|
+
`);
|
|
676
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
677
|
+
if (total === 0) {
|
|
678
|
+
return this.createEmptyCrmActivityPagination(page, pageSize);
|
|
679
|
+
}
|
|
680
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
681
|
+
SELECT
|
|
682
|
+
a.id,
|
|
683
|
+
a.person_id,
|
|
684
|
+
a.owner_user_id,
|
|
685
|
+
a.type,
|
|
686
|
+
a.subject,
|
|
687
|
+
a.notes,
|
|
688
|
+
a.due_at,
|
|
689
|
+
a.completed_at,
|
|
690
|
+
a.created_at,
|
|
691
|
+
a.priority,
|
|
692
|
+
p.name AS person_name,
|
|
693
|
+
p.type AS person_type,
|
|
694
|
+
p.status AS person_status,
|
|
695
|
+
pc.trade_name AS person_trade_name,
|
|
696
|
+
owner_user.name AS owner_user_name
|
|
697
|
+
FROM crm_activity a
|
|
698
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
699
|
+
LEFT JOIN person_company pc ON pc.id = p.id
|
|
700
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
701
|
+
WHERE 1 = 1
|
|
702
|
+
${filters}
|
|
703
|
+
ORDER BY a.due_at ASC, a.id ASC
|
|
704
|
+
LIMIT ${pageSize}
|
|
705
|
+
OFFSET ${skip}
|
|
706
|
+
`);
|
|
707
|
+
const data = rows.map((row) => this.mapCrmActivityListRow(row));
|
|
708
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
709
|
+
return {
|
|
710
|
+
total,
|
|
711
|
+
lastPage,
|
|
712
|
+
page,
|
|
713
|
+
pageSize,
|
|
714
|
+
prev: page > 1 ? page - 1 : null,
|
|
715
|
+
next: page < lastPage ? page + 1 : null,
|
|
716
|
+
data,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async getActivityStats() {
|
|
720
|
+
var _a, _b, _c, _d;
|
|
721
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
722
|
+
const visibilityFilter = allowCompanyRegistration
|
|
723
|
+
? api_prisma_1.Prisma.empty
|
|
724
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
725
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
726
|
+
SELECT
|
|
727
|
+
COUNT(*) AS total,
|
|
728
|
+
COUNT(*) FILTER (
|
|
729
|
+
WHERE a.completed_at IS NULL
|
|
730
|
+
AND a.due_at >= NOW()
|
|
731
|
+
) AS pending,
|
|
732
|
+
COUNT(*) FILTER (
|
|
733
|
+
WHERE a.completed_at IS NULL
|
|
734
|
+
AND a.due_at < NOW()
|
|
735
|
+
) AS overdue,
|
|
736
|
+
COUNT(*) FILTER (
|
|
737
|
+
WHERE a.completed_at IS NOT NULL
|
|
738
|
+
) AS completed
|
|
739
|
+
FROM crm_activity a
|
|
740
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
741
|
+
WHERE 1 = 1
|
|
742
|
+
${visibilityFilter}
|
|
743
|
+
`);
|
|
744
|
+
return {
|
|
745
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
746
|
+
pending: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.pending),
|
|
747
|
+
overdue: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.overdue),
|
|
748
|
+
completed: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.completed),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async getActivity(id, locale) {
|
|
752
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
753
|
+
const visibilityFilter = allowCompanyRegistration
|
|
754
|
+
? api_prisma_1.Prisma.empty
|
|
755
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
756
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
757
|
+
SELECT
|
|
758
|
+
a.id,
|
|
759
|
+
a.person_id,
|
|
760
|
+
a.owner_user_id,
|
|
761
|
+
a.created_by_user_id,
|
|
762
|
+
a.completed_by_user_id,
|
|
763
|
+
a.type,
|
|
764
|
+
a.subject,
|
|
765
|
+
a.notes,
|
|
766
|
+
a.due_at,
|
|
767
|
+
a.completed_at,
|
|
768
|
+
a.created_at,
|
|
769
|
+
a.priority,
|
|
770
|
+
a.source_kind,
|
|
771
|
+
p.name AS person_name,
|
|
772
|
+
p.type AS person_type,
|
|
773
|
+
p.status AS person_status,
|
|
774
|
+
pc.trade_name AS person_trade_name,
|
|
775
|
+
owner_user.name AS owner_user_name,
|
|
776
|
+
created_by_user.name AS created_by_user_name,
|
|
777
|
+
completed_by_user.name AS completed_by_user_name
|
|
778
|
+
FROM crm_activity a
|
|
779
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
780
|
+
LEFT JOIN person_company pc ON pc.id = p.id
|
|
781
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
782
|
+
LEFT JOIN "user" created_by_user ON created_by_user.id = a.created_by_user_id
|
|
783
|
+
LEFT JOIN "user" completed_by_user ON completed_by_user.id = a.completed_by_user_id
|
|
784
|
+
WHERE a.id = ${id}
|
|
785
|
+
${visibilityFilter}
|
|
786
|
+
LIMIT 1
|
|
787
|
+
`);
|
|
788
|
+
const row = rows[0];
|
|
789
|
+
if (!row) {
|
|
790
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Activity with ID ${id} not found`));
|
|
791
|
+
}
|
|
792
|
+
return this.mapCrmActivityDetailRow(row);
|
|
793
|
+
}
|
|
794
|
+
async completeActivity(id, locale, user) {
|
|
795
|
+
const actorUserId = Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null;
|
|
796
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
797
|
+
const visibilityFilter = allowCompanyRegistration
|
|
798
|
+
? api_prisma_1.Prisma.empty
|
|
799
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
800
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
801
|
+
const rows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
|
|
802
|
+
SELECT
|
|
803
|
+
a.id,
|
|
804
|
+
a.person_id,
|
|
805
|
+
a.completed_at,
|
|
806
|
+
a.source_kind
|
|
807
|
+
FROM crm_activity a
|
|
808
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
809
|
+
WHERE a.id = ${id}
|
|
810
|
+
${visibilityFilter}
|
|
811
|
+
LIMIT 1
|
|
812
|
+
`));
|
|
813
|
+
const activity = rows[0];
|
|
814
|
+
if (!activity) {
|
|
815
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Activity with ID ${id} not found`));
|
|
816
|
+
}
|
|
817
|
+
if (activity.completed_at) {
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
completed_at: activity.completed_at.toISOString(),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const completedAt = new Date();
|
|
824
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
825
|
+
UPDATE crm_activity
|
|
826
|
+
SET
|
|
827
|
+
completed_at = CAST(${completedAt.toISOString()} AS TIMESTAMPTZ),
|
|
828
|
+
completed_by_user_id = ${actorUserId},
|
|
829
|
+
updated_at = NOW()
|
|
830
|
+
WHERE id = ${id}
|
|
831
|
+
`);
|
|
832
|
+
if (activity.source_kind === 'followup') {
|
|
833
|
+
await this.upsertMetadataValue(tx, activity.person_id, NEXT_ACTION_AT_METADATA_KEY, null);
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
success: true,
|
|
837
|
+
completed_at: completedAt.toISOString(),
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
async listFollowups(paginationParams, _currentUserId) {
|
|
842
|
+
var _a;
|
|
843
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
844
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
845
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
846
|
+
const skip = (page - 1) * pageSize;
|
|
847
|
+
const searchPersonIds = await this.findFollowupSearchPersonIds(paginationParams.search, allowCompanyRegistration);
|
|
848
|
+
if (searchPersonIds && searchPersonIds.length === 0) {
|
|
849
|
+
return this.createEmptyFollowupPagination(page, pageSize);
|
|
850
|
+
}
|
|
851
|
+
const filters = this.buildFollowupSqlFilters({
|
|
852
|
+
allowCompanyRegistration,
|
|
853
|
+
searchPersonIds,
|
|
854
|
+
status: paginationParams.status,
|
|
855
|
+
dateFrom: paginationParams.date_from,
|
|
856
|
+
dateTo: paginationParams.date_to,
|
|
857
|
+
});
|
|
858
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
859
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
860
|
+
SELECT COUNT(*) AS total
|
|
861
|
+
FROM person p
|
|
862
|
+
INNER JOIN person_metadata pm_next
|
|
863
|
+
ON pm_next.person_id = p.id
|
|
864
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
865
|
+
WHERE 1 = 1
|
|
866
|
+
${filters}
|
|
867
|
+
`);
|
|
868
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
869
|
+
if (total === 0) {
|
|
870
|
+
return this.createEmptyFollowupPagination(page, pageSize);
|
|
871
|
+
}
|
|
872
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
873
|
+
SELECT p.id AS person_id
|
|
874
|
+
FROM person p
|
|
875
|
+
INNER JOIN person_metadata pm_next
|
|
876
|
+
ON pm_next.person_id = p.id
|
|
877
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
878
|
+
WHERE 1 = 1
|
|
879
|
+
${filters}
|
|
880
|
+
ORDER BY ${followupTimestampSql} ASC, p.id ASC
|
|
881
|
+
LIMIT ${pageSize}
|
|
882
|
+
OFFSET ${skip}
|
|
883
|
+
`);
|
|
884
|
+
const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
|
|
885
|
+
const people = personIds.length > 0
|
|
886
|
+
? await this.prismaService.person.findMany({
|
|
887
|
+
where: {
|
|
888
|
+
id: {
|
|
889
|
+
in: personIds,
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
include: {
|
|
893
|
+
person_address: {
|
|
894
|
+
include: {
|
|
895
|
+
address: true,
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
contact: true,
|
|
899
|
+
document: true,
|
|
900
|
+
person_metadata: true,
|
|
901
|
+
},
|
|
902
|
+
})
|
|
903
|
+
: [];
|
|
904
|
+
const enrichedPeople = await this.enrichPeople(people, allowCompanyRegistration);
|
|
905
|
+
const personById = new Map(enrichedPeople.map((person) => [person.id, person]));
|
|
906
|
+
const data = rows
|
|
907
|
+
.map((row) => {
|
|
908
|
+
var _a;
|
|
909
|
+
const person = personById.get(row.person_id);
|
|
910
|
+
const nextActionAt = this.normalizeDateTimeOrNull(person === null || person === void 0 ? void 0 : person.next_action_at);
|
|
911
|
+
if (!person || !nextActionAt) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
person,
|
|
916
|
+
next_action_at: nextActionAt,
|
|
917
|
+
last_interaction_at: (_a = this.normalizeDateTimeOrNull(person.last_interaction_at)) !== null && _a !== void 0 ? _a : null,
|
|
918
|
+
status: this.getFollowupStatus(nextActionAt),
|
|
919
|
+
};
|
|
920
|
+
})
|
|
921
|
+
.filter((item) => item != null);
|
|
922
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
923
|
+
return {
|
|
924
|
+
total,
|
|
925
|
+
lastPage,
|
|
926
|
+
page,
|
|
927
|
+
pageSize,
|
|
928
|
+
prev: page > 1 ? page - 1 : null,
|
|
929
|
+
next: page < lastPage ? page + 1 : null,
|
|
930
|
+
data,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
async getFollowupStats(query, _currentUserId) {
|
|
934
|
+
var _a, _b, _c, _d;
|
|
935
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
936
|
+
const searchPersonIds = await this.findFollowupSearchPersonIds(query.search, allowCompanyRegistration);
|
|
937
|
+
if (searchPersonIds && searchPersonIds.length === 0) {
|
|
938
|
+
return {
|
|
939
|
+
total: 0,
|
|
940
|
+
today: 0,
|
|
941
|
+
overdue: 0,
|
|
942
|
+
upcoming: 0,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const filters = this.buildFollowupSqlFilters({
|
|
946
|
+
allowCompanyRegistration,
|
|
947
|
+
searchPersonIds,
|
|
948
|
+
});
|
|
949
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
950
|
+
const { todayStartIso, tomorrowStartIso } = this.getFollowupDayBoundaryIsoStrings();
|
|
951
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
952
|
+
SELECT
|
|
953
|
+
COUNT(*) AS total,
|
|
954
|
+
COUNT(*) FILTER (
|
|
955
|
+
WHERE ${followupTimestampSql} >= CAST(${todayStartIso} AS TIMESTAMPTZ)
|
|
956
|
+
AND ${followupTimestampSql} < CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
|
|
957
|
+
) AS today,
|
|
958
|
+
COUNT(*) FILTER (
|
|
959
|
+
WHERE ${followupTimestampSql} < CAST(${todayStartIso} AS TIMESTAMPTZ)
|
|
960
|
+
) AS overdue,
|
|
961
|
+
COUNT(*) FILTER (
|
|
962
|
+
WHERE ${followupTimestampSql} >= CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
|
|
963
|
+
) AS upcoming
|
|
964
|
+
FROM person p
|
|
965
|
+
INNER JOIN person_metadata pm_next
|
|
966
|
+
ON pm_next.person_id = p.id
|
|
967
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
968
|
+
WHERE 1 = 1
|
|
969
|
+
${filters}
|
|
970
|
+
`);
|
|
971
|
+
return {
|
|
972
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
973
|
+
today: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.today),
|
|
974
|
+
overdue: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.overdue),
|
|
975
|
+
upcoming: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.upcoming),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
350
978
|
async get(locale, id) {
|
|
351
979
|
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
352
980
|
const person = await this.prismaService.person.findUnique({
|
|
@@ -387,6 +1015,7 @@ let PersonService = class PersonService {
|
|
|
387
1015
|
async createInteraction(id, data, locale, user) {
|
|
388
1016
|
const person = await this.ensurePersonAccessible(id, locale);
|
|
389
1017
|
const interaction = this.buildInteractionRecord(data, user);
|
|
1018
|
+
const ownerUserId = await this.getPersonOwnerUserId(person.id);
|
|
390
1019
|
await this.prismaService.$transaction(async (tx) => {
|
|
391
1020
|
const currentInteractions = await this.loadInteractionsFromTx(tx, person.id);
|
|
392
1021
|
const nextInteractions = this.sortInteractions([
|
|
@@ -395,12 +1024,19 @@ let PersonService = class PersonService {
|
|
|
395
1024
|
]);
|
|
396
1025
|
await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
|
|
397
1026
|
await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
|
|
1027
|
+
await this.createCompletedInteractionActivity(tx, {
|
|
1028
|
+
personId: person.id,
|
|
1029
|
+
ownerUserId,
|
|
1030
|
+
interaction,
|
|
1031
|
+
actorUserId: Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null,
|
|
1032
|
+
});
|
|
398
1033
|
});
|
|
399
1034
|
return interaction;
|
|
400
1035
|
}
|
|
401
1036
|
async scheduleFollowup(id, data, locale, user) {
|
|
402
1037
|
const person = await this.ensurePersonAccessible(id, locale);
|
|
403
1038
|
const normalizedNextActionAt = this.normalizeDateTimeOrNull(data.next_action_at);
|
|
1039
|
+
const ownerUserId = await this.getPersonOwnerUserId(person.id);
|
|
404
1040
|
if (!normalizedNextActionAt) {
|
|
405
1041
|
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'next_action_at must be a valid datetime.'));
|
|
406
1042
|
}
|
|
@@ -420,6 +1056,13 @@ let PersonService = class PersonService {
|
|
|
420
1056
|
await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
|
|
421
1057
|
await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
|
|
422
1058
|
}
|
|
1059
|
+
await this.upsertFollowupActivity(tx, {
|
|
1060
|
+
personId: person.id,
|
|
1061
|
+
ownerUserId,
|
|
1062
|
+
dueAt: normalizedNextActionAt,
|
|
1063
|
+
notes: data.notes,
|
|
1064
|
+
actorUserId: Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null,
|
|
1065
|
+
});
|
|
423
1066
|
});
|
|
424
1067
|
return {
|
|
425
1068
|
success: true,
|
|
@@ -592,6 +1235,118 @@ let PersonService = class PersonService {
|
|
|
592
1235
|
}),
|
|
593
1236
|
]);
|
|
594
1237
|
}
|
|
1238
|
+
async findFollowupSearchPersonIds(search, allowCompanyRegistration) {
|
|
1239
|
+
const normalizedSearch = this.normalizeTextOrNull(search);
|
|
1240
|
+
if (!normalizedSearch) {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
const where = {
|
|
1244
|
+
OR: await this.buildSearchFilters(normalizedSearch),
|
|
1245
|
+
};
|
|
1246
|
+
if (!allowCompanyRegistration) {
|
|
1247
|
+
where.type = 'individual';
|
|
1248
|
+
}
|
|
1249
|
+
const people = await this.prismaService.person.findMany({
|
|
1250
|
+
where,
|
|
1251
|
+
select: {
|
|
1252
|
+
id: true,
|
|
1253
|
+
},
|
|
1254
|
+
});
|
|
1255
|
+
return people.map((person) => person.id).filter((id) => id > 0);
|
|
1256
|
+
}
|
|
1257
|
+
buildFollowupSqlFilters({ allowCompanyRegistration, searchPersonIds, status, dateFrom, dateTo, }) {
|
|
1258
|
+
const filters = [];
|
|
1259
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
1260
|
+
if (!allowCompanyRegistration) {
|
|
1261
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.type = 'individual'`);
|
|
1262
|
+
}
|
|
1263
|
+
if (searchPersonIds && searchPersonIds.length > 0) {
|
|
1264
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.id IN (${api_prisma_1.Prisma.join(searchPersonIds)})`);
|
|
1265
|
+
}
|
|
1266
|
+
const { todayStartIso, tomorrowStartIso } = this.getFollowupDayBoundaryIsoStrings();
|
|
1267
|
+
if (status === 'overdue') {
|
|
1268
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${todayStartIso} AS TIMESTAMPTZ)`);
|
|
1269
|
+
}
|
|
1270
|
+
else if (status === 'today') {
|
|
1271
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${todayStartIso} AS TIMESTAMPTZ)`);
|
|
1272
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${tomorrowStartIso} AS TIMESTAMPTZ)`);
|
|
1273
|
+
}
|
|
1274
|
+
else if (status === 'upcoming') {
|
|
1275
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${tomorrowStartIso} AS TIMESTAMPTZ)`);
|
|
1276
|
+
}
|
|
1277
|
+
const dateFromIso = this.normalizeDateOnlyBoundary(dateFrom, 'start');
|
|
1278
|
+
if (dateFromIso) {
|
|
1279
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${dateFromIso} AS TIMESTAMPTZ)`);
|
|
1280
|
+
}
|
|
1281
|
+
const dateToIso = this.normalizeDateOnlyBoundary(dateTo, 'endExclusive');
|
|
1282
|
+
if (dateToIso) {
|
|
1283
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${dateToIso} AS TIMESTAMPTZ)`);
|
|
1284
|
+
}
|
|
1285
|
+
return filters.length > 0
|
|
1286
|
+
? api_prisma_1.Prisma.join(filters, '\n')
|
|
1287
|
+
: api_prisma_1.Prisma.empty;
|
|
1288
|
+
}
|
|
1289
|
+
createEmptyFollowupPagination(page, pageSize) {
|
|
1290
|
+
return {
|
|
1291
|
+
total: 0,
|
|
1292
|
+
lastPage: 1,
|
|
1293
|
+
page,
|
|
1294
|
+
pageSize,
|
|
1295
|
+
prev: page > 1 ? page - 1 : null,
|
|
1296
|
+
next: null,
|
|
1297
|
+
data: [],
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
getFollowupTimestampSql() {
|
|
1301
|
+
return api_prisma_1.Prisma.sql `CAST(TRIM(BOTH '"' FROM pm_next.value::text) AS TIMESTAMPTZ)`;
|
|
1302
|
+
}
|
|
1303
|
+
getFollowupDayBoundaryDates(reference = new Date()) {
|
|
1304
|
+
const todayStart = new Date(reference.getFullYear(), reference.getMonth(), reference.getDate());
|
|
1305
|
+
const tomorrowStart = new Date(todayStart);
|
|
1306
|
+
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
|
1307
|
+
return {
|
|
1308
|
+
todayStart,
|
|
1309
|
+
tomorrowStart,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
getFollowupDayBoundaryIsoStrings(reference = new Date()) {
|
|
1313
|
+
const { todayStart, tomorrowStart } = this.getFollowupDayBoundaryDates(reference);
|
|
1314
|
+
return {
|
|
1315
|
+
todayStartIso: todayStart.toISOString(),
|
|
1316
|
+
tomorrowStartIso: tomorrowStart.toISOString(),
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
normalizeDateOnlyBoundary(value, mode) {
|
|
1320
|
+
if (!value) {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
const parsed = new Date(`${value}T00:00:00`);
|
|
1324
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
if (mode === 'endExclusive') {
|
|
1328
|
+
parsed.setDate(parsed.getDate() + 1);
|
|
1329
|
+
}
|
|
1330
|
+
return parsed.toISOString();
|
|
1331
|
+
}
|
|
1332
|
+
getFollowupStatus(nextActionAt) {
|
|
1333
|
+
const parsed = new Date(nextActionAt);
|
|
1334
|
+
const { todayStart, tomorrowStart } = this.getFollowupDayBoundaryDates();
|
|
1335
|
+
if (parsed < todayStart) {
|
|
1336
|
+
return 'overdue';
|
|
1337
|
+
}
|
|
1338
|
+
if (parsed < tomorrowStart) {
|
|
1339
|
+
return 'today';
|
|
1340
|
+
}
|
|
1341
|
+
return 'upcoming';
|
|
1342
|
+
}
|
|
1343
|
+
coerceCount(value) {
|
|
1344
|
+
if (typeof value === 'bigint') {
|
|
1345
|
+
return Number(value);
|
|
1346
|
+
}
|
|
1347
|
+
const parsed = Number(value);
|
|
1348
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1349
|
+
}
|
|
595
1350
|
async openPublicAvatar(locale, fileId, res) {
|
|
596
1351
|
const personWithAvatar = await this.prismaService.person.findFirst({
|
|
597
1352
|
where: {
|
|
@@ -612,6 +1367,110 @@ let PersonService = class PersonService {
|
|
|
612
1367
|
});
|
|
613
1368
|
res.send(buffer);
|
|
614
1369
|
}
|
|
1370
|
+
resolveDashboardRanges(query, locale) {
|
|
1371
|
+
var _a;
|
|
1372
|
+
const period = (_a = query.period) !== null && _a !== void 0 ? _a : '30d';
|
|
1373
|
+
const now = new Date();
|
|
1374
|
+
if (period === 'custom') {
|
|
1375
|
+
if (!query.date_from || !query.date_to) {
|
|
1376
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'date_from and date_to are required when period is custom.'));
|
|
1377
|
+
}
|
|
1378
|
+
const start = this.startOfDay(this.parseDateOrThrow(query.date_from, locale));
|
|
1379
|
+
const end = this.endOfDay(this.parseDateOrThrow(query.date_to, locale));
|
|
1380
|
+
if (start.getTime() > end.getTime()) {
|
|
1381
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'date_from must be less than or equal to date_to.'));
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
created: { start, end },
|
|
1385
|
+
operational: { start, end },
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const dayCount = Number(period.replace('d', ''));
|
|
1389
|
+
const createdStart = this.startOfDay(this.addDays(now, -(dayCount - 1)));
|
|
1390
|
+
const createdEnd = this.endOfDay(now);
|
|
1391
|
+
const operationalStart = this.startOfDay(this.addDays(now, -(dayCount - 1)));
|
|
1392
|
+
const operationalEnd = this.endOfDay(this.addDays(now, dayCount - 1));
|
|
1393
|
+
return {
|
|
1394
|
+
created: {
|
|
1395
|
+
start: createdStart,
|
|
1396
|
+
end: createdEnd,
|
|
1397
|
+
},
|
|
1398
|
+
operational: {
|
|
1399
|
+
start: operationalStart,
|
|
1400
|
+
end: operationalEnd,
|
|
1401
|
+
},
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
buildDashboardOwnerPerformance(people) {
|
|
1405
|
+
var _a, _b;
|
|
1406
|
+
const byOwnerId = new Map();
|
|
1407
|
+
for (const person of people) {
|
|
1408
|
+
const ownerUserId = this.coerceNumber(person.owner_user_id);
|
|
1409
|
+
if (ownerUserId <= 0) {
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const current = (_a = byOwnerId.get(ownerUserId)) !== null && _a !== void 0 ? _a : {
|
|
1413
|
+
owner_user_id: ownerUserId,
|
|
1414
|
+
owner_name: ((_b = person.owner_user) === null || _b === void 0 ? void 0 : _b.name) || `#${ownerUserId}`,
|
|
1415
|
+
leads: 0,
|
|
1416
|
+
customers: 0,
|
|
1417
|
+
pipeline_value: 0,
|
|
1418
|
+
};
|
|
1419
|
+
current.leads += 1;
|
|
1420
|
+
if (person.lifecycle_stage === 'customer') {
|
|
1421
|
+
current.customers += 1;
|
|
1422
|
+
}
|
|
1423
|
+
if (person.lifecycle_stage !== 'lost') {
|
|
1424
|
+
current.pipeline_value += this.coerceNumber(person.deal_value);
|
|
1425
|
+
}
|
|
1426
|
+
byOwnerId.set(ownerUserId, current);
|
|
1427
|
+
}
|
|
1428
|
+
return Array.from(byOwnerId.values()).sort((left, right) => left.owner_name.localeCompare(right.owner_name));
|
|
1429
|
+
}
|
|
1430
|
+
mapDashboardListItem(person, { includeCreatedAt, includeNextActionAt, }) {
|
|
1431
|
+
var _a, _b, _c, _d, _e;
|
|
1432
|
+
const source = ((_a = person.source) !== null && _a !== void 0 ? _a : 'other');
|
|
1433
|
+
const lifecycleStage = ((_b = person.lifecycle_stage) !== null && _b !== void 0 ? _b : 'new');
|
|
1434
|
+
return Object.assign(Object.assign({ id: person.id, name: person.name, trade_name: (_c = person.trade_name) !== null && _c !== void 0 ? _c : null, owner_user: (_d = person.owner_user) !== null && _d !== void 0 ? _d : null, source, lifecycle_stage: lifecycleStage }, (includeNextActionAt && person.next_action_at
|
|
1435
|
+
? { next_action_at: person.next_action_at }
|
|
1436
|
+
: {})), (includeCreatedAt && person.created_at
|
|
1437
|
+
? {
|
|
1438
|
+
created_at: (_e = this.normalizeDateTimeOrNull(person.created_at)) !== null && _e !== void 0 ? _e : person.created_at,
|
|
1439
|
+
}
|
|
1440
|
+
: {}));
|
|
1441
|
+
}
|
|
1442
|
+
isDateWithinRange(value, range) {
|
|
1443
|
+
const parsed = this.parseDateOrNull(value);
|
|
1444
|
+
if (!parsed) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
return (parsed.getTime() >= range.start.getTime() &&
|
|
1448
|
+
parsed.getTime() <= range.end.getTime());
|
|
1449
|
+
}
|
|
1450
|
+
parseDateOrThrow(value, locale) {
|
|
1451
|
+
const parsed = /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
1452
|
+
? this.parseDateOrNull(`${value}T00:00:00`)
|
|
1453
|
+
: this.parseDateOrNull(value);
|
|
1454
|
+
if (!parsed) {
|
|
1455
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, `Invalid date value: ${value}`));
|
|
1456
|
+
}
|
|
1457
|
+
return parsed;
|
|
1458
|
+
}
|
|
1459
|
+
addDays(date, amount) {
|
|
1460
|
+
const next = new Date(date);
|
|
1461
|
+
next.setDate(next.getDate() + amount);
|
|
1462
|
+
return next;
|
|
1463
|
+
}
|
|
1464
|
+
startOfDay(date) {
|
|
1465
|
+
const next = new Date(date);
|
|
1466
|
+
next.setHours(0, 0, 0, 0);
|
|
1467
|
+
return next;
|
|
1468
|
+
}
|
|
1469
|
+
endOfDay(date) {
|
|
1470
|
+
const next = new Date(date);
|
|
1471
|
+
next.setHours(23, 59, 59, 999);
|
|
1472
|
+
return next;
|
|
1473
|
+
}
|
|
615
1474
|
async enrichPeople(people, allowCompanyRegistration = true) {
|
|
616
1475
|
if (people.length === 0) {
|
|
617
1476
|
return [];
|
|
@@ -917,6 +1776,13 @@ let PersonService = class PersonService {
|
|
|
917
1776
|
create: {
|
|
918
1777
|
id: personId,
|
|
919
1778
|
trade_name: this.normalizeTextOrNull(data.trade_name),
|
|
1779
|
+
industry: this.normalizeTextOrNull(data.industry),
|
|
1780
|
+
website: this.normalizeTextOrNull(data.website),
|
|
1781
|
+
annual_revenue: this.normalizeDecimalOrNull(data.annual_revenue),
|
|
1782
|
+
employee_count: this.normalizeIntegerOrNull(data.employee_count),
|
|
1783
|
+
account_lifecycle_stage: this.normalizeAccountLifecycleStage(data.lifecycle_stage),
|
|
1784
|
+
city: this.normalizeTextOrNull(data.city),
|
|
1785
|
+
state: this.normalizeStateOrNull(data.state),
|
|
920
1786
|
foundation_date: this.parseDateOrNull(data.foundation_date),
|
|
921
1787
|
legal_nature: this.normalizeTextOrNull(data.legal_nature),
|
|
922
1788
|
headquarter_id: normalizedHeadquarterId > 0 ? normalizedHeadquarterId : null,
|
|
@@ -925,6 +1791,27 @@ let PersonService = class PersonService {
|
|
|
925
1791
|
trade_name: data.trade_name === undefined
|
|
926
1792
|
? undefined
|
|
927
1793
|
: this.normalizeTextOrNull(data.trade_name),
|
|
1794
|
+
industry: data.industry === undefined
|
|
1795
|
+
? undefined
|
|
1796
|
+
: this.normalizeTextOrNull(data.industry),
|
|
1797
|
+
website: data.website === undefined
|
|
1798
|
+
? undefined
|
|
1799
|
+
: this.normalizeTextOrNull(data.website),
|
|
1800
|
+
annual_revenue: data.annual_revenue === undefined
|
|
1801
|
+
? undefined
|
|
1802
|
+
: this.normalizeDecimalOrNull(data.annual_revenue),
|
|
1803
|
+
employee_count: data.employee_count === undefined
|
|
1804
|
+
? undefined
|
|
1805
|
+
: this.normalizeIntegerOrNull(data.employee_count),
|
|
1806
|
+
account_lifecycle_stage: data.lifecycle_stage === undefined
|
|
1807
|
+
? undefined
|
|
1808
|
+
: this.normalizeAccountLifecycleStage(data.lifecycle_stage),
|
|
1809
|
+
city: data.city === undefined
|
|
1810
|
+
? undefined
|
|
1811
|
+
: this.normalizeTextOrNull(data.city),
|
|
1812
|
+
state: data.state === undefined
|
|
1813
|
+
? undefined
|
|
1814
|
+
: this.normalizeStateOrNull(data.state),
|
|
928
1815
|
foundation_date: data.foundation_date === undefined
|
|
929
1816
|
? undefined
|
|
930
1817
|
: this.parseDateOrNull(data.foundation_date),
|
|
@@ -1394,6 +2281,20 @@ let PersonService = class PersonService {
|
|
|
1394
2281
|
const date = value instanceof Date ? value : new Date(String(value));
|
|
1395
2282
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
1396
2283
|
}
|
|
2284
|
+
normalizeDecimalOrNull(value) {
|
|
2285
|
+
if (value == null || value === '') {
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
const parsed = Number(value);
|
|
2289
|
+
return Number.isFinite(parsed) ? new api_prisma_1.Prisma.Decimal(parsed) : null;
|
|
2290
|
+
}
|
|
2291
|
+
normalizeIntegerOrNull(value) {
|
|
2292
|
+
if (value == null || value === '') {
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
const parsed = Number(value);
|
|
2296
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
2297
|
+
}
|
|
1397
2298
|
normalizeTextOrNull(value) {
|
|
1398
2299
|
if (typeof value !== 'string') {
|
|
1399
2300
|
return value == null ? null : String(value);
|
|
@@ -1411,6 +2312,20 @@ let PersonService = class PersonService {
|
|
|
1411
2312
|
const parsed = Number(value);
|
|
1412
2313
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
1413
2314
|
}
|
|
2315
|
+
normalizeStateOrNull(value) {
|
|
2316
|
+
var _a, _b;
|
|
2317
|
+
const normalized = (_b = (_a = this.normalizeTextOrNull(value)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
|
|
2318
|
+
return normalized ? normalized.slice(0, 2) : null;
|
|
2319
|
+
}
|
|
2320
|
+
normalizeAccountLifecycleStage(value) {
|
|
2321
|
+
const normalized = this.normalizeTextOrNull(value);
|
|
2322
|
+
if (!normalized) {
|
|
2323
|
+
return null;
|
|
2324
|
+
}
|
|
2325
|
+
return account_dto_1.ACCOUNT_LIFECYCLE_STAGES.includes(normalized)
|
|
2326
|
+
? normalized
|
|
2327
|
+
: null;
|
|
2328
|
+
}
|
|
1414
2329
|
resolveRequestedOwnerUserId(ownerUserId, mine, currentUserId) {
|
|
1415
2330
|
if (mine === true ||
|
|
1416
2331
|
mine === 'true' ||
|
|
@@ -1436,6 +2351,288 @@ let PersonService = class PersonService {
|
|
|
1436
2351
|
}
|
|
1437
2352
|
return person;
|
|
1438
2353
|
}
|
|
2354
|
+
async getPersonOwnerUserId(personId) {
|
|
2355
|
+
const metadata = await this.prismaService.person_metadata.findFirst({
|
|
2356
|
+
where: {
|
|
2357
|
+
person_id: personId,
|
|
2358
|
+
key: OWNER_USER_METADATA_KEY,
|
|
2359
|
+
},
|
|
2360
|
+
select: {
|
|
2361
|
+
value: true,
|
|
2362
|
+
},
|
|
2363
|
+
});
|
|
2364
|
+
const ownerUserId = this.metadataToNumber(metadata === null || metadata === void 0 ? void 0 : metadata.value);
|
|
2365
|
+
return ownerUserId && ownerUserId > 0 ? ownerUserId : null;
|
|
2366
|
+
}
|
|
2367
|
+
async upsertFollowupActivity(tx, { personId, ownerUserId, dueAt, notes, actorUserId, }) {
|
|
2368
|
+
const existingRows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
|
|
2369
|
+
SELECT id
|
|
2370
|
+
FROM crm_activity
|
|
2371
|
+
WHERE person_id = ${personId}
|
|
2372
|
+
AND source_kind = 'followup'
|
|
2373
|
+
AND completed_at IS NULL
|
|
2374
|
+
ORDER BY id DESC
|
|
2375
|
+
LIMIT 1
|
|
2376
|
+
`));
|
|
2377
|
+
const existing = existingRows[0];
|
|
2378
|
+
const normalizedNotes = this.normalizeTextOrNull(notes);
|
|
2379
|
+
if (existing) {
|
|
2380
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2381
|
+
UPDATE crm_activity
|
|
2382
|
+
SET
|
|
2383
|
+
owner_user_id = ${ownerUserId},
|
|
2384
|
+
type = CAST(${'task'} AS crm_activity_type_enum),
|
|
2385
|
+
subject = ${this.getFollowupActivitySubject()},
|
|
2386
|
+
notes = ${normalizedNotes},
|
|
2387
|
+
due_at = CAST(${dueAt} AS TIMESTAMPTZ),
|
|
2388
|
+
priority = CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2389
|
+
updated_at = NOW()
|
|
2390
|
+
WHERE id = ${existing.id}
|
|
2391
|
+
`);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2395
|
+
INSERT INTO crm_activity (
|
|
2396
|
+
person_id,
|
|
2397
|
+
owner_user_id,
|
|
2398
|
+
created_by_user_id,
|
|
2399
|
+
type,
|
|
2400
|
+
subject,
|
|
2401
|
+
notes,
|
|
2402
|
+
due_at,
|
|
2403
|
+
priority,
|
|
2404
|
+
source_kind,
|
|
2405
|
+
created_at,
|
|
2406
|
+
updated_at
|
|
2407
|
+
)
|
|
2408
|
+
VALUES (
|
|
2409
|
+
${personId},
|
|
2410
|
+
${ownerUserId},
|
|
2411
|
+
${actorUserId},
|
|
2412
|
+
CAST(${'task'} AS crm_activity_type_enum),
|
|
2413
|
+
${this.getFollowupActivitySubject()},
|
|
2414
|
+
${normalizedNotes},
|
|
2415
|
+
CAST(${dueAt} AS TIMESTAMPTZ),
|
|
2416
|
+
CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2417
|
+
CAST(${'followup'} AS crm_activity_source_kind_enum),
|
|
2418
|
+
NOW(),
|
|
2419
|
+
NOW()
|
|
2420
|
+
)
|
|
2421
|
+
`);
|
|
2422
|
+
}
|
|
2423
|
+
async createCompletedInteractionActivity(tx, { personId, ownerUserId, interaction, actorUserId, }) {
|
|
2424
|
+
const completedAt = new Date(interaction.created_at);
|
|
2425
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2426
|
+
INSERT INTO crm_activity (
|
|
2427
|
+
person_id,
|
|
2428
|
+
owner_user_id,
|
|
2429
|
+
created_by_user_id,
|
|
2430
|
+
completed_by_user_id,
|
|
2431
|
+
type,
|
|
2432
|
+
subject,
|
|
2433
|
+
notes,
|
|
2434
|
+
due_at,
|
|
2435
|
+
completed_at,
|
|
2436
|
+
priority,
|
|
2437
|
+
source_kind,
|
|
2438
|
+
created_at,
|
|
2439
|
+
updated_at
|
|
2440
|
+
)
|
|
2441
|
+
VALUES (
|
|
2442
|
+
${personId},
|
|
2443
|
+
${ownerUserId},
|
|
2444
|
+
${actorUserId},
|
|
2445
|
+
${actorUserId},
|
|
2446
|
+
CAST(${interaction.type} AS crm_activity_type_enum),
|
|
2447
|
+
${this.getInteractionActivitySubject(interaction.type)},
|
|
2448
|
+
${this.normalizeTextOrNull(interaction.notes)},
|
|
2449
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2450
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2451
|
+
CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2452
|
+
CAST(${'interaction'} AS crm_activity_source_kind_enum),
|
|
2453
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2454
|
+
NOW()
|
|
2455
|
+
)
|
|
2456
|
+
`);
|
|
2457
|
+
}
|
|
2458
|
+
getFollowupActivitySubject() {
|
|
2459
|
+
return 'Follow-up';
|
|
2460
|
+
}
|
|
2461
|
+
getInteractionActivitySubject(type) {
|
|
2462
|
+
switch (type) {
|
|
2463
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.CALL:
|
|
2464
|
+
return 'Call';
|
|
2465
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.EMAIL:
|
|
2466
|
+
return 'Email';
|
|
2467
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.WHATSAPP:
|
|
2468
|
+
return 'WhatsApp';
|
|
2469
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.MEETING:
|
|
2470
|
+
return 'Meeting';
|
|
2471
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.NOTE:
|
|
2472
|
+
default:
|
|
2473
|
+
return 'Note';
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
async ensureCompanyAccountAccessible(id, locale) {
|
|
2477
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
2478
|
+
if (!allowCompanyRegistration) {
|
|
2479
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
|
|
2480
|
+
}
|
|
2481
|
+
const person = await this.prismaService.person.findUnique({
|
|
2482
|
+
where: { id },
|
|
2483
|
+
select: {
|
|
2484
|
+
id: true,
|
|
2485
|
+
name: true,
|
|
2486
|
+
status: true,
|
|
2487
|
+
type: true,
|
|
2488
|
+
},
|
|
2489
|
+
});
|
|
2490
|
+
if (!person || person.type !== 'company') {
|
|
2491
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
|
|
2492
|
+
}
|
|
2493
|
+
return person;
|
|
2494
|
+
}
|
|
2495
|
+
async loadAccountPeopleByIds(personIds) {
|
|
2496
|
+
if (personIds.length === 0) {
|
|
2497
|
+
return [];
|
|
2498
|
+
}
|
|
2499
|
+
const people = await this.prismaService.person.findMany({
|
|
2500
|
+
where: {
|
|
2501
|
+
id: {
|
|
2502
|
+
in: personIds,
|
|
2503
|
+
},
|
|
2504
|
+
},
|
|
2505
|
+
include: {
|
|
2506
|
+
contact: {
|
|
2507
|
+
include: {
|
|
2508
|
+
contact_type: true,
|
|
2509
|
+
},
|
|
2510
|
+
},
|
|
2511
|
+
person_metadata: true,
|
|
2512
|
+
},
|
|
2513
|
+
});
|
|
2514
|
+
return this.enrichPeople(people, true);
|
|
2515
|
+
}
|
|
2516
|
+
mapAccountFromPerson(person, company) {
|
|
2517
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2518
|
+
return {
|
|
2519
|
+
id: person.id,
|
|
2520
|
+
name: person.name,
|
|
2521
|
+
trade_name: (_a = company.trade_name) !== null && _a !== void 0 ? _a : null,
|
|
2522
|
+
status: person.status,
|
|
2523
|
+
industry: (_b = company.industry) !== null && _b !== void 0 ? _b : null,
|
|
2524
|
+
website: (_c = company.website) !== null && _c !== void 0 ? _c : null,
|
|
2525
|
+
email: this.getPrimaryAccountContactValue(person.contact, ['EMAIL']),
|
|
2526
|
+
phone: this.getPrimaryAccountContactValue(person.contact, [
|
|
2527
|
+
'PHONE',
|
|
2528
|
+
'MOBILE',
|
|
2529
|
+
'WHATSAPP',
|
|
2530
|
+
]),
|
|
2531
|
+
owner_user_id: (_d = person.owner_user_id) !== null && _d !== void 0 ? _d : null,
|
|
2532
|
+
owner_user: (_e = person.owner_user) !== null && _e !== void 0 ? _e : null,
|
|
2533
|
+
annual_revenue: company.annual_revenue == null ? null : Number(company.annual_revenue),
|
|
2534
|
+
employee_count: (_f = company.employee_count) !== null && _f !== void 0 ? _f : null,
|
|
2535
|
+
lifecycle_stage: (_g = company.account_lifecycle_stage) !== null && _g !== void 0 ? _g : null,
|
|
2536
|
+
city: (_h = company.city) !== null && _h !== void 0 ? _h : null,
|
|
2537
|
+
state: (_j = company.state) !== null && _j !== void 0 ? _j : null,
|
|
2538
|
+
created_at: (_m = (_l = (_k = person.created_at) === null || _k === void 0 ? void 0 : _k.toISOString) === null || _l === void 0 ? void 0 : _l.call(_k)) !== null && _m !== void 0 ? _m : String(person.created_at),
|
|
2539
|
+
last_interaction_at: (_o = this.normalizeDateTimeOrNull(person.last_interaction_at)) !== null && _o !== void 0 ? _o : null,
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
getPrimaryAccountContactValue(contacts, codes) {
|
|
2543
|
+
var _a;
|
|
2544
|
+
const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
|
|
2545
|
+
const items = Array.isArray(contacts)
|
|
2546
|
+
? contacts.filter((contact) => {
|
|
2547
|
+
var _a;
|
|
2548
|
+
return normalizedCodes.has(String(((_a = contact === null || contact === void 0 ? void 0 : contact.contact_type) === null || _a === void 0 ? void 0 : _a.code) || '').toUpperCase());
|
|
2549
|
+
})
|
|
2550
|
+
: [];
|
|
2551
|
+
const primary = items.find((contact) => contact === null || contact === void 0 ? void 0 : contact.is_primary);
|
|
2552
|
+
const fallback = items[0];
|
|
2553
|
+
return this.normalizeTextOrNull((_a = primary === null || primary === void 0 ? void 0 : primary.value) !== null && _a !== void 0 ? _a : fallback === null || fallback === void 0 ? void 0 : fallback.value);
|
|
2554
|
+
}
|
|
2555
|
+
async upsertPrimaryAccountContact(tx, personId, code, value) {
|
|
2556
|
+
const normalizedValue = this.normalizeTextOrNull(value);
|
|
2557
|
+
const allowedCodes = code === 'PHONE' ? ['PHONE', 'MOBILE', 'WHATSAPP'] : ['EMAIL'];
|
|
2558
|
+
const type = await tx.contact_type.findFirst({
|
|
2559
|
+
where: {
|
|
2560
|
+
code: {
|
|
2561
|
+
equals: code,
|
|
2562
|
+
mode: 'insensitive',
|
|
2563
|
+
},
|
|
2564
|
+
},
|
|
2565
|
+
select: {
|
|
2566
|
+
id: true,
|
|
2567
|
+
},
|
|
2568
|
+
});
|
|
2569
|
+
if (!type) {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const existingContacts = await tx.contact.findMany({
|
|
2573
|
+
where: {
|
|
2574
|
+
person_id: personId,
|
|
2575
|
+
contact_type: {
|
|
2576
|
+
code: {
|
|
2577
|
+
in: allowedCodes,
|
|
2578
|
+
},
|
|
2579
|
+
},
|
|
2580
|
+
},
|
|
2581
|
+
orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
|
|
2582
|
+
select: {
|
|
2583
|
+
id: true,
|
|
2584
|
+
is_primary: true,
|
|
2585
|
+
},
|
|
2586
|
+
});
|
|
2587
|
+
if (!normalizedValue) {
|
|
2588
|
+
const contactToDelete = existingContacts[0];
|
|
2589
|
+
if (contactToDelete) {
|
|
2590
|
+
await tx.contact.delete({
|
|
2591
|
+
where: {
|
|
2592
|
+
id: contactToDelete.id,
|
|
2593
|
+
},
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
const primaryContact = existingContacts[0];
|
|
2599
|
+
if (primaryContact) {
|
|
2600
|
+
await tx.contact.update({
|
|
2601
|
+
where: {
|
|
2602
|
+
id: primaryContact.id,
|
|
2603
|
+
},
|
|
2604
|
+
data: {
|
|
2605
|
+
value: normalizedValue,
|
|
2606
|
+
is_primary: true,
|
|
2607
|
+
contact_type_id: type.id,
|
|
2608
|
+
},
|
|
2609
|
+
});
|
|
2610
|
+
const secondaryContacts = existingContacts
|
|
2611
|
+
.slice(1)
|
|
2612
|
+
.filter((contact) => contact.is_primary);
|
|
2613
|
+
if (secondaryContacts.length > 0) {
|
|
2614
|
+
await tx.contact.updateMany({
|
|
2615
|
+
where: {
|
|
2616
|
+
id: {
|
|
2617
|
+
in: secondaryContacts.map((contact) => contact.id),
|
|
2618
|
+
},
|
|
2619
|
+
},
|
|
2620
|
+
data: {
|
|
2621
|
+
is_primary: false,
|
|
2622
|
+
},
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
await tx.contact.create({
|
|
2628
|
+
data: {
|
|
2629
|
+
person_id: personId,
|
|
2630
|
+
contact_type_id: type.id,
|
|
2631
|
+
value: normalizedValue,
|
|
2632
|
+
is_primary: true,
|
|
2633
|
+
},
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
1439
2636
|
async loadInteractionsFromTx(tx, personId) {
|
|
1440
2637
|
const metadata = await tx.person_metadata.findFirst({
|
|
1441
2638
|
where: {
|
|
@@ -1513,6 +2710,176 @@ let PersonService = class PersonService {
|
|
|
1513
2710
|
}
|
|
1514
2711
|
return filters;
|
|
1515
2712
|
}
|
|
2713
|
+
buildAccountSqlFilters({ search, status, lifecycleStage, }) {
|
|
2714
|
+
const filters = [];
|
|
2715
|
+
if (status && status !== 'all') {
|
|
2716
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.status = ${status}`);
|
|
2717
|
+
}
|
|
2718
|
+
if (lifecycleStage && lifecycleStage !== 'all') {
|
|
2719
|
+
filters.push(api_prisma_1.Prisma.sql `AND pc.account_lifecycle_stage = CAST(${lifecycleStage} AS person_company_account_lifecycle_stage_enum)`);
|
|
2720
|
+
}
|
|
2721
|
+
if (search) {
|
|
2722
|
+
const searchLike = `%${search}%`;
|
|
2723
|
+
const normalizedDigits = this.normalizeDigits(search);
|
|
2724
|
+
const digitsLike = `%${normalizedDigits}%`;
|
|
2725
|
+
filters.push(api_prisma_1.Prisma.sql `
|
|
2726
|
+
AND (
|
|
2727
|
+
p.name ILIKE ${searchLike}
|
|
2728
|
+
OR COALESCE(pc.trade_name, '') ILIKE ${searchLike}
|
|
2729
|
+
OR COALESCE(pc.city, '') ILIKE ${searchLike}
|
|
2730
|
+
OR COALESCE(pc.state, '') ILIKE ${searchLike}
|
|
2731
|
+
OR EXISTS (
|
|
2732
|
+
SELECT 1
|
|
2733
|
+
FROM contact c
|
|
2734
|
+
INNER JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
2735
|
+
WHERE c.person_id = p.id
|
|
2736
|
+
AND UPPER(ct.code) = 'EMAIL'
|
|
2737
|
+
AND c.value ILIKE ${searchLike}
|
|
2738
|
+
)
|
|
2739
|
+
${normalizedDigits.length > 0
|
|
2740
|
+
? api_prisma_1.Prisma.sql `
|
|
2741
|
+
OR EXISTS (
|
|
2742
|
+
SELECT 1
|
|
2743
|
+
FROM contact c
|
|
2744
|
+
INNER JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
2745
|
+
WHERE c.person_id = p.id
|
|
2746
|
+
AND UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
|
|
2747
|
+
AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${digitsLike}
|
|
2748
|
+
)
|
|
2749
|
+
`
|
|
2750
|
+
: api_prisma_1.Prisma.empty}
|
|
2751
|
+
)
|
|
2752
|
+
`);
|
|
2753
|
+
}
|
|
2754
|
+
return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
|
|
2755
|
+
}
|
|
2756
|
+
getAccountOrderBySql(sortField, sortOrder) {
|
|
2757
|
+
const normalizedSortField = sortField === 'created_at' ? 'created_at' : 'name';
|
|
2758
|
+
const normalizedSortOrder = sortOrder === 'desc' ? 'DESC' : 'ASC';
|
|
2759
|
+
if (normalizedSortField === 'created_at') {
|
|
2760
|
+
return normalizedSortOrder === 'DESC'
|
|
2761
|
+
? api_prisma_1.Prisma.sql `p.created_at DESC`
|
|
2762
|
+
: api_prisma_1.Prisma.sql `p.created_at ASC`;
|
|
2763
|
+
}
|
|
2764
|
+
return normalizedSortOrder === 'DESC'
|
|
2765
|
+
? api_prisma_1.Prisma.sql `LOWER(p.name) DESC`
|
|
2766
|
+
: api_prisma_1.Prisma.sql `LOWER(p.name) ASC`;
|
|
2767
|
+
}
|
|
2768
|
+
createEmptyAccountPagination(page, pageSize) {
|
|
2769
|
+
return {
|
|
2770
|
+
total: 0,
|
|
2771
|
+
lastPage: 1,
|
|
2772
|
+
page,
|
|
2773
|
+
pageSize,
|
|
2774
|
+
prev: page > 1 ? page - 1 : null,
|
|
2775
|
+
next: null,
|
|
2776
|
+
data: [],
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
buildCrmActivitySqlFilters({ allowCompanyRegistration, search, status, type, priority, }) {
|
|
2780
|
+
const filters = [];
|
|
2781
|
+
if (!allowCompanyRegistration) {
|
|
2782
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.type = 'individual'`);
|
|
2783
|
+
}
|
|
2784
|
+
if (status === 'pending') {
|
|
2785
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NULL AND a.due_at >= NOW()`);
|
|
2786
|
+
}
|
|
2787
|
+
else if (status === 'overdue') {
|
|
2788
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NULL AND a.due_at < NOW()`);
|
|
2789
|
+
}
|
|
2790
|
+
else if (status === 'completed') {
|
|
2791
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NOT NULL`);
|
|
2792
|
+
}
|
|
2793
|
+
if (type && type !== 'all') {
|
|
2794
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.type = CAST(${type} AS crm_activity_type_enum)`);
|
|
2795
|
+
}
|
|
2796
|
+
if (priority && priority !== 'all') {
|
|
2797
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.priority = CAST(${priority} AS crm_activity_priority_enum)`);
|
|
2798
|
+
}
|
|
2799
|
+
if (search) {
|
|
2800
|
+
const searchLike = `%${search}%`;
|
|
2801
|
+
filters.push(api_prisma_1.Prisma.sql `
|
|
2802
|
+
AND (
|
|
2803
|
+
a.subject ILIKE ${searchLike}
|
|
2804
|
+
OR COALESCE(a.notes, '') ILIKE ${searchLike}
|
|
2805
|
+
OR p.name ILIKE ${searchLike}
|
|
2806
|
+
OR COALESCE(owner_user.name, '') ILIKE ${searchLike}
|
|
2807
|
+
)
|
|
2808
|
+
`);
|
|
2809
|
+
}
|
|
2810
|
+
return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
|
|
2811
|
+
}
|
|
2812
|
+
createEmptyCrmActivityPagination(page, pageSize) {
|
|
2813
|
+
return {
|
|
2814
|
+
total: 0,
|
|
2815
|
+
lastPage: 1,
|
|
2816
|
+
page,
|
|
2817
|
+
pageSize,
|
|
2818
|
+
prev: page > 1 ? page - 1 : null,
|
|
2819
|
+
next: null,
|
|
2820
|
+
data: [],
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
mapCrmActivityListRow(row) {
|
|
2824
|
+
var _a, _b;
|
|
2825
|
+
const completedAt = this.normalizeDateTimeOrNull(row.completed_at);
|
|
2826
|
+
const dueAt = (_a = this.normalizeDateTimeOrNull(row.due_at)) !== null && _a !== void 0 ? _a : new Date().toISOString();
|
|
2827
|
+
const ownerUserId = this.coerceNumber(row.owner_user_id) || null;
|
|
2828
|
+
return {
|
|
2829
|
+
id: this.coerceNumber(row.id),
|
|
2830
|
+
person_id: this.coerceNumber(row.person_id),
|
|
2831
|
+
person: {
|
|
2832
|
+
id: this.coerceNumber(row.person_id),
|
|
2833
|
+
name: this.normalizeTextOrNull(row.person_name) || `#${row.person_id}`,
|
|
2834
|
+
type: this.normalizeTextOrNull(row.person_type) === 'company'
|
|
2835
|
+
? 'company'
|
|
2836
|
+
: 'individual',
|
|
2837
|
+
status: this.normalizeTextOrNull(row.person_status) === 'inactive'
|
|
2838
|
+
? 'inactive'
|
|
2839
|
+
: 'active',
|
|
2840
|
+
trade_name: this.normalizeTextOrNull(row.person_trade_name),
|
|
2841
|
+
},
|
|
2842
|
+
owner_user_id: ownerUserId,
|
|
2843
|
+
owner_user: ownerUserId
|
|
2844
|
+
? {
|
|
2845
|
+
id: ownerUserId,
|
|
2846
|
+
name: this.normalizeTextOrNull(row.owner_user_name) || `#${ownerUserId}`,
|
|
2847
|
+
}
|
|
2848
|
+
: null,
|
|
2849
|
+
type: (this.normalizeTextOrNull(row.type) || 'task'),
|
|
2850
|
+
subject: this.normalizeTextOrNull(row.subject) || 'Activity',
|
|
2851
|
+
notes: this.normalizeTextOrNull(row.notes),
|
|
2852
|
+
due_at: dueAt,
|
|
2853
|
+
completed_at: completedAt,
|
|
2854
|
+
created_at: (_b = this.normalizeDateTimeOrNull(row.created_at)) !== null && _b !== void 0 ? _b : new Date().toISOString(),
|
|
2855
|
+
priority: (this.normalizeTextOrNull(row.priority) || 'medium'),
|
|
2856
|
+
status: this.getCrmActivityStatus(dueAt, completedAt),
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
mapCrmActivityDetailRow(row) {
|
|
2860
|
+
const base = this.mapCrmActivityListRow(row);
|
|
2861
|
+
const createdByUserId = this.coerceNumber(row.created_by_user_id) || null;
|
|
2862
|
+
const completedByUserId = this.coerceNumber(row.completed_by_user_id) || null;
|
|
2863
|
+
return Object.assign(Object.assign({}, base), { source_kind: (this.normalizeTextOrNull(row.source_kind) || 'manual'), created_by_user_id: createdByUserId, created_by_user: createdByUserId
|
|
2864
|
+
? {
|
|
2865
|
+
id: createdByUserId,
|
|
2866
|
+
name: this.normalizeTextOrNull(row.created_by_user_name) ||
|
|
2867
|
+
`#${createdByUserId}`,
|
|
2868
|
+
}
|
|
2869
|
+
: null, completed_by_user_id: completedByUserId, completed_by_user: completedByUserId
|
|
2870
|
+
? {
|
|
2871
|
+
id: completedByUserId,
|
|
2872
|
+
name: this.normalizeTextOrNull(row.completed_by_user_name) ||
|
|
2873
|
+
`#${completedByUserId}`,
|
|
2874
|
+
}
|
|
2875
|
+
: null });
|
|
2876
|
+
}
|
|
2877
|
+
getCrmActivityStatus(dueAt, completedAt) {
|
|
2878
|
+
if (completedAt) {
|
|
2879
|
+
return 'completed';
|
|
2880
|
+
}
|
|
2881
|
+
return new Date(dueAt) < new Date() ? 'overdue' : 'pending';
|
|
2882
|
+
}
|
|
1516
2883
|
normalizeDigits(value) {
|
|
1517
2884
|
return value.replace(/\D/g, '');
|
|
1518
2885
|
}
|