Deals import: imported pipelines keep only Didar stages; remove unused default stages
This commit is contained in:
@@ -35,7 +35,7 @@ import {
|
||||
resolveExcelImportChunk,
|
||||
type ExcelImportLookupCaches,
|
||||
} from '../common/excel-import-session';
|
||||
import { PipelinesService } from './pipelines.service';
|
||||
import { DEFAULT_STAGES, PipelinesService } from './pipelines.service';
|
||||
|
||||
const MAX_ROWS_PER_REQUEST = 80;
|
||||
|
||||
@@ -97,6 +97,7 @@ type PipelineCacheEntry = {
|
||||
type RowContext = {
|
||||
pipelines: Map<string, PipelineCacheEntry>;
|
||||
customFieldKeys: Map<string, string>;
|
||||
touchedPipelines: Set<string>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -290,6 +291,7 @@ export class DealsExcelService {
|
||||
const ctx: RowContext = {
|
||||
pipelines: new Map(),
|
||||
customFieldKeys: await this.loadCustomFieldKeys(actor.organizationId),
|
||||
touchedPipelines: new Set(),
|
||||
};
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
@@ -299,7 +301,10 @@ export class DealsExcelService {
|
||||
if (type && type !== 'deal') continue;
|
||||
try {
|
||||
const outcome = await this.processRow(actor, row, rowNum, result, caches, ctx);
|
||||
if (outcome) affected.set(outcome.pipelineId, outcome.pipelineName);
|
||||
if (outcome) {
|
||||
affected.set(outcome.pipelineId, outcome.pipelineName);
|
||||
ctx.touchedPipelines.add(outcome.pipelineId);
|
||||
}
|
||||
} catch (e) {
|
||||
result.errors.push({
|
||||
row: rowNum,
|
||||
@@ -307,6 +312,7 @@ export class DealsExcelService {
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.removeUnusedDefaultStages(actor.organizationId, ctx.touchedPipelines);
|
||||
return affected;
|
||||
}
|
||||
|
||||
@@ -342,7 +348,8 @@ export class DealsExcelService {
|
||||
if (didarId) {
|
||||
const existingId = await this.findDealByRef(orgId, didarId, caches);
|
||||
if (existingId) {
|
||||
await this.mergeIntoExisting(existingId, customFields, lineItem);
|
||||
const pipelineId = await this.mergeIntoExisting(existingId, customFields, lineItem);
|
||||
if (pipelineId) ctx.touchedPipelines.add(pipelineId);
|
||||
result.skipped += 1;
|
||||
return null;
|
||||
}
|
||||
@@ -448,12 +455,13 @@ export class DealsExcelService {
|
||||
const deal = await this.prisma.deal.findUnique({
|
||||
where: { id: dealId },
|
||||
select: {
|
||||
pipelineId: true,
|
||||
customFields: true,
|
||||
currency: true,
|
||||
lineItems: { select: { name: true, position: true } },
|
||||
},
|
||||
});
|
||||
if (!deal) return;
|
||||
if (!deal) return null;
|
||||
const merged = { ...asCustomFields(deal.customFields) };
|
||||
let changed = false;
|
||||
for (const [k, v] of Object.entries(customFields)) {
|
||||
@@ -490,6 +498,33 @@ export class DealsExcelService {
|
||||
data: { amount: new Prisma.Decimal(total) },
|
||||
});
|
||||
}
|
||||
return deal.pipelineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* مراحل پیشفرض سیستم (سرنخ، تماس اول، …) که خالی ماندهاند از کاریزهای واردشده حذف میشوند
|
||||
* تا فقط مراحل خود فایل دیده شوند.
|
||||
*/
|
||||
private async removeUnusedDefaultStages(organizationId: string, pipelineIds: Set<string>) {
|
||||
const defaultNames = new Set<string>(DEFAULT_STAGES.map((s) => s.name));
|
||||
for (const pipelineId of pipelineIds) {
|
||||
const stages = await this.prisma.pipelineStage.findMany({
|
||||
where: { pipelineId, pipeline: { organizationId } },
|
||||
orderBy: { position: 'asc' },
|
||||
select: { id: true, name: true, _count: { select: { deals: true } } },
|
||||
});
|
||||
if (!stages.some((s) => !defaultNames.has(s.name))) continue;
|
||||
const removable = stages.filter((s) => defaultNames.has(s.name) && s._count.deals === 0);
|
||||
if (!removable.length) continue;
|
||||
const removeIds = new Set(removable.map((s) => s.id));
|
||||
const remaining = stages.filter((s) => !removeIds.has(s.id));
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.pipelineStage.deleteMany({ where: { id: { in: [...removeIds] } } }),
|
||||
...remaining.map((s, index) =>
|
||||
this.prisma.pipelineStage.update({ where: { id: s.id }, data: { position: index } }),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async findDealByRef(
|
||||
@@ -608,7 +643,9 @@ export class DealsExcelService {
|
||||
include: { stages: { orderBy: { position: 'asc' } } },
|
||||
});
|
||||
if (!row) {
|
||||
const created = await this.pipelines.create(organizationId, name);
|
||||
const created = await this.pipelines.create(organizationId, name, {
|
||||
withDefaultStages: false,
|
||||
});
|
||||
row = await this.prisma.pipeline.findFirstOrThrow({
|
||||
where: { id: created.id },
|
||||
include: { stages: { orderBy: { position: 'asc' } } },
|
||||
@@ -622,7 +659,15 @@ export class DealsExcelService {
|
||||
ctx.pipelines.set(cacheKey, pipeline);
|
||||
}
|
||||
|
||||
const wanted = stageName?.trim();
|
||||
const fallbackName =
|
||||
status === 'won' ? 'موفق' : status === 'lost' ? 'ناموفق' : 'جاری';
|
||||
const byStatusExisting =
|
||||
status === 'won'
|
||||
? pipeline.stages.find((s) => s.isWon)
|
||||
: status === 'lost'
|
||||
? pipeline.stages.find((s) => s.isLost)
|
||||
: pipeline.stages.find((s) => !s.isWon && !s.isLost);
|
||||
const wanted = stageName?.trim() || (byStatusExisting ? '' : fallbackName);
|
||||
if (wanted) {
|
||||
const existing = pipeline.stages.find(
|
||||
(s) => s.name.trim().toLowerCase() === wanted.toLowerCase(),
|
||||
@@ -642,13 +687,7 @@ export class DealsExcelService {
|
||||
return { pipelineId: pipeline.id, stageId: stage.id, pipelineName: pipeline.name };
|
||||
}
|
||||
|
||||
const byStatus =
|
||||
status === 'won'
|
||||
? pipeline.stages.find((s) => s.isWon)
|
||||
: status === 'lost'
|
||||
? pipeline.stages.find((s) => s.isLost)
|
||||
: pipeline.stages.find((s) => !s.isWon && !s.isLost);
|
||||
const stage = byStatus ?? pipeline.stages[0];
|
||||
const stage = byStatusExisting ?? pipeline.stages[0];
|
||||
if (!stage) throw new Error('کاریز مرحلهای ندارد');
|
||||
return { pipelineId: pipeline.id, stageId: stage.id, pipelineName: pipeline.name };
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { asCustomFields } from '../common/custom-fields';
|
||||
import { type Actor, dealAccessWhere } from '../common/record-scope';
|
||||
import { decimalToBase, loadOrgRates } from '../common/currency';
|
||||
|
||||
const DEFAULT_STAGES = [
|
||||
export const DEFAULT_STAGES = [
|
||||
{ name: 'سرنخ', isWon: false, isLost: false },
|
||||
{ name: 'تماس اول', isWon: false, isLost: false },
|
||||
{ name: 'پیشنهاد', isWon: false, isLost: false },
|
||||
@@ -107,21 +107,30 @@ export class PipelinesService {
|
||||
return rows.map((row) => this.toPipelineDto(row));
|
||||
}
|
||||
|
||||
async create(organizationId: string, name: string): Promise<PipelineDto> {
|
||||
async create(
|
||||
organizationId: string,
|
||||
name: string,
|
||||
opts: { withDefaultStages?: boolean } = {},
|
||||
): Promise<PipelineDto> {
|
||||
await this.ensureDefaultPipeline(organizationId);
|
||||
const withDefaults = opts.withDefaultStages ?? true;
|
||||
const created = await this.prisma.pipeline.create({
|
||||
data: {
|
||||
organizationId,
|
||||
name: name.trim(),
|
||||
isDefault: false,
|
||||
stages: {
|
||||
create: DEFAULT_STAGES.map((s, index) => ({
|
||||
name: s.name,
|
||||
position: index,
|
||||
isWon: s.isWon,
|
||||
isLost: s.isLost,
|
||||
})),
|
||||
},
|
||||
...(withDefaults
|
||||
? {
|
||||
stages: {
|
||||
create: DEFAULT_STAGES.map((s, index) => ({
|
||||
name: s.name,
|
||||
position: index,
|
||||
isWon: s.isWon,
|
||||
isLost: s.isLost,
|
||||
})),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
include: { stages: { orderBy: { position: 'asc' } } },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user