Track person import source and previous names; add source report and edit UX.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-04 12:01:21 +03:30
co-authored by Cursor
parent e144b2277a
commit 8b37525f2f
15 changed files with 632 additions and 25 deletions
+60 -1
View File
@@ -7,6 +7,7 @@ import type {
CreatePersonRequest,
PaginatedResponse,
PersonDto,
PersonImportSourceStatDto,
PersonMood,
UpdatePersonRequest,
} from '@mento/shared';
@@ -31,6 +32,10 @@ import { AutomationsService } from '../automations/automations.service';
import { MessageAutomationsService } from '../message-automations/message-automations.service';
import { WebhooksService } from '../public-api/webhooks.service';
import { normalizeTags } from '../common/tags';
import {
normalizePersonImportSource,
personImportSourceLabel,
} from './person-source';
const personInclude = {
company: { select: { id: true, name: true } },
@@ -44,6 +49,10 @@ type PersonRow = {
id: string;
firstName: string;
lastName: string;
previousFirstName: string | null;
previousLastName: string | null;
importSource: string;
sourceRef: string | null;
email: string | null;
phone: string | null;
mobile: string | null;
@@ -85,20 +94,31 @@ export class PeopleService {
q?: string,
page = 1,
limit = 20,
opts?: { mood?: PersonMood; sort?: 'name' | 'newest' },
opts?: {
mood?: PersonMood;
sort?: 'name' | 'newest';
importSource?: string;
},
): Promise<PaginatedResponse<PersonDto>> {
const sourceFilter = opts?.importSource?.trim()
? normalizePersonImportSource(opts.importSource)
: undefined;
const where: Prisma.PersonWhereInput = {
organizationId: actor.organizationId,
...ownerWhere(actor),
...(opts?.mood ? { mood: opts.mood } : {}),
...(sourceFilter ? { importSource: sourceFilter } : {}),
...(q?.trim()
? {
OR: [
{ firstName: { contains: q.trim(), mode: 'insensitive' } },
{ lastName: { contains: q.trim(), mode: 'insensitive' } },
{ previousFirstName: { contains: q.trim(), mode: 'insensitive' } },
{ previousLastName: { contains: q.trim(), mode: 'insensitive' } },
{ email: { contains: q.trim(), mode: 'insensitive' } },
{ mobile: { contains: q.trim() } },
{ phone: { contains: q.trim() } },
{ sourceRef: { contains: q.trim() } },
{ company: { name: { contains: q.trim(), mode: 'insensitive' } } },
],
}
@@ -127,6 +147,24 @@ export class PeopleService {
};
}
async sourceStats(actor: Actor): Promise<PersonImportSourceStatDto[]> {
const rows = await this.prisma.person.groupBy({
by: ['importSource'],
where: {
organizationId: actor.organizationId,
...ownerWhere(actor),
},
_count: { _all: true },
});
return rows
.map((row) => ({
source: row.importSource || 'manual',
label: personImportSourceLabel(row.importSource || 'manual'),
count: row._count._all,
}))
.sort((a, b) => b.count - a.count);
}
async get(actor: Actor, id: string): Promise<PersonDto> {
const row = await this.prisma.person.findFirst({
where: { id, organizationId: actor.organizationId, ...ownerWhere(actor) },
@@ -147,6 +185,10 @@ export class PeopleService {
organizationId: actor.organizationId,
firstName: (dto.firstName ?? '').trim(),
lastName: (dto.lastName ?? '').trim(),
previousFirstName: emptyToNull(dto.previousFirstName),
previousLastName: emptyToNull(dto.previousLastName),
importSource: normalizePersonImportSource(dto.importSource),
sourceRef: emptyToNull(dto.sourceRef),
email: normalizeEmail(dto.email),
phone: normalizePhone(dto.phone),
mobile: normalizePhone(dto.mobile),
@@ -207,6 +249,16 @@ export class PeopleService {
const data: Prisma.PersonUncheckedUpdateInput = {};
if (dto.firstName !== undefined) data.firstName = dto.firstName.trim();
if (dto.lastName !== undefined) data.lastName = dto.lastName.trim();
if ('previousFirstName' in dto) {
data.previousFirstName = emptyToNull(dto.previousFirstName);
}
if ('previousLastName' in dto) {
data.previousLastName = emptyToNull(dto.previousLastName);
}
if (dto.importSource !== undefined) {
data.importSource = normalizePersonImportSource(dto.importSource);
}
if ('sourceRef' in dto) data.sourceRef = emptyToNull(dto.sourceRef);
if ('email' in dto) data.email = normalizeEmail(dto.email);
if ('phone' in dto) data.phone = normalizePhone(dto.phone);
if ('mobile' in dto) data.mobile = normalizePhone(dto.mobile);
@@ -297,10 +349,17 @@ export class PeopleService {
}
private toDto(row: PersonRow): PersonDto {
const previousFullName = `${row.previousFirstName ?? ''} ${row.previousLastName ?? ''}`.trim();
const importSource = row.importSource || 'manual';
return {
id: row.id,
firstName: row.firstName,
lastName: row.lastName,
previousFirstName: row.previousFirstName,
previousLastName: row.previousLastName,
previousFullName: previousFullName || null,
importSource,
sourceRef: row.sourceRef,
fullName: `${row.firstName} ${row.lastName}`.trim(),
email: row.email,
phone: row.phone,