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;