Improve deal board activity cues with relative dates and a quick activity list popup.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,10 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type {
|
||||
ActivityStatus,
|
||||
ActivityType,
|
||||
DealActivityCue,
|
||||
DealActivitySummaryDto,
|
||||
DealDto,
|
||||
MergePipelineResultDto,
|
||||
PipelineBoardDto,
|
||||
@@ -374,12 +378,23 @@ export class PipelinesService {
|
||||
});
|
||||
|
||||
const rateMap = await loadOrgRates(this.prisma, organizationId);
|
||||
const activityHints = await this.loadDealActivityHints(
|
||||
organizationId,
|
||||
deals.map((deal) => deal.id),
|
||||
);
|
||||
|
||||
const byStage = new Map<string, DealDto[]>();
|
||||
for (const stage of resolved.stages) byStage.set(stage.id, []);
|
||||
for (const deal of deals) {
|
||||
const list = byStage.get(deal.stageId);
|
||||
if (list) list.push(this.toDealDto(deal));
|
||||
if (list) {
|
||||
const hint = activityHints.get(deal.id);
|
||||
list.push({
|
||||
...this.toDealDto(deal),
|
||||
activityCue: hint?.cue ?? 'none',
|
||||
activitySummaries: hint?.summaries ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -597,6 +612,105 @@ export class PipelinesService {
|
||||
};
|
||||
}
|
||||
|
||||
private async loadDealActivityHints(
|
||||
organizationId: string,
|
||||
dealIds: string[],
|
||||
): Promise<
|
||||
Map<string, { cue: DealActivityCue; summaries: DealActivitySummaryDto[] }>
|
||||
> {
|
||||
const hints = new Map<
|
||||
string,
|
||||
{ cue: DealActivityCue; summaries: DealActivitySummaryDto[] }
|
||||
>();
|
||||
if (!dealIds.length) return hints;
|
||||
|
||||
const rows = await this.prisma.activity.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
dealId: { in: dealIds },
|
||||
status: { in: ['open', 'done'] },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
dealId: true,
|
||||
title: true,
|
||||
type: true,
|
||||
status: true,
|
||||
dueAt: true,
|
||||
completedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: 'open' | 'done';
|
||||
dueAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
};
|
||||
const byDeal = new Map<string, Row[]>();
|
||||
for (const row of rows) {
|
||||
if (!row.dealId) continue;
|
||||
const list = byDeal.get(row.dealId) ?? [];
|
||||
list.push({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
type: row.type,
|
||||
status: row.status === 'done' ? 'done' : 'open',
|
||||
dueAt: row.dueAt,
|
||||
completedAt: row.completedAt,
|
||||
});
|
||||
byDeal.set(row.dealId, list);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cueOf = (item: Row): Exclude<DealActivityCue, 'none'> => {
|
||||
if (item.status === 'done') return 'done';
|
||||
if (item.dueAt && item.dueAt.getTime() < now) return 'overdue';
|
||||
return 'upcoming';
|
||||
};
|
||||
const rank = (cue: Exclude<DealActivityCue, 'none'>) =>
|
||||
cue === 'overdue' ? 0 : cue === 'upcoming' ? 1 : 2;
|
||||
|
||||
for (const dealId of dealIds) {
|
||||
const list = byDeal.get(dealId) ?? [];
|
||||
const summaries = list
|
||||
.map((item) => {
|
||||
const cue = cueOf(item);
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: item.type as ActivityType,
|
||||
status: item.status as ActivityStatus,
|
||||
dueAt: item.dueAt?.toISOString() ?? null,
|
||||
cue,
|
||||
_due: item.dueAt?.getTime() ?? Number.POSITIVE_INFINITY,
|
||||
_done: item.completedAt?.getTime() ?? 0,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const byRank = rank(a.cue) - rank(b.cue);
|
||||
if (byRank) return byRank;
|
||||
if (a.cue === 'done' && b.cue === 'done') return b._done - a._done;
|
||||
return a._due - b._due;
|
||||
})
|
||||
.map(({ _due: _d, _done: _c, ...summary }) => summary);
|
||||
|
||||
const open = list.filter((item) => item.status === 'open');
|
||||
let cue: DealActivityCue = 'none';
|
||||
if (open.some((item) => item.dueAt && item.dueAt.getTime() < now)) {
|
||||
cue = 'overdue';
|
||||
} else if (open.length) {
|
||||
cue = 'upcoming';
|
||||
} else if (list.some((item) => item.status === 'done')) {
|
||||
cue = 'done';
|
||||
}
|
||||
hints.set(dealId, { cue, summaries });
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
toDealDto(row: DealRow): DealDto {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
Reference in New Issue
Block a user