diff --git a/apps/api/prisma/migrations/20261005180000_person_previous_source/migration.sql b/apps/api/prisma/migrations/20261005180000_person_previous_source/migration.sql new file mode 100644 index 0000000..b0b32b2 --- /dev/null +++ b/apps/api/prisma/migrations/20261005180000_person_previous_source/migration.sql @@ -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'), '') <> ''; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4f10a99..c731384 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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]) diff --git a/apps/api/src/contacts/people.service.ts b/apps/api/src/contacts/people.service.ts index 375ce0c..36f8393 100644 --- a/apps/api/src/contacts/people.service.ts +++ b/apps/api/src/contacts/people.service.ts @@ -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, diff --git a/apps/api/src/contacts/person-source.ts b/apps/api/src/contacts/person-source.ts index 1aa0ebd..1caddae 100644 --- a/apps/api/src/contacts/person-source.ts +++ b/apps/api/src/contacts/person-source.ts @@ -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).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, diff --git a/apps/api/src/integrations/tahesab/person-link.service.ts b/apps/api/src/integrations/tahesab/person-link.service.ts index 0fc3bf8..38cc2dd 100644 --- a/apps/api/src/integrations/tahesab/person-link.service.ts +++ b/apps/api/src/integrations/tahesab/person-link.service.ts @@ -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, }, diff --git a/apps/web/src/views/ContactsView.vue b/apps/web/src/views/ContactsView.vue index 4a9214f..195e13d 100644 --- a/apps/web/src/views/ContactsView.vue +++ b/apps/web/src/views/ContactsView.vue @@ -655,7 +655,13 @@ onMounted(load); قبلی: {{ p.previousFullName }} - {{ sourceLabel(p.importSource || 'manual') }} + + {{ sourceLabel(p.importSource || 'manual') }} +
+ قبلاً: {{ sourceLabel(p.previousImportSource) }} + {{ toFaDigits(p.previousSourceRef) }} +
+ {{ externalCodeLabel(p) }} {{ p.position || '—' }} diff --git a/apps/web/src/views/PersonProfileView.vue b/apps/web/src/views/PersonProfileView.vue index 8db4595..b3dcad2 100644 --- a/apps/web/src/views/PersonProfileView.vue +++ b/apps/web/src/views/PersonProfileView.vue @@ -148,18 +148,25 @@ const externalCode = computed(() => { } return ''; }); +const SOURCE_LABELS: Record = { + manual: 'دستی', + tahesab: 'ته‌حساب', + didar: 'دیدار', + excel: 'اکسل', + phone: 'مخاطبین گوشی', + lead_form: 'فرم لید', + other: 'سایر', +}; + const importSourceLabel = computed(() => { const src = person.value?.importSource || 'manual'; - const map: Record = { - 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 }); نام قبلی: {{ person.previousFullName }} + + قبلاً در {{ previousSourceLabel }} + +
@@ -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; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index afebaa4..8cc73b1 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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;