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