Keep previous import source and code when a person is re-imported from another source

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 20:54:23 +03:30
co-authored by Cursor
parent c8753a4c4e
commit 46dcf296d3
8 changed files with 103 additions and 11 deletions
@@ -0,0 +1,14 @@
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousImportSource" TEXT;
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousSourceRef" TEXT;
CREATE INDEX IF NOT EXISTS "Person_organizationId_previousSourceRef_idx" ON "Person"("organizationId", "previousSourceRef");
-- اشخاصی که قبلاً از دیدار آمده‌اند و بعد با ته‌حساب بازنویسی شده‌اند: کد دیدار را به‌عنوان کد قبلی بازیابی کن
UPDATE "Person"
SET "previousImportSource" = 'didar',
"previousSourceRef" = TRIM("customFields"->>'didarId')
WHERE "importSource" = 'tahesab'
AND "previousSourceRef" IS NULL
AND "customFields" IS NOT NULL
AND jsonb_typeof("customFields") = 'object'
AND COALESCE(TRIM("customFields"->>'didarId'), '') <> '';
+4
View File
@@ -441,6 +441,9 @@ model Person {
importSource String @default("manual")
/// شناسه در مبدأ (کد دیدار، کد مشتری ته‌حساب، …)
sourceRef String?
/// منبع و کد قبلی وقتی شخص از منبع دیگری دوباره وارد شده (مثلاً دیدار قبل از ته‌حساب)
previousImportSource String?
previousSourceRef String?
email String?
phone String?
mobile String?
@@ -488,6 +491,7 @@ model Person {
@@index([organizationId, lastName, firstName])
@@index([organizationId, importSource])
@@index([organizationId, sourceRef])
@@index([organizationId, previousSourceRef])
@@index([organizationId, loyaltyPoints])
@@index([companyId])
@@index([ownerId])
+5
View File
@@ -53,6 +53,8 @@ type PersonRow = {
previousLastName: string | null;
importSource: string;
sourceRef: string | null;
previousImportSource: string | null;
previousSourceRef: string | null;
email: string | null;
phone: string | null;
mobile: string | null;
@@ -119,6 +121,7 @@ export class PeopleService {
{ mobile: { contains: q.trim() } },
{ phone: { contains: q.trim() } },
{ sourceRef: { contains: q.trim() } },
{ previousSourceRef: { contains: q.trim() } },
{ company: { name: { contains: q.trim(), mode: 'insensitive' } } },
],
}
@@ -373,6 +376,8 @@ export class PeopleService {
previousFullName: previousFullName || null,
importSource,
sourceRef: row.sourceRef,
previousImportSource: row.previousImportSource,
previousSourceRef: row.previousSourceRef,
fullName: `${row.firstName} ${row.lastName}`.trim(),
email: row.email,
phone: row.phone,
+39
View File
@@ -29,6 +29,45 @@ export function personImportSourceLabel(source: string): string {
return source || PERSON_IMPORT_SOURCE_LABELS.manual;
}
type SourceSnapshot = {
importSource?: string | null;
sourceRef?: string | null;
previousImportSource?: string | null;
previousSourceRef?: string | null;
customFields?: unknown;
};
function didarIdFrom(customFields: unknown): string | null {
if (!customFields || typeof customFields !== 'object') return null;
const raw = (customFields as Record<string, unknown>).didarId;
if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw);
if (typeof raw === 'string' && raw.trim()) return raw.trim();
return null;
}
/**
* وقتی شخصِ موجود از منبع دیگری دوباره وارد می‌شود، منبع و کد فعلی را به‌عنوان «قبلی» نگه می‌دارد.
* اگر منبع یکسان باشد (ورود مجدد از همان منبع)، مقادیر قبلی دست نمی‌خورند.
*/
export function carryPreviousSource(
existing: SourceSnapshot,
nextSource: PersonImportSource,
): { previousImportSource?: string | null; previousSourceRef?: string | null } {
const currentSource = normalizePersonImportSource(existing.importSource);
if (currentSource === nextSource) return {};
let ref = existing.sourceRef?.trim() || null;
let source: string = currentSource;
if (!ref) {
const didarId = didarIdFrom(existing.customFields);
if (didarId && nextSource !== 'didar') {
ref = didarId;
source = 'didar';
}
}
if (source === 'manual' && !ref) return {};
return { previousImportSource: source, previousSourceRef: ref };
}
export function namesDiffer(
aFirst: string,
aLast: string,
@@ -6,6 +6,7 @@ import {
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { carryPreviousSource } from '../../contacts/person-source';
import { TahesabConnectionsService } from './connections.service';
import {
CreateInTahesabDto,
@@ -514,6 +515,7 @@ export class TahesabPersonLinkService {
previousLastName: prevLast || null,
}
: {}),
...carryPreviousSource(existing, 'tahesab'),
importSource: 'tahesab',
sourceRef,
...(mobile
@@ -549,6 +551,7 @@ export class TahesabPersonLinkService {
previousLastName: byMobile.lastName || null,
}
: {}),
...carryPreviousSource(byMobile, 'tahesab'),
importSource: 'tahesab',
sourceRef,
},
+7 -1
View File
@@ -655,7 +655,13 @@ onMounted(load);
قبلی: {{ p.previousFullName }}
</div>
</td>
<td>{{ sourceLabel(p.importSource || 'manual') }}</td>
<td>
{{ sourceLabel(p.importSource || 'manual') }}
<div v-if="p.previousImportSource" class="muted tiny">
قبلاً: {{ sourceLabel(p.previousImportSource) }}
<bdi v-if="p.previousSourceRef">{{ toFaDigits(p.previousSourceRef) }}</bdi>
</div>
</td>
<td dir="ltr">{{ externalCodeLabel(p) }}</td>
<td>{{ p.position || '—' }}</td>
<td>
+28 -10
View File
@@ -148,18 +148,25 @@ const externalCode = computed(() => {
}
return '';
});
const SOURCE_LABELS: Record<string, string> = {
manual: 'دستی',
tahesab: 'ته‌حساب',
didar: 'دیدار',
excel: 'اکسل',
phone: 'مخاطبین گوشی',
lead_form: 'فرم لید',
other: 'سایر',
};
const importSourceLabel = computed(() => {
const src = person.value?.importSource || 'manual';
const map: Record<string, string> = {
manual: 'دستی',
tahesab: 'ته‌حساب',
didar: 'دیدار',
excel: 'اکسل',
phone: 'مخاطبین گوشی',
lead_form: 'فرم لید',
other: 'سایر',
};
return map[src] || src;
return SOURCE_LABELS[src] || src;
});
const previousSourceLabel = computed(() => {
const src = person.value?.previousImportSource;
if (!src) return '';
return SOURCE_LABELS[src] || src;
});
const lastCallAt = computed(() => {
@@ -643,6 +650,12 @@ watch(() => route.query.tab, readTab, { immediate: true });
<span v-if="person.previousFullName" class="meta-chip">
نام قبلی: {{ person.previousFullName }}
</span>
<span v-if="previousSourceLabel" class="meta-chip prev">
قبلاً در {{ previousSourceLabel }}
<template v-if="person.previousSourceRef">
— کد <bdi>{{ toFaDigits(person.previousSourceRef) }}</bdi>
</template>
</span>
</div>
</div>
<div class="hero-side">
@@ -1185,6 +1198,11 @@ watch(() => route.query.tab, readTab, { immediate: true });
color: #0f766e;
}
.meta-chip.prev {
background: #fef3c7;
color: #92400e;
}
.hero-side {
display: grid;
justify-items: end;
+3
View File
@@ -747,6 +747,9 @@ export interface PersonDto {
previousFullName: string | null;
importSource: PersonImportSource | string;
sourceRef: string | null;
/** منبع/کد قبل از آخرین ورود از منبع دیگر (مثلاً کد دیدار پیش از ته‌حساب) */
previousImportSource: PersonImportSource | string | null;
previousSourceRef: string | null;
fullName: string;
email: string | null;
phone: string | null;