fix: keep birthdays and show Shamsi dates
Imported customers keep their birthday, and assistant text plus remaining calendars use the Jalali date. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from '../common/custom-fields';
|
||||
import {
|
||||
formatJalaliDate,
|
||||
parseBirthDate,
|
||||
parseExcelMapping,
|
||||
parseJalaliDateTime,
|
||||
previewExcel,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from '../common/excel-import-session';
|
||||
import { CompaniesService } from './companies.service';
|
||||
import { PeopleService } from './people.service';
|
||||
import { normalizeEmail, normalizePhone } from './normalize';
|
||||
import { normalizePersonImportSource } from './person-source';
|
||||
|
||||
type ImportChunkOpts = {
|
||||
@@ -112,6 +114,11 @@ const PERSON_ALIASES: Record<string, string> = {
|
||||
تاریختولدمشتری: 'birthDate',
|
||||
تاریختولد1: 'birthDate',
|
||||
تاریختولد: 'birthDate',
|
||||
تاریختولدشمسی: 'birthDate',
|
||||
تاریخمیلادت: 'birthDate',
|
||||
birthdate: 'birthDate',
|
||||
dateofbirth: 'birthDate',
|
||||
bdate: 'birthDate',
|
||||
منبعاطلاعات: 'source',
|
||||
نوعمشتری: 'customerType',
|
||||
type: '_type',
|
||||
@@ -162,6 +169,7 @@ const PERSON_FIELDS = [
|
||||
{ key: 'homeAddress', label: 'آدرس منزل' },
|
||||
{ key: 'instagram', label: 'اینستاگرام' },
|
||||
{ key: 'registeredAt', label: 'تاریخ ثبت' },
|
||||
{ key: 'birthDate', label: 'تاریخ تولد' },
|
||||
{ key: 'notes', label: 'یادداشت' },
|
||||
{ key: 'mood', label: 'وضعیت مشتری' },
|
||||
];
|
||||
@@ -393,7 +401,7 @@ export class ContactsExcelService {
|
||||
visibility: 'همه افراد شرکت',
|
||||
nationalId: extra.nationalId ?? '',
|
||||
registeredAt: formatJalaliDate(p.createdAt),
|
||||
birthDate: extra.birthDate ?? '',
|
||||
birthDate: p.birthDate ? formatJalaliDate(p.birthDate) : (extra.birthDate ?? ''),
|
||||
mood: extra.group || PERSON_MOOD_LABELS[p.mood || 'newcomer'],
|
||||
otherPhones: extra.otherPhones ?? '',
|
||||
address: p.addresses[0] ?? extra.address ?? '',
|
||||
@@ -408,7 +416,7 @@ export class ContactsExcelService {
|
||||
postalCode2: extra.postalCode ?? '',
|
||||
referrer: extra.referrer ?? p.referrer ?? '',
|
||||
homeAddress: p.addresses[2] ?? extra.homeAddress ?? '',
|
||||
birthDate2: extra.birthDate ?? '',
|
||||
birthDate2: p.birthDate ? formatJalaliDate(p.birthDate) : (extra.birthDate ?? ''),
|
||||
instagram: p.instagram ?? extra.instagram ?? '',
|
||||
group: extra.group ?? '',
|
||||
colleagueContact: extra.colleagueContact ?? '',
|
||||
@@ -578,6 +586,7 @@ export class ContactsExcelService {
|
||||
position: row.position || undefined,
|
||||
website: row.website || undefined,
|
||||
instagram: row.instagram || undefined,
|
||||
birthDate: parseBirthDate(row.birthDate)?.toISOString() ?? null,
|
||||
addresses,
|
||||
notes: row.notes || undefined,
|
||||
mood: parseMood(row.mood) ?? parseMood(row.group),
|
||||
@@ -597,6 +606,14 @@ export class ContactsExcelService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ConflictException) {
|
||||
const birthDate = parseBirthDate(row.birthDate);
|
||||
if (birthDate) {
|
||||
await this.people.fillBirthDateIfEmpty(actor.organizationId, {
|
||||
mobile: normalizePhone(row.mobile),
|
||||
email: normalizeEmail(row.email),
|
||||
birthDate,
|
||||
});
|
||||
}
|
||||
result.skipped += 1;
|
||||
} else {
|
||||
result.errors.push({
|
||||
|
||||
@@ -293,6 +293,11 @@ export class CreatePersonDto {
|
||||
@MaxLength(200)
|
||||
instagram?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsString()
|
||||
birthDate?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
@@ -401,6 +406,11 @@ export class UpdatePersonDto {
|
||||
@MaxLength(200)
|
||||
instagram?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== '' && v != null)
|
||||
@IsString()
|
||||
birthDate?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(120)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
type OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
CreatePersonRequest,
|
||||
@@ -32,6 +34,7 @@ import { AutomationsService } from '../automations/automations.service';
|
||||
import { MessageAutomationsService } from '../message-automations/message-automations.service';
|
||||
import { WebhooksService } from '../public-api/webhooks.service';
|
||||
import { GamificationService } from '../gamification/gamification.service';
|
||||
import { parseBirthDate } from '../common/excel';
|
||||
import { normalizeTags } from '../common/tags';
|
||||
import { buildSearchWhere, ci } from '../common/search';
|
||||
import {
|
||||
@@ -63,6 +66,7 @@ type PersonRow = {
|
||||
position: string | null;
|
||||
website: string | null;
|
||||
instagram: string | null;
|
||||
birthDate: Date | null;
|
||||
referrer: string | null;
|
||||
addresses: unknown;
|
||||
notes: string | null;
|
||||
@@ -83,7 +87,8 @@ type PersonRow = {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PeopleService {
|
||||
export class PeopleService implements OnModuleInit {
|
||||
private readonly logger = new Logger(PeopleService.name);
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly companies: CompaniesService,
|
||||
@@ -168,6 +173,71 @@ export class PeopleService {
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
void this.backfillStoredBirthDates();
|
||||
}
|
||||
|
||||
/** تاریخ تولدی که قبلاً در فیلد اضافی دیدار/اکسل یا پروفایل مونتیفای مانده، روی شخص مینشیند. */
|
||||
async backfillStoredBirthDates() {
|
||||
try {
|
||||
const fromFields = await this.prisma.$queryRaw<Array<{ id: string; birth: string }>>`
|
||||
SELECT id,
|
||||
COALESCE(
|
||||
NULLIF("customFields"->>'birthDate', ''),
|
||||
NULLIF("customFields"->>'birthDate2', '')
|
||||
) AS birth
|
||||
FROM "Person"
|
||||
WHERE "birthDate" IS NULL
|
||||
AND (
|
||||
COALESCE("customFields"->>'birthDate', '') <> ''
|
||||
OR COALESCE("customFields"->>'birthDate2', '') <> ''
|
||||
)
|
||||
LIMIT 20000
|
||||
`;
|
||||
const fromMontify = await this.prisma.$queryRaw<Array<{ id: string; birth: string }>>`
|
||||
SELECT p.id, m.data->>'birthday' AS birth
|
||||
FROM "MontifyLink" m
|
||||
JOIN "Person" p ON p.id = m."entityId"
|
||||
WHERE m."entityType" = 'customer'
|
||||
AND p."birthDate" IS NULL
|
||||
AND COALESCE(m.data->>'birthday', '') <> ''
|
||||
LIMIT 20000
|
||||
`;
|
||||
const seen = new Set<string>();
|
||||
let filled = 0;
|
||||
for (const row of [...fromFields, ...fromMontify]) {
|
||||
if (!row.birth || seen.has(row.id)) continue;
|
||||
seen.add(row.id);
|
||||
const birthDate = parseBirthDate(row.birth);
|
||||
if (!birthDate) continue;
|
||||
const updated = await this.prisma.person.updateMany({
|
||||
where: { id: row.id, birthDate: null },
|
||||
data: { birthDate },
|
||||
});
|
||||
filled += updated.count;
|
||||
}
|
||||
if (filled) this.logger.log(`تاریخ تولد ${filled} مشتری از دادههای ذخیرهشده پر شد`);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`پر کردن تاریخ تولد ذخیرهشده ناموفق بود: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fillBirthDateIfEmpty(
|
||||
organizationId: string,
|
||||
input: { mobile?: string | null; email?: string | null; birthDate: Date },
|
||||
) {
|
||||
const or: Prisma.PersonWhereInput[] = [];
|
||||
if (input.mobile) or.push({ mobile: input.mobile });
|
||||
if (input.email) or.push({ email: input.email });
|
||||
if (!or.length) return;
|
||||
await this.prisma.person.updateMany({
|
||||
where: { organizationId, birthDate: null, OR: or },
|
||||
data: { birthDate: input.birthDate },
|
||||
});
|
||||
}
|
||||
|
||||
async get(actor: Actor, id: string): Promise<PersonDto> {
|
||||
const row = await this.prisma.person.findFirst({
|
||||
where: { id, organizationId: actor.organizationId, ...ownerWhere(actor) },
|
||||
@@ -198,6 +268,7 @@ export class PeopleService {
|
||||
position: emptyToNull(dto.position),
|
||||
website: emptyToNull(dto.website),
|
||||
instagram: emptyToNull(dto.instagram),
|
||||
birthDate: parseBirthDate(dto.birthDate),
|
||||
referrer: emptyToNull(dto.referrer),
|
||||
addresses: toJsonAddresses(dto.addresses),
|
||||
notes: emptyToNull(dto.notes),
|
||||
@@ -279,6 +350,7 @@ export class PeopleService {
|
||||
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 ('birthDate' in dto) data.birthDate = parseBirthDate(dto.birthDate);
|
||||
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);
|
||||
@@ -396,6 +468,7 @@ export class PeopleService {
|
||||
position: row.position,
|
||||
website: row.website,
|
||||
instagram: row.instagram,
|
||||
birthDate: row.birthDate?.toISOString() ?? null,
|
||||
referrer: row.referrer,
|
||||
addresses: (() => {
|
||||
const fromCol = asAddresses(row.addresses);
|
||||
|
||||
Reference in New Issue
Block a user