Adding registration date and multiple addresses for contacts and leads.

This commit is contained in:
2026-10-03 12:29:11 +03:30
parent 989c03b381
commit 1309da2281
22 changed files with 699 additions and 32 deletions
@@ -4,9 +4,13 @@ import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import type { Actor } from '../common/record-scope';
import { ownerWhere, resolveOwnerId } from '../common/record-scope';
import {
collectAddresses,
} from '../common/custom-fields';
import {
formatJalaliDate,
parseExcelMapping,
parseJalaliDateTime,
previewExcel,
readMappedRows,
writeXlsx,
@@ -73,8 +77,17 @@ const PERSON_ALIASES: Record<string, string> = {
گروهمشتری: 'group',
آدرس: 'address',
آدرس1: 'address',
آدرساصلی: 'address',
address: 'address',
address2: 'address2',
آدرس۲: 'address2',
آدرس2: 'address2',
آدرس_1: 'address2',
آدرسدوم: 'address2',
آدرسمنزل: 'homeAddress',
آدرمنزل: 'homeAddress',
آدرسخانه: 'homeAddress',
homeaddress: 'homeAddress',
کدپستی: 'postalCode',
کدپستی1: 'postalCode',
استانفیلدپیشفرض: 'province',
@@ -93,8 +106,13 @@ const PERSON_ALIASES: Record<string, string> = {
type: '_type',
_type: '_type',
آیدیاینستاگرام: 'instagram',
اینستاگرام: 'instagram',
پیجاینستاگرام: 'instagram',
instagram: 'instagram',
معرف: 'referrer',
سایتهایمشتری: 'website',
تاریخثبت: 'registeredAt',
registeredat: 'registeredAt',
شمارهفکس: 'fax',
تلفن2: 'phone2',
تلفنهایدیگرمشتری: 'otherPhones',
@@ -128,6 +146,11 @@ const PERSON_FIELDS = [
{ key: 'position', label: 'سمت' },
{ key: 'companyName', label: 'شرکت' },
{ key: 'ownerName', label: 'کارشناس فروش' },
{ key: 'address', label: 'آدرس' },
{ key: 'address2', label: 'آدرس ۲' },
{ key: 'homeAddress', label: 'آدرس منزل' },
{ key: 'instagram', label: 'اینستاگرام' },
{ key: 'registeredAt', label: 'تاریخ ثبت' },
{ key: 'notes', label: 'یادداشت' },
{ key: 'mood', label: 'وضعیت مشتری' },
];
@@ -218,6 +241,7 @@ const CUSTOM_KEYS = [
'province',
'city',
'address',
'address2',
'nationalId',
'birthDate',
'group',
@@ -279,20 +303,20 @@ export class ContactsExcelService {
birthDate: extra.birthDate ?? '',
mood: extra.group || PERSON_MOOD_LABELS[p.mood || 'newcomer'],
otherPhones: extra.otherPhones ?? '',
address: extra.address ?? '',
website: extra.website ?? '',
address: p.addresses[0] ?? extra.address ?? '',
website: p.website ?? extra.website ?? '',
otherEmails: extra.otherEmails ?? '',
position2: p.position ?? extra.position ?? '',
city2: extra.city ?? '',
code2: extra.customerCode ?? '',
group2: extra.group ?? '',
address2: extra.address ?? '',
address2: p.addresses[1] ?? extra.address ?? '',
phone2: extra.phone2 ?? p.phone ?? '',
postalCode2: extra.postalCode ?? '',
referrer: extra.referrer ?? '',
homeAddress: extra.homeAddress ?? '',
referrer: extra.referrer ?? p.referrer ?? '',
homeAddress: p.addresses[2] ?? extra.homeAddress ?? '',
birthDate2: extra.birthDate ?? '',
instagram: extra.instagram ?? '',
instagram: p.instagram ?? extra.instagram ?? '',
group: extra.group ?? '',
colleagueContact: extra.colleagueContact ?? '',
previousCompany: extra.previousCompany ?? '',
@@ -388,10 +412,33 @@ export class ContactsExcelService {
const customFields: Record<string, string> = {};
for (const key of CUSTOM_KEYS) {
if (
key === 'address' ||
key === 'address2' ||
key === 'homeAddress' ||
key === 'instagram' ||
key === 'website'
) {
continue;
}
const value = row[key]?.trim();
if (value) customFields[key] = value;
}
const addresses = collectAddresses(
row.address,
row.address2,
row.homeAddress,
);
const registeredAt =
parseJalaliDateTime(row.registeredAt) ??
(() => {
const raw = row.registeredAt?.trim();
if (!raw) return null;
const d = new Date(raw);
return Number.isNaN(d.getTime()) ? null : d;
})();
try {
await this.people.create(actor, {
firstName: names.firstName,
@@ -400,11 +447,15 @@ export class ContactsExcelService {
phone: row.phone || row.phone2 || undefined,
mobile: row.mobile || undefined,
position: row.position || undefined,
website: row.website || undefined,
instagram: row.instagram || undefined,
addresses,
notes: row.notes || undefined,
mood: parseMood(row.mood) ?? parseMood(row.group),
companyId,
ownerId: resolveOwnerId(actor, ownerId ?? null),
customFields,
registeredAt: registeredAt?.toISOString() ?? null,
});
result.created += 1;
if (ownerMissing) {
+25
View File
@@ -181,11 +181,21 @@ export class CreatePersonDto {
@MaxLength(300)
website?: string;
@IsOptional()
@IsString()
@MaxLength(200)
instagram?: string;
@IsOptional()
@IsString()
@MaxLength(120)
referrer?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
addresses?: string[];
@IsOptional()
@IsString()
@MaxLength(2000)
@@ -213,6 +223,11 @@ export class CreatePersonDto {
@ValidateIf((_, v) => v !== '' && v != null)
@IsString()
ownerId?: string | null;
/** فقط برای ایمپورت اکسل — تاریخ ثبت مبدأ */
@IsOptional()
@IsString()
registeredAt?: string | null;
}
export class UpdatePersonDto {
@@ -251,11 +266,21 @@ export class UpdatePersonDto {
@MaxLength(300)
website?: string;
@IsOptional()
@IsString()
@MaxLength(200)
instagram?: string;
@IsOptional()
@IsString()
@MaxLength(120)
referrer?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
addresses?: string[];
@IsOptional()
@IsString()
@MaxLength(2000)
+23
View File
@@ -15,7 +15,9 @@ import { PrismaService } from '../prisma/prisma.service';
import { CompaniesService } from './companies.service';
import { emptyToNull, normalizeEmail, normalizePhone } from './normalize';
import {
asAddresses,
asCustomFields,
toJsonAddresses,
toJsonCustomFields,
} from '../common/custom-fields';
import {
@@ -47,7 +49,9 @@ type PersonRow = {
mobile: string | null;
position: string | null;
website: string | null;
instagram: string | null;
referrer: string | null;
addresses: unknown;
notes: string | null;
mood: PersonMood;
tags: string[];
@@ -138,6 +142,7 @@ export class PeopleService {
await this.companies.ensureInOrg(actor.organizationId, companyId, actor);
}
const registeredAt = dto.registeredAt ? new Date(dto.registeredAt) : null;
const data: Prisma.PersonUncheckedCreateInput = {
organizationId: actor.organizationId,
firstName: (dto.firstName ?? '').trim(),
@@ -147,13 +152,18 @@ export class PeopleService {
mobile: normalizePhone(dto.mobile),
position: emptyToNull(dto.position),
website: emptyToNull(dto.website),
instagram: emptyToNull(dto.instagram),
referrer: emptyToNull(dto.referrer),
addresses: toJsonAddresses(dto.addresses),
notes: emptyToNull(dto.notes),
mood: dto.mood ?? 'newcomer',
tags: normalizeTags(dto.tags),
companyId,
ownerId: resolveOwnerId(actor, dto.ownerId),
customFields: toJsonCustomFields(dto.customFields),
...(registeredAt && !Number.isNaN(registeredAt.getTime())
? { createdAt: registeredAt }
: {}),
};
await this.assertNoDuplicate(actor.organizationId, data);
@@ -202,7 +212,9 @@ export class PeopleService {
if ('mobile' in dto) data.mobile = normalizePhone(dto.mobile);
if ('position' in dto) data.position = emptyToNull(dto.position);
if ('website' in dto) data.website = emptyToNull(dto.website);
if ('instagram' in dto) data.instagram = emptyToNull(dto.instagram);
if ('referrer' in dto) data.referrer = emptyToNull(dto.referrer);
if ('addresses' in dto) data.addresses = toJsonAddresses(dto.addresses);
if ('notes' in dto) data.notes = emptyToNull(dto.notes);
if (dto.mood !== undefined) data.mood = dto.mood;
if ('tags' in dto) data.tags = normalizeTags(dto.tags);
@@ -295,7 +307,18 @@ export class PeopleService {
mobile: row.mobile,
position: row.position,
website: row.website,
instagram: row.instagram,
referrer: row.referrer,
addresses: (() => {
const fromCol = asAddresses(row.addresses);
if (fromCol.length) return fromCol;
const extra = asCustomFields(row.customFields);
return asAddresses(
[extra.address, extra.homeAddress, extra.address2]
.map((v) => (typeof v === 'string' ? v : ''))
.filter(Boolean),
);
})(),
notes: row.notes,
mood: row.mood,
tags: row.tags ?? [],