From 8b8d01976d8c85ec90c1eda0ac43b87ecab69301 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 9 Nov 2026 15:09:49 +0330 Subject: [PATCH] fix(pipeline): hide prior-only sales paths from current work The board lists a pipeline in current work only when it has a deal in that period, and keeps archive-only paths for the prior-data view. Co-authored-by: Cursor --- .../api/src/pipelines/pipelines.controller.ts | 7 ++- apps/api/src/pipelines/pipelines.service.ts | 47 +++++++++++++++---- apps/web/src/views/PipelineBoardView.vue | 23 ++++++--- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/apps/api/src/pipelines/pipelines.controller.ts b/apps/api/src/pipelines/pipelines.controller.ts index e7d26f8..518456d 100644 --- a/apps/api/src/pipelines/pipelines.controller.ts +++ b/apps/api/src/pipelines/pipelines.controller.ts @@ -31,8 +31,11 @@ export class PipelinesController { constructor(private readonly pipelinesService: PipelinesService) {} @Get() - list(@CurrentUser() user: { organizationId: string }) { - return this.pipelinesService.list(user.organizationId); + list( + @CurrentUser() user: Actor, + @Query('forLens') forLens?: string, + ) { + return this.pipelinesService.list(user, forLens === '1'); } @Get('board') diff --git a/apps/api/src/pipelines/pipelines.service.ts b/apps/api/src/pipelines/pipelines.service.ts index f5db4d1..e7eac1c 100644 --- a/apps/api/src/pipelines/pipelines.service.ts +++ b/apps/api/src/pipelines/pipelines.service.ts @@ -102,24 +102,55 @@ export class PipelinesService { return this.toPipelineDto(created); } - async list(organizationId: string): Promise { + async list(actorOrOrgId: Actor | string, forLens = false): Promise { + const organizationId = + typeof actorOrOrgId === 'string' ? actorOrOrgId : actorOrOrgId.organizationId; + const actor = typeof actorOrOrgId === 'string' ? undefined : actorOrOrgId; await this.ensureDefaultPipeline(organizationId); const rows = await this.prisma.pipeline.findMany({ where: { organizationId }, include: { stages: { orderBy: { position: 'asc' } } }, orderBy: [{ isDefault: 'desc' }, { name: 'asc' }], }); - const counts = await this.prisma.deal.groupBy({ - by: ['pipelineId'], - where: { organizationId }, - _count: { _all: true }, - }); + const lens = actor?.priorLens; + const lensActive = forLens && (lens === 'current' || lens === 'archive'); + const access = actor ? dealAccessWhere(actor) : {}; + const prior = lensActive + ? dealPriorWhere(actor?.priorLens, actor?.priorCutoff) + : {}; + const [counts, lensCounts] = await Promise.all([ + this.prisma.deal.groupBy({ + by: ['pipelineId'], + where: lensActive ? { organizationId, AND: [access] } : { organizationId }, + _count: { _all: true }, + }), + lensActive + ? this.prisma.deal.groupBy({ + by: ['pipelineId'], + where: { organizationId, AND: [access, prior] }, + _count: { _all: true }, + }) + : Promise.resolve([]), + ]); const dealCountByPipeline = new Map( counts.map((row) => [row.pipelineId, row._count._all]), ); - return rows.map((row) => ({ + const lensCountByPipeline = new Map( + lensCounts.map((row) => [row.pipelineId, row._count._all]), + ); + const visible = lensActive + ? rows.filter((row) => { + const inLens = lensCountByPipeline.get(row.id) ?? 0; + const total = dealCountByPipeline.get(row.id) ?? 0; + if (lens === 'archive') return inLens > 0; + return inLens > 0 || total === 0; + }) + : rows; + return visible.map((row) => ({ ...this.toPipelineDto(row), - dealCount: dealCountByPipeline.get(row.id) ?? 0, + dealCount: lensActive + ? (lensCountByPipeline.get(row.id) ?? 0) + : (dealCountByPipeline.get(row.id) ?? 0), })); } diff --git a/apps/web/src/views/PipelineBoardView.vue b/apps/web/src/views/PipelineBoardView.vue index 94f1dd6..6a50291 100644 --- a/apps/web/src/views/PipelineBoardView.vue +++ b/apps/web/src/views/PipelineBoardView.vue @@ -177,7 +177,9 @@ function findAccent(dealId: string): PipelineStage['color'] { } async function loadPipelines() { - const { data } = await api.get('/pipelines'); + const { data } = await api.get('/pipelines', { + params: { forLens: '1' }, + }); pipelines.value = data; } @@ -186,16 +188,23 @@ async function load() { error.value = ''; try { await loadPipelines(); - const pipelineId = - typeof route.query.pipelineId === 'string' - ? route.query.pipelineId - : undefined; + const requested = + typeof route.query.pipelineId === 'string' ? route.query.pipelineId : ''; + const pipelineId = pipelines.value.some((item) => item.id === requested) + ? requested + : pipelines.value[0]?.id; + if (!pipelineId) { + boardPipelineId.value = ''; + stages.value = []; + if (requested) await router.replace({ name: 'pipeline', query: {} }); + return; + } const { data } = await api.get('/pipelines/board', { - params: pipelineId ? { pipelineId } : undefined, + params: { pipelineId }, }); boardPipelineId.value = data.pipeline.id; stages.value = mapBoardToStages(data, formatDealDate); - if (!pipelineId || pipelineId !== data.pipeline.id) { + if (requested !== data.pipeline.id) { await router.replace({ name: 'pipeline', query: { pipelineId: data.pipeline.id },