From f71f09c85d406f2e362331d20e65a74755b48b63 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Tue, 6 Oct 2026 12:25:20 +0330 Subject: [PATCH] Align companies with the Didar export: new fields, Didar-format Excel import/export, and people linked to companies by Didar code --- .../migration.sql | 33 ++ apps/api/prisma/schema.prisma | 18 ++ apps/api/src/contacts/companies.service.ts | 66 ++++ .../src/contacts/contacts-excel.service.ts | 304 +++++++++++++++--- apps/api/src/contacts/dto.ts | 71 +++- apps/web/src/views/CompanyFormView.vue | 155 ++++++--- apps/web/src/views/ContactsView.vue | 40 ++- packages/shared/src/index.ts | 28 ++ 8 files changed, 630 insertions(+), 85 deletions(-) create mode 100644 apps/api/prisma/migrations/20261006090000_company_didar_fields/migration.sql diff --git a/apps/api/prisma/migrations/20261006090000_company_didar_fields/migration.sql b/apps/api/prisma/migrations/20261006090000_company_didar_fields/migration.sql new file mode 100644 index 0000000..d033da7 --- /dev/null +++ b/apps/api/prisma/migrations/20261006090000_company_didar_fields/migration.sql @@ -0,0 +1,33 @@ +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "code" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "importSource" TEXT NOT NULL DEFAULT 'manual'; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "sourceRef" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "economicCode" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "registrationNumber" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "otherPhones" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "fax" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "otherEmails" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "province" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "city" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "postalCode" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "groupName" TEXT; +ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "registeredAt" TIMESTAMP(3); + +CREATE INDEX IF NOT EXISTS "Company_organizationId_sourceRef_idx" ON "Company"("organizationId", "sourceRef"); +CREATE INDEX IF NOT EXISTS "Company_organizationId_code_idx" ON "Company"("organizationId", "code"); + +-- شرکت‌هایی که از ورود اشخاص دیدار ساخته شده‌اند: کد دیدار شرکت را از customFields اشخاص بازیابی کن +UPDATE "Company" c +SET "sourceRef" = sub.didar_code, + "importSource" = 'didar' +FROM ( + SELECT DISTINCT ON (p."companyId") + p."companyId" AS company_id, + TRIM(p."customFields"->>'companyDidarId') AS didar_code + FROM "Person" p + WHERE p."companyId" IS NOT NULL + AND jsonb_typeof(p."customFields") = 'object' + AND COALESCE(TRIM(p."customFields"->>'companyDidarId'), '') <> '' + ORDER BY p."companyId", p."createdAt" +) sub +WHERE c."id" = sub.company_id + AND c."sourceRef" IS NULL; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index c731384..c508eba 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -398,12 +398,28 @@ model Company { organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) name String + /// کد داخلی شرکت (مثل «کد شرکت» در دیدار) + code String? + /// منبع ورود: manual | didar | tahesab | excel | other + importSource String @default("manual") + /// شناسه در مبدأ (مثلاً کد دیدار شرکت) + sourceRef String? nationalId String? + economicCode String? + registrationNumber String? phone String? + otherPhones String? + fax String? email String? + otherEmails String? website String? + province String? + city String? + postalCode String? address String? + groupName String? notes String? + registeredAt DateTime? tags String[] @default([]) customFields Json @default("{}") people Person[] @@ -423,6 +439,8 @@ model Company { @@index([organizationId, name]) @@index([organizationId, email]) @@index([organizationId, phone]) + @@index([organizationId, sourceRef]) + @@index([organizationId, code]) @@index([ownerId]) } diff --git a/apps/api/src/contacts/companies.service.ts b/apps/api/src/contacts/companies.service.ts index b527e74..dea6c8a 100644 --- a/apps/api/src/contacts/companies.service.ts +++ b/apps/api/src/contacts/companies.service.ts @@ -24,6 +24,13 @@ import { resolveOwnerId, } from '../common/record-scope'; import { normalizeTags } from '../common/tags'; +import { normalizePersonImportSource } from './person-source'; + +function parseDateOrNull(raw?: string | null): Date | null { + if (!raw?.trim()) return null; + const d = new Date(raw); + return Number.isNaN(d.getTime()) ? null : d; +} const companyInclude = { _count: { select: { people: true } }, @@ -33,12 +40,25 @@ const companyInclude = { type CompanyRow = { id: string; name: string; + code: string | null; + importSource: string; + sourceRef: string | null; nationalId: string | null; + economicCode: string | null; + registrationNumber: string | null; phone: string | null; + otherPhones: string | null; + fax: string | null; email: string | null; + otherEmails: string | null; website: string | null; + province: string | null; + city: string | null; + postalCode: string | null; address: string | null; + groupName: string | null; notes: string | null; + registeredAt: Date | null; tags: string[]; customFields: unknown; createdAt: Date; @@ -67,6 +87,9 @@ export class CompaniesService { { email: { contains: q.trim(), mode: 'insensitive' } }, { phone: { contains: q.trim() } }, { nationalId: { contains: q.trim() } }, + { code: { contains: q.trim() } }, + { sourceRef: { contains: q.trim() } }, + { city: { contains: q.trim(), mode: 'insensitive' } }, ], } : {}), @@ -104,12 +127,25 @@ export class CompaniesService { const data: Prisma.CompanyUncheckedCreateInput = { organizationId: actor.organizationId, name: dto.name.trim(), + code: emptyToNull(dto.code), + importSource: normalizePersonImportSource(dto.importSource), + sourceRef: emptyToNull(dto.sourceRef), nationalId: emptyToNull(dto.nationalId), + economicCode: emptyToNull(dto.economicCode), + registrationNumber: emptyToNull(dto.registrationNumber), phone: normalizePhone(dto.phone), + otherPhones: emptyToNull(dto.otherPhones), + fax: emptyToNull(dto.fax), email: normalizeEmail(dto.email), + otherEmails: emptyToNull(dto.otherEmails), website: emptyToNull(dto.website), + province: emptyToNull(dto.province), + city: emptyToNull(dto.city), + postalCode: emptyToNull(dto.postalCode), address: emptyToNull(dto.address), + groupName: emptyToNull(dto.groupName), notes: emptyToNull(dto.notes), + registeredAt: parseDateOrNull(dto.registeredAt), tags: normalizeTags(dto.tags), ownerId: resolveOwnerId(actor, dto.ownerId), customFields: toJsonCustomFields(dto.customFields), @@ -131,12 +167,29 @@ export class CompaniesService { await this.ensureExists(actor.organizationId, id, actor); const data: Prisma.CompanyUncheckedUpdateInput = {}; if (dto.name !== undefined) data.name = dto.name.trim(); + if ('code' in dto) data.code = emptyToNull(dto.code); + if (dto.importSource !== undefined) { + data.importSource = normalizePersonImportSource(dto.importSource); + } + if ('sourceRef' in dto) data.sourceRef = emptyToNull(dto.sourceRef); if ('nationalId' in dto) data.nationalId = emptyToNull(dto.nationalId); + if ('economicCode' in dto) data.economicCode = emptyToNull(dto.economicCode); + if ('registrationNumber' in dto) { + data.registrationNumber = emptyToNull(dto.registrationNumber); + } if ('phone' in dto) data.phone = normalizePhone(dto.phone); + if ('otherPhones' in dto) data.otherPhones = emptyToNull(dto.otherPhones); + if ('fax' in dto) data.fax = emptyToNull(dto.fax); if ('email' in dto) data.email = normalizeEmail(dto.email); + if ('otherEmails' in dto) data.otherEmails = emptyToNull(dto.otherEmails); if ('website' in dto) data.website = emptyToNull(dto.website); + if ('province' in dto) data.province = emptyToNull(dto.province); + if ('city' in dto) data.city = emptyToNull(dto.city); + if ('postalCode' in dto) data.postalCode = emptyToNull(dto.postalCode); if ('address' in dto) data.address = emptyToNull(dto.address); + if ('groupName' in dto) data.groupName = emptyToNull(dto.groupName); if ('notes' in dto) data.notes = emptyToNull(dto.notes); + if ('registeredAt' in dto) data.registeredAt = parseDateOrNull(dto.registeredAt); if ('tags' in dto) data.tags = normalizeTags(dto.tags); if ('customFields' in dto) { data.customFields = toJsonCustomFields(dto.customFields); @@ -226,12 +279,25 @@ export class CompaniesService { return { id: row.id, name: row.name, + code: row.code, + importSource: row.importSource || 'manual', + sourceRef: row.sourceRef, nationalId: row.nationalId, + economicCode: row.economicCode, + registrationNumber: row.registrationNumber, phone: row.phone, + otherPhones: row.otherPhones, + fax: row.fax, email: row.email, + otherEmails: row.otherEmails, website: row.website, + province: row.province, + city: row.city, + postalCode: row.postalCode, address: row.address, + groupName: row.groupName, notes: row.notes, + registeredAt: row.registeredAt ? row.registeredAt.toISOString() : null, tags: row.tags ?? [], customFields: asCustomFields(row.customFields), peopleCount: row._count.people, diff --git a/apps/api/src/contacts/contacts-excel.service.ts b/apps/api/src/contacts/contacts-excel.service.ts index 4a406bf..068d77b 100644 --- a/apps/api/src/contacts/contacts-excel.service.ts +++ b/apps/api/src/contacts/contacts-excel.service.ts @@ -167,34 +167,116 @@ const PERSON_FIELDS = [ ]; const COMPANY_ALIASES: Record = { + type: '_type', + _type: '_type', name: 'name', نام: 'name', نامشرکت: 'name', + کددیدارشرکت: 'didarId', + didarid: 'didarId', + کدشرکت: 'code', + code: 'code', + استانفیلدپیشفرض: 'province', + استان: 'province', + province: 'province', + شهرستان: 'city', + شهر: 'city', + city: 'city', nationalid: 'nationalId', شناسهملی: 'nationalId', + شمارهاقتصادی: 'economicCode', + کداقتصادی: 'economicCode', + شمارهثبت: 'registrationNumber', phone: 'phone', تلفن: 'phone', + تلفنشرکت: 'phone', تلفنثابتشرکت: 'phone', + تلفنهایدیگرشرکت: 'otherPhones', + شمارهفکس: 'fax', + فکس: 'fax', + fax: 'fax', email: 'email', ایمیل: 'email', + ایمیلشرکت: 'email', + ایمیلهایدیگرشرکت: 'otherEmails', website: 'website', وبسایت: 'website', + سایتهایشرکت: 'website', + کدپستی: 'postalCode', + postalcode: 'postalCode', address: 'address', آدرس: 'address', notes: 'notes', یادداشت: 'notes', + توضیحاتشرکت: 'notes', + مسئولشرکت: 'ownerName', + مسئول: 'ownerName', + owner: 'ownerName', + مجوزمشاهدهشرکت: 'visibility', + تاریخثبتشرکت: 'registeredAt', + تاریخثبت: 'registeredAt', + گروهبندیشرکت: 'groupName', + گروه: 'groupName', }; const COMPANY_FIELDS = [ { key: 'name', label: 'نام شرکت', required: true }, + { key: 'didarId', label: 'کد دیدار شرکت' }, + { key: 'code', label: 'کد شرکت' }, + { key: 'province', label: 'استان' }, + { key: 'city', label: 'شهرستان' }, + { key: 'phone', label: 'تلفن شرکت' }, + { key: 'email', label: 'ایمیل شرکت' }, + { key: 'economicCode', label: 'شماره اقتصادی' }, + { key: 'registrationNumber', label: 'شماره ثبت' }, { key: 'nationalId', label: 'شناسه ملی' }, - { key: 'phone', label: 'تلفن' }, - { key: 'email', label: 'ایمیل' }, - { key: 'website', label: 'وب‌سایت' }, + { key: 'fax', label: 'شماره فکس' }, + { key: 'postalCode', label: 'کد پستی' }, + { key: 'notes', label: 'توضیحات شرکت' }, + { key: 'ownerName', label: 'مسئول شرکت' }, + { key: 'visibility', label: 'مجوز مشاهده شرکت' }, + { key: 'registeredAt', label: 'تاریخ ثبت شرکت' }, + { key: 'groupName', label: 'گروه بندی شرکت' }, + { key: 'otherPhones', label: 'تلفن های دیگر شرکت' }, { key: 'address', label: 'آدرس' }, - { key: 'notes', label: 'یادداشت' }, + { key: 'website', label: 'سایت های شرکت' }, + { key: 'otherEmails', label: 'ایمیل های دیگر شرکت' }, ]; +const COMPANY_COLUMNS = [ + { header: '_type', key: '_type', width: 12 }, + { header: 'کد دیدار شرکت', key: 'didarId', width: 14 }, + { header: 'کد شرکت', key: 'code', width: 12 }, + { header: 'نام شرکت', key: 'name', width: 28 }, + { header: 'استان (فیلد پیش فرض)', key: 'province', width: 16 }, + { header: 'شهرستان', key: 'city', width: 14 }, + { header: 'تلفن شرکت', key: 'phone', width: 16 }, + { header: 'ایمیل شرکت', key: 'email', width: 24 }, + { header: 'شماره اقتصادی', key: 'economicCode', width: 16 }, + { header: 'شماره ثبت', key: 'registrationNumber', width: 14 }, + { header: 'شناسه ملی', key: 'nationalId', width: 16 }, + { header: 'شماره فکس', key: 'fax', width: 14 }, + { header: 'کد پستی', key: 'postalCode', width: 12 }, + { header: 'توضیحات شرکت', key: 'notes', width: 32 }, + { header: 'مسئول شرکت', key: 'ownerName', width: 16 }, + { header: 'مجوز مشاهده شرکت', key: 'visibility', width: 18 }, + { header: 'تاریخ ثبت شرکت', key: 'registeredAt', width: 16 }, + { header: 'گروه بندی شرکت', key: 'groupName', width: 16 }, + { header: 'تلفن های دیگر شرکت', key: 'otherPhones', width: 18 }, + { header: 'آدرس', key: 'address', width: 28 }, + { header: 'سایت های شرکت', key: 'website', width: 20 }, + { header: 'ایمیل های دیگر شرکت', key: 'otherEmails', width: 20 }, +]; + +function parseLooseDate(raw?: string): Date | null { + const jalali = parseJalaliDateTime(raw); + if (jalali) return jalali; + const v = raw?.trim(); + if (!v) return null; + const d = new Date(v); + return Number.isNaN(d.getTime()) ? null : d; +} + const CONTACT_COLUMNS = [ { header: '_type', key: '_type', width: 12 }, { header: 'کد دیدار مشتری', key: 'didarId', width: 16 }, @@ -342,25 +424,37 @@ export class ContactsExcelService { async exportCompanies(actor: Actor, q?: string): Promise { const { items } = await this.companies.list(actor, q, 1, 5000); return writeXlsx( - 'شرکت‌ها', - [ - { header: 'نام شرکت', key: 'name', width: 28 }, - { header: 'شناسه ملی', key: 'nationalId', width: 16 }, - { header: 'تلفن', key: 'phone', width: 16 }, - { header: 'ایمیل', key: 'email', width: 28 }, - { header: 'وب‌سایت', key: 'website', width: 24 }, - { header: 'آدرس', key: 'address', width: 32 }, - { header: 'یادداشت', key: 'notes', width: 32 }, - ], - items.map((c) => ({ - name: c.name, - nationalId: c.nationalId ?? '', - phone: c.phone ?? '', - email: c.email ?? '', - website: c.website ?? '', - address: c.address ?? '', - notes: c.notes ?? '', - })), + 'Company', + COMPANY_COLUMNS, + items.map((c) => { + const extra = (c.customFields ?? {}) as Record; + return { + _type: 'Company', + didarId: c.importSource === 'didar' && c.sourceRef ? c.sourceRef : '', + code: c.code ?? '', + name: c.name, + province: c.province ?? '', + city: c.city ?? '', + phone: c.phone ?? '', + email: c.email ?? '', + economicCode: c.economicCode ?? '', + registrationNumber: c.registrationNumber ?? '', + nationalId: c.nationalId ?? '', + fax: c.fax ?? '', + postalCode: c.postalCode ?? '', + notes: c.notes ?? '', + ownerName: c.owner?.fullName ?? '', + visibility: extra.visibility || 'همه افراد شرکت', + registeredAt: formatJalaliDate( + c.registeredAt ? new Date(c.registeredAt) : new Date(c.createdAt), + ), + groupName: c.groupName ?? '', + otherPhones: c.otherPhones ?? '', + address: c.address ?? '', + website: c.website ?? '', + otherEmails: c.otherEmails ?? '', + }; + }), ); } @@ -414,7 +508,12 @@ export class ContactsExcelService { let companyId: string | null = null; const companyName = row.companyName?.trim(); if (companyName) { - companyId = await this.findOrCreateCompanyId(actor, companyName); + companyId = await this.findOrCreateCompanyId( + actor, + companyName, + row.companyDidarId, + row.companyPhone, + ); } const ownerName = row.ownerName?.trim() ?? ''; @@ -527,26 +626,91 @@ export class ContactsExcelService { readMappedRows(file!, COMPANY_ALIASES, parseExcelMapping(mappingRaw)), }); const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] }; + const ownerCache = new Map(); for (let i = 0; i < ctx.slice.length; i++) { const row = ctx.slice[i]!; const rowNum = ctx.offset + i + 2; + if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'company') { + continue; + } const name = row.name?.trim() ?? ''; if (!name) { result.errors.push({ row: rowNum, message: 'نام شرکت الزامی است' }); continue; } + + const ownerName = row.ownerName?.trim() ?? ''; + let ownerId: string | undefined; + let ownerMissing = false; + if (ownerName) { + const key = ownerName.toLowerCase(); + if (!ownerCache.has(key)) { + ownerCache.set(key, await this.findUserId(actor.organizationId, ownerName)); + } + const matched = ownerCache.get(key); + if (matched) ownerId = matched; + else ownerMissing = true; + } + + const didarId = row.didarId?.trim() || ''; + const customFields: Record = {}; + const visibility = row.visibility?.trim(); + if (visibility) customFields.visibility = visibility; + const registeredAt = parseLooseDate(row.registeredAt); + + const payload = { + name, + code: row.code?.trim() || null, + importSource: didarId ? 'didar' : 'excel', + sourceRef: didarId || null, + nationalId: row.nationalId || undefined, + economicCode: row.economicCode?.trim() || null, + registrationNumber: row.registrationNumber?.trim() || null, + phone: row.phone || undefined, + otherPhones: row.otherPhones?.trim() || null, + fax: row.fax?.trim() || null, + email: row.email || undefined, + otherEmails: row.otherEmails?.trim() || null, + website: row.website || undefined, + province: row.province?.trim() || null, + city: row.city?.trim() || null, + postalCode: row.postalCode?.trim() || null, + address: row.address || undefined, + groupName: row.groupName?.trim() || null, + notes: row.notes || undefined, + registeredAt: registeredAt?.toISOString() ?? null, + customFields, + }; + try { - await this.companies.create(actor, { - name, - nationalId: row.nationalId || undefined, - phone: row.phone || undefined, - email: row.email || undefined, - website: row.website || undefined, - address: row.address || undefined, - notes: row.notes || undefined, - }); - result.created += 1; + const existing = await this.findImportedCompany(actor, didarId, name); + if (existing) { + const merged: Record = {}; + for (const [k, v] of Object.entries(payload)) { + if (k === 'customFields') continue; + if (v != null && v !== '') merged[k] = v; + } + merged.customFields = { + ...((existing.customFields ?? {}) as Record), + ...customFields, + }; + if (ownerId) merged.ownerId = ownerId; + await this.companies.update(actor, existing.id, merged); + result.skipped += 1; + } else { + await this.companies.create(actor, { + ...payload, + ownerId: resolveOwnerId(actor, ownerId ?? null), + }); + result.created += 1; + } + if (ownerMissing) { + result.errors.push({ + row: rowNum, + message: `مسئول شرکت «${ownerName}» در تیم پیدا نشد؛ به کاربر ایمپورتر اختصاص داده شد`, + }); + } } catch (e) { if (e instanceof ConflictException) { result.skipped += 1; @@ -562,18 +726,80 @@ export class ContactsExcelService { return { ...result, ...chunkImportMeta(ctx) }; } - private async findOrCreateCompanyId(actor: Actor, name: string): Promise { + /** شرکت موجود برای ورود مجدد: اول با کد دیدار، بعد با نام (اگر کد مبدأ نداشته باشد). */ + private async findImportedCompany(actor: Actor, didarId: string, name: string) { + if (didarId) { + const byRef = await this.prisma.company.findFirst({ + where: { + organizationId: actor.organizationId, + sourceRef: didarId, + ...ownerWhere(actor), + }, + select: { id: true, customFields: true }, + }); + if (byRef) return byRef; + } + return this.prisma.company.findFirst({ + where: { + organizationId: actor.organizationId, + name: { equals: name, mode: 'insensitive' }, + ...(didarId ? { sourceRef: null } : {}), + ...ownerWhere(actor), + }, + select: { id: true, customFields: true }, + }); + } + + private async findOrCreateCompanyId( + actor: Actor, + name: string, + didarId?: string, + phone?: string, + ): Promise { + const ref = didarId?.trim() || ''; + if (ref) { + const byRef = await this.prisma.company.findFirst({ + where: { + organizationId: actor.organizationId, + sourceRef: ref, + ...ownerWhere(actor), + }, + select: { id: true }, + }); + if (byRef) return byRef.id; + } const existing = await this.prisma.company.findFirst({ where: { organizationId: actor.organizationId, name: { equals: name, mode: 'insensitive' }, ...ownerWhere(actor), }, - select: { id: true }, + select: { id: true, sourceRef: true }, }); - if (existing) return existing.id; - const created = await this.companies.create(actor, { name }); - return created.id; + if (existing) { + if (ref && !existing.sourceRef) { + await this.prisma.company.update({ + where: { id: existing.id }, + data: { sourceRef: ref, importSource: 'didar' }, + }); + } + return existing.id; + } + try { + const created = await this.companies.create(actor, { + name, + ...(ref ? { sourceRef: ref, importSource: 'didar' } : {}), + ...(phone?.trim() ? { phone: phone.trim() } : {}), + }); + return created.id; + } catch (e) { + if (!(e instanceof ConflictException) || !phone) throw e; + const created = await this.companies.create(actor, { + name, + ...(ref ? { sourceRef: ref, importSource: 'didar' } : {}), + }); + return created.id; + } } private async findUserId(organizationId: string, name?: string) { diff --git a/apps/api/src/contacts/dto.ts b/apps/api/src/contacts/dto.ts index 6bdeb58..205f9a8 100644 --- a/apps/api/src/contacts/dto.ts +++ b/apps/api/src/contacts/dto.ts @@ -58,7 +58,74 @@ export class ListQueryDto { importSource?: string; } -export class CreateCompanyDto { +class CompanyExtraFieldsDto { + @IsOptional() + @IsString() + @MaxLength(60) + code?: string | null; + + @IsOptional() + @IsString() + @MaxLength(40) + importSource?: string; + + @IsOptional() + @IsString() + @MaxLength(80) + sourceRef?: string | null; + + @IsOptional() + @IsString() + @MaxLength(40) + economicCode?: string | null; + + @IsOptional() + @IsString() + @MaxLength(40) + registrationNumber?: string | null; + + @IsOptional() + @IsString() + @MaxLength(400) + otherPhones?: string | null; + + @IsOptional() + @IsString() + @MaxLength(40) + fax?: string | null; + + @IsOptional() + @IsString() + @MaxLength(400) + otherEmails?: string | null; + + @IsOptional() + @IsString() + @MaxLength(80) + province?: string | null; + + @IsOptional() + @IsString() + @MaxLength(80) + city?: string | null; + + @IsOptional() + @IsString() + @MaxLength(20) + postalCode?: string | null; + + @IsOptional() + @IsString() + @MaxLength(120) + groupName?: string | null; + + @IsOptional() + @ValidateIf((_, v) => v !== '' && v != null) + @IsString() + registeredAt?: string | null; +} + +export class CreateCompanyDto extends CompanyExtraFieldsDto { @IsString() @MinLength(2) @MaxLength(160) @@ -109,7 +176,7 @@ export class CreateCompanyDto { ownerId?: string | null; } -export class UpdateCompanyDto { +export class UpdateCompanyDto extends CompanyExtraFieldsDto { @IsOptional() @IsString() @MinLength(2) diff --git a/apps/web/src/views/CompanyFormView.vue b/apps/web/src/views/CompanyFormView.vue index e9640c5..894d6e7 100644 --- a/apps/web/src/views/CompanyFormView.vue +++ b/apps/web/src/views/CompanyFormView.vue @@ -6,6 +6,8 @@ import { api } from '@/lib/api'; import type { CompanyDto, CreateCompanyRequest } from '@mento/shared'; import CustomFieldsSection from '@/components/CustomFieldsSection.vue'; import EntityTimeline from '@/components/EntityTimeline.vue'; +import JalaliDateInput from '@/components/JalaliDateInput.vue'; +import { formatLocalDateInput, parseLocalInput } from '@/lib/jalali'; import { UiButton, UiCard, @@ -39,11 +41,24 @@ const saving = ref(false); const error = ref(''); const name = ref(''); +const code = ref(''); +const sourceRef = ref(''); +const importSource = ref('manual'); const nationalId = ref(''); +const economicCode = ref(''); +const registrationNumber = ref(''); const phone = ref(''); +const otherPhones = ref(''); +const fax = ref(''); const email = ref(''); +const otherEmails = ref(''); const website = ref(''); +const province = ref(''); +const city = ref(''); +const postalCode = ref(''); const address = ref(''); +const groupName = ref(''); +const registeredAt = ref(''); const notes = ref(''); const tagsRaw = ref(''); const customFields = ref>({}); @@ -61,11 +76,24 @@ function parseTags(raw: string): string[] { function fill(c: CompanyDto) { name.value = c.name; + code.value = c.code ?? ''; + sourceRef.value = c.sourceRef ?? ''; + importSource.value = c.importSource || 'manual'; nationalId.value = c.nationalId ?? ''; + economicCode.value = c.economicCode ?? ''; + registrationNumber.value = c.registrationNumber ?? ''; phone.value = c.phone ?? ''; + otherPhones.value = c.otherPhones ?? ''; + fax.value = c.fax ?? ''; email.value = c.email ?? ''; + otherEmails.value = c.otherEmails ?? ''; website.value = c.website ?? ''; + province.value = c.province ?? ''; + city.value = c.city ?? ''; + postalCode.value = c.postalCode ?? ''; address.value = c.address ?? ''; + groupName.value = c.groupName ?? ''; + registeredAt.value = c.registeredAt ? formatLocalDateInput(new Date(c.registeredAt)) : ''; notes.value = c.notes ?? ''; tagsRaw.value = (c.tags ?? []).join('، '); customFields.value = { ...(c.customFields ?? {}) }; @@ -88,11 +116,24 @@ async function load() { function payload(): CreateCompanyRequest { return { name: name.value, + code: code.value || null, + sourceRef: sourceRef.value || null, + ...(sourceRef.value && importSource.value === 'manual' ? { importSource: 'didar' } : {}), nationalId: nationalId.value || undefined, + economicCode: economicCode.value || null, + registrationNumber: registrationNumber.value || null, phone: phone.value || undefined, + otherPhones: otherPhones.value || null, + fax: fax.value || null, email: email.value || undefined, + otherEmails: otherEmails.value || null, website: website.value || undefined, + province: province.value || null, + city: city.value || null, + postalCode: postalCode.value || null, address: address.value || undefined, + groupName: groupName.value || null, + registeredAt: parseLocalInput(registeredAt.value)?.toISOString() ?? null, notes: notes.value || undefined, tags: parseTags(tagsRaw.value), customFields: customFields.value, @@ -204,50 +245,20 @@ onMounted(load); /> - - + + - - + + - - + + - - - - - - + + @@ -258,7 +269,7 @@ onMounted(load); /> - + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + + +
+
+ diff --git a/apps/web/src/views/ContactsView.vue b/apps/web/src/views/ContactsView.vue index 195e13d..9cb3ce7 100644 --- a/apps/web/src/views/ContactsView.vue +++ b/apps/web/src/views/ContactsView.vue @@ -733,7 +733,17 @@ onMounted(load); {{ c.email }}

-

افراد: {{ toFaDigits(c.peopleCount) }}

+

+ + {{ c.sourceRef ? `کد دیدار: ${toFaDigits(c.sourceRef)}` : '' }} + {{ c.sourceRef && c.code ? ' • ' : '' }} + {{ c.code ? `کد: ${toFaDigits(c.code)}` : '' }} + +

+

+ {{ [c.city, c.owner?.fullName].filter(Boolean).join(' • ') }} + {{ c.city || c.owner ? ' • ' : '' }}افراد: {{ toFaDigits(c.peopleCount) }} +

@@ -743,8 +753,18 @@ onMounted(load); # نام شرکت - تلفن - ایمیل + کد دیدار شرکت + کد شرکت + منبع + گروه بندی + استان + شهرستان + تلفن شرکت + ایمیل شرکت + شناسه ملی + شماره اقتصادی + مسئول شرکت + تاریخ ثبت افراد @@ -756,8 +776,18 @@ onMounted(load); {{ c.name }} - {{ c.phone || '—' }} - {{ c.email || '—' }} + {{ c.sourceRef ? toFaDigits(c.sourceRef) : '—' }} + {{ c.code ? toFaDigits(c.code) : '—' }} + {{ sourceLabel(c.importSource || 'manual') }} + {{ c.groupName || '—' }} + {{ c.province || '—' }} + {{ c.city || '—' }} + {{ c.phone || '—' }} + {{ c.email || '—' }} + {{ c.nationalId || '—' }} + {{ c.economicCode || '—' }} + {{ c.owner?.fullName || '—' }} + {{ formatDate(c.registeredAt || c.createdAt) }} {{ toFaDigits(c.peopleCount) }} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8cc73b1..16c510b 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -664,12 +664,27 @@ export interface CompanySummaryDto { export interface CompanyDto { id: string; name: string; + /** کد داخلی شرکت */ + code: string | null; + importSource: PersonImportSource | string; + /** کد شرکت در مبدأ (مثلاً کد دیدار) */ + sourceRef: string | null; nationalId: string | null; + economicCode: string | null; + registrationNumber: string | null; phone: string | null; + otherPhones: string | null; + fax: string | null; email: string | null; + otherEmails: string | null; website: string | null; + province: string | null; + city: string | null; + postalCode: string | null; address: string | null; + groupName: string | null; notes: string | null; + registeredAt: string | null; tags: string[]; customFields: Record; peopleCount: number; @@ -680,12 +695,25 @@ export interface CompanyDto { export interface CreateCompanyRequest { name: string; + code?: string | null; + importSource?: PersonImportSource | string; + sourceRef?: string | null; nationalId?: string; + economicCode?: string | null; + registrationNumber?: string | null; phone?: string; + otherPhones?: string | null; + fax?: string | null; email?: string; + otherEmails?: string | null; website?: string; + province?: string | null; + city?: string | null; + postalCode?: string | null; address?: string; + groupName?: string | null; notes?: string; + registeredAt?: string | null; tags?: string[]; customFields?: Record; ownerId?: string | null;