Deals Excel import/export aligned with Didar: browser-side import, new deal fields (code, probability, closedAt, creator, lostNote, referrer), Didar custom fields
This commit is contained in:
@@ -39,17 +39,24 @@ export type ClientExcelPositionalColumn = { key: string; label: string };
|
||||
export type ClientExcelReadOptions = {
|
||||
positionalColumns?: ClientExcelPositionalColumn[];
|
||||
detectHeaderless?: (firstRowCells: string[]) => boolean;
|
||||
/**
|
||||
* مقدار خام سلول بهجای متن قالببندیشده؛ خروجی دیدار به کدهای عددی قالب تاریخ میدهد
|
||||
* و بدون این گزینه «4231» بهصورت «8/1/11» خوانده میشود.
|
||||
*/
|
||||
rawValues?: boolean;
|
||||
/** ستونهای ناشناخته با کلید `${prefix}${عنوان ستون}` ارسال میشوند (فیلد سفارشی) */
|
||||
customFieldPrefix?: string;
|
||||
};
|
||||
|
||||
function readMatrix(fileBuffer: ArrayBuffer) {
|
||||
const wb = XLSX.read(fileBuffer, { type: 'array', cellDates: true });
|
||||
function readMatrix(fileBuffer: ArrayBuffer, rawValues = false) {
|
||||
const wb = XLSX.read(fileBuffer, { type: 'array', cellDates: !rawValues });
|
||||
const sheetName = wb.SheetNames[0];
|
||||
if (!sheetName) throw new Error('شیت اکسل پیدا نشد');
|
||||
const sheet = wb.Sheets[sheetName]!;
|
||||
return XLSX.utils.sheet_to_json<unknown[]>(sheet, {
|
||||
header: 1,
|
||||
defval: '',
|
||||
raw: false,
|
||||
raw: rawValues,
|
||||
}) as unknown[][];
|
||||
}
|
||||
|
||||
@@ -60,11 +67,13 @@ export async function previewExcelClient(
|
||||
fields: ExcelFieldOption[],
|
||||
options?: ClientExcelReadOptions,
|
||||
): Promise<ExcelPreviewResult> {
|
||||
const matrix = readMatrix(await file.arrayBuffer());
|
||||
const matrix = readMatrix(await file.arrayBuffer(), options?.rawValues);
|
||||
const headerRow = (matrix[0] ?? []).map((c) => cellToString(c));
|
||||
const headerless =
|
||||
Boolean(options?.positionalColumns?.length) &&
|
||||
Boolean(options?.detectHeaderless?.(headerRow));
|
||||
const prefix = options?.customFieldPrefix;
|
||||
const allFields = [...fields];
|
||||
|
||||
const columns: ExcelPreviewResult['columns'] = [];
|
||||
const suggested: Record<string, string> = {};
|
||||
@@ -96,6 +105,11 @@ export async function previewExcelClient(
|
||||
if (key && !used.has(key)) {
|
||||
suggested[header] = key;
|
||||
used.add(key);
|
||||
} else if (!key && prefix) {
|
||||
const cfKey = `${prefix}${header}`;
|
||||
allFields.push({ key: cfKey, label: `فیلد سفارشی: ${header}` });
|
||||
suggested[header] = cfKey;
|
||||
used.add(cfKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -116,7 +130,7 @@ export async function previewExcelClient(
|
||||
|
||||
return {
|
||||
columns,
|
||||
fields,
|
||||
fields: allFields,
|
||||
suggested,
|
||||
needsMapping: headerless
|
||||
? false
|
||||
@@ -132,7 +146,7 @@ export async function readMappedRowsClient(
|
||||
mapping?: Record<string, string>,
|
||||
options?: ClientExcelReadOptions,
|
||||
): Promise<Record<string, string>[]> {
|
||||
const matrix = readMatrix(await file.arrayBuffer());
|
||||
const matrix = readMatrix(await file.arrayBuffer(), options?.rawValues);
|
||||
const headerRow = (matrix[0] ?? []).map((c) => cellToString(c));
|
||||
const headerless =
|
||||
!mapping &&
|
||||
@@ -152,6 +166,9 @@ export async function readMappedRowsClient(
|
||||
if (!header) return;
|
||||
const key = resolveFieldKey(header, aliases, mapping);
|
||||
if (key) colMap.set(idx, key);
|
||||
else if (!mapping && options?.customFieldPrefix) {
|
||||
colMap.set(idx, `${options.customFieldPrefix}${header}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,14 @@ const pipelineId = ref('');
|
||||
const ownerId = ref('');
|
||||
const selectedAssigneeIds = ref<string[]>([]);
|
||||
const lostReason = ref('');
|
||||
const lostNote = ref('');
|
||||
const dealCode = ref('');
|
||||
const probability = ref('');
|
||||
const sourceDetail = ref('');
|
||||
const referrer = ref('');
|
||||
const closedAt = ref('');
|
||||
const createdByName = ref('');
|
||||
const sourceRef = ref('');
|
||||
const status = ref('');
|
||||
const customFields = ref<Record<string, unknown>>({});
|
||||
const lines = ref<LineDraft[]>([]);
|
||||
@@ -251,6 +259,14 @@ async function load() {
|
||||
ownerId.value = data.owner?.id ?? auth.user?.id ?? '';
|
||||
selectedAssigneeIds.value = (data.assignees ?? []).map((a) => a.id);
|
||||
lostReason.value = data.lostReason ?? '';
|
||||
lostNote.value = data.lostNote ?? '';
|
||||
dealCode.value = data.code ?? '';
|
||||
probability.value = data.probability == null ? '' : String(data.probability);
|
||||
sourceDetail.value = data.sourceDetail ?? '';
|
||||
referrer.value = data.referrer ?? '';
|
||||
closedAt.value = data.closedAt ? data.closedAt.slice(0, 10) : '';
|
||||
createdByName.value = data.createdBy?.fullName ?? '';
|
||||
sourceRef.value = data.importSource === 'didar' && data.sourceRef ? data.sourceRef : '';
|
||||
status.value = data.status;
|
||||
customFields.value = { ...(data.customFields ?? {}) };
|
||||
lines.value = (data.lineItems ?? []).map((item) => ({
|
||||
@@ -302,6 +318,10 @@ function payload(): CreateDealRequest {
|
||||
? null
|
||||
: Number(amount.value),
|
||||
source: source.value || undefined,
|
||||
sourceDetail: sourceDetail.value.trim() || null,
|
||||
referrer: referrer.value.trim() || null,
|
||||
code: dealCode.value.trim() || null,
|
||||
probability: probability.value === '' ? null : Number(probability.value),
|
||||
notes: notes.value || undefined,
|
||||
tags: parseTags(tagsRaw.value),
|
||||
isStale: health.value === 'stale',
|
||||
@@ -374,6 +394,8 @@ async function submit() {
|
||||
await api.patch(`/deals/${id.value}`, {
|
||||
...payload(),
|
||||
lostReason: lostReason.value || null,
|
||||
lostNote: lostNote.value.trim() || null,
|
||||
closedAt: closedAt.value || null,
|
||||
});
|
||||
if (stageId.value) {
|
||||
await api.patch(`/deals/${id.value}/move`, { stageId: stageId.value });
|
||||
@@ -679,13 +701,43 @@ onMounted(load);
|
||||
<UiInput v-model="source" placeholder="مثال: وبسایت، معرفی، نمایشگاه…" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="شیوه آشنایی دلخواه">
|
||||
<UiInput v-model="sourceDetail" placeholder="توضیح آزاد" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="معرف">
|
||||
<UiInput v-model="referrer" placeholder="نام معرف" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="کد دستی معامله">
|
||||
<UiInput v-model="dealCode" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="احتمال موفقیت (درصد)">
|
||||
<UiInput v-model="probability" type="number" min="0" max="100" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="برچسبها">
|
||||
<UiInput v-model="tagsRaw" placeholder="سازمانی، فوری (با کاما جدا کنید)" />
|
||||
</UiField>
|
||||
|
||||
<UiField v-if="!isNew" label="تاریخ انجام معامله">
|
||||
<JalaliDateInput v-model="closedAt" placeholder="انتخاب تاریخ" />
|
||||
</UiField>
|
||||
|
||||
<UiField v-if="!isNew" label="دلیل شکست (در صورت باخت معامله)" class="crm-col-span-full">
|
||||
<UiInput id="lostReason" v-model="lostReason" placeholder="علت از دست رفتن معامله را بنویسید…" />
|
||||
</UiField>
|
||||
|
||||
<UiField v-if="!isNew" label="یادداشت شکست" class="crm-col-span-full">
|
||||
<UiInput v-model="lostNote" />
|
||||
</UiField>
|
||||
|
||||
<p v-if="!isNew && (createdByName || sourceRef)" class="hint crm-col-span-full">
|
||||
<span v-if="createdByName">ثبتکننده: {{ createdByName }}</span>
|
||||
<span v-if="createdByName && sourceRef"> · </span>
|
||||
<span v-if="sourceRef">کد دیدار: {{ toFaDigits(sourceRef) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- بخش محصولات / خدمات -->
|
||||
|
||||
@@ -24,7 +24,22 @@ import UiModal from '@/components/ui/UiModal.vue';
|
||||
import DealFormView from '@/views/DealFormView.vue';
|
||||
import SavedFiltersBar from '@/components/SavedFiltersBar.vue';
|
||||
import type { ImportContactsResult, PipelineBoardDto, PipelineDto } from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import {
|
||||
DEAL_EXCEL_ALIASES,
|
||||
DEAL_EXCEL_COLUMNS,
|
||||
EXCEL_CUSTOM_FIELD_PREFIX,
|
||||
brand,
|
||||
} from '@mento/shared';
|
||||
|
||||
const dealExcelFields = DEAL_EXCEL_COLUMNS.filter((c) => c.key !== '_type').map((c) => ({
|
||||
key: c.key,
|
||||
label: c.header,
|
||||
required: c.key === 'title',
|
||||
}));
|
||||
const dealClientReadOptions = {
|
||||
rawValues: true,
|
||||
customFieldPrefix: EXCEL_CUSTOM_FIELD_PREFIX,
|
||||
};
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canManageSettings } from '@/lib/roles';
|
||||
|
||||
@@ -506,6 +521,10 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
<ExcelImportFlow
|
||||
preview-path="/deals/import/preview"
|
||||
import-path="/deals/import"
|
||||
rows-import-path="/deals/import/rows"
|
||||
:client-aliases="DEAL_EXCEL_ALIASES"
|
||||
:client-fields="dealExcelFields"
|
||||
:client-read-options="dealClientReadOptions"
|
||||
:disabled="loading || busy"
|
||||
@start="onImportStart"
|
||||
@done="onImportDone"
|
||||
|
||||
Reference in New Issue
Block a user