diff --git a/apps/api/src/common/excel.ts b/apps/api/src/common/excel.ts index aaae836..29aba3c 100644 --- a/apps/api/src/common/excel.ts +++ b/apps/api/src/common/excel.ts @@ -170,9 +170,34 @@ export function parseAmount(raw?: string): number | null { } /** تاریخ تولد: سال زیر ۱۷۰۰ شمسی است، سال ۱۷۰۰ به بعد میلادی. ظهر UTC تا روز جابه‌جا نشود. */ +/** «۲۰۲۴-۰۵-۰۱T۱۹:۳۰:۰۰Z» و مشابه: لحظهٔ دقیق با منطقهٔ زمانی */ +const ZONED_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i; + +function tehranCalendarDay(d: Date): { y: number; m: number; d: number } { + const [y, m, day] = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Asia/Tehran', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + .format(d) + .split('-') + .map(Number); + return { y, m, d: day }; +} + export function parseBirthDate(raw?: string | null): Date | null { const text = toAsciiDigits(String(raw ?? '')).trim(); if (!text || text === '-' || text === '0' || text === '0000-00-00') return null; + // تاریخ تولد فقط «روز» است. لحظهٔ UTC (مثلاً نیمه‌شب تهران که به روز قبلِ UTC می‌افتد) + // باید با تقویم تهران به روز تبدیل شود، نه با بریدن بخش تاریخِ UTC. + if (ZONED_TIMESTAMP.test(text)) { + const instant = new Date(text); + if (!Number.isNaN(instant.getTime())) { + const day = tehranCalendarDay(instant); + return new Date(Date.UTC(day.y, day.m - 1, day.d, 12, 0, 0)); + } + } const datePart = text.split(/[T\s]/)[0] ?? text; const match = /^(\d{3,4})[\/\-.](\d{1,2})[\/\-.](\d{1,2})$/.exec(datePart); if (!match) { diff --git a/apps/web/src/views/LeadDetailView.vue b/apps/web/src/views/LeadDetailView.vue index a50eb57..f2dec61 100644 --- a/apps/web/src/views/LeadDetailView.vue +++ b/apps/web/src/views/LeadDetailView.vue @@ -206,9 +206,7 @@ async function saveEdit() { region: editForm.value.region.trim() || null, source: editForm.value.source.trim() || null, instagram: editForm.value.instagram.trim() || null, - birthDate: editForm.value.birthDate - ? (parseLocalInput(editForm.value.birthDate)?.toISOString() ?? null) - : null, + birthDate: editForm.value.birthDate || null, trade: editForm.value.trade.trim() || null, notes: editForm.value.notes.trim() || null, }); diff --git a/apps/web/src/views/PersonFormView.vue b/apps/web/src/views/PersonFormView.vue index 3f10955..0c2df10 100644 --- a/apps/web/src/views/PersonFormView.vue +++ b/apps/web/src/views/PersonFormView.vue @@ -36,7 +36,7 @@ import { import { api } from '@/lib/api'; import type { CompanyDto, CreatePersonRequest, PersonDto, PersonMood } from '@mento/shared'; import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood'; -import { formatLocalDateInput, parseLocalInput, toFaDigits } from '@/lib/jalali'; +import { formatLocalDateInput, toFaDigits } from '@/lib/jalali'; import JalaliDateInput from '@/components/JalaliDateInput.vue'; import { filterPhoneContacts, @@ -327,7 +327,7 @@ function payload(): CreatePersonRequest { position: position.value || undefined, website: website.value || undefined, instagram: instagram.value || undefined, - birthDate: birthDate.value ? parseLocalInput(birthDate.value)?.toISOString() ?? null : null, + birthDate: birthDate.value || null, referrer: referrer.value || undefined, addresses: addresses.value.map((a) => a.trim()).filter(Boolean), notes: notes.value || undefined,