feat: group activities into boards with their own stages, like sales pipelines
Existing stages and activities move into a default board per environment; new activities pick a board in the form.
This commit is contained in:
@@ -30,14 +30,14 @@ import { GamificationService } from '../gamification/gamification.service';
|
||||
import { tehranDayKey } from '../routines/routine-dates';
|
||||
import { buildSearchWhere, ci } from '../common/search';
|
||||
import { activityPriorWhere } from '../common/prior-archive';
|
||||
import { ActivityStagesService, type StageDefaults } from './activity-stages.service';
|
||||
import { ActivityStagesService, type BoardStageContext } from './activity-stages.service';
|
||||
import {
|
||||
ActivityTypesService,
|
||||
type SystemTypes,
|
||||
type TypeRow,
|
||||
} from './activity-types.service';
|
||||
|
||||
type DtoContext = { stages?: StageDefaults; types?: SystemTypes };
|
||||
type DtoContext = { stages?: BoardStageContext; types?: SystemTypes };
|
||||
|
||||
type ActivityActor = Pick<Actor, 'userId' | 'role' | 'priorLens' | 'priorCutoff'>;
|
||||
|
||||
@@ -49,6 +49,8 @@ type ListFacets = {
|
||||
typeDefId?: string;
|
||||
status?: ActivityDto['status'];
|
||||
assigneeId?: string;
|
||||
/** گروه فعالیت؛ خالی = همهٔ گروهها */
|
||||
boardId?: string;
|
||||
};
|
||||
|
||||
type ActivityRow = {
|
||||
@@ -77,14 +79,15 @@ type ActivityRow = {
|
||||
assignee: { id: string; fullName: string } | null;
|
||||
assignees: Array<{ id: string; fullName: string }>;
|
||||
createdBy: { id: string; fullName: string } | null;
|
||||
stage?: { id: string; kind: ActivityDto['status'] } | null;
|
||||
boardId?: string | null;
|
||||
stage?: { id: string; kind: ActivityDto['status']; boardId: string } | null;
|
||||
typeDef?: { id: string; name: string; icon: string; color: string } | null;
|
||||
fields?: Prisma.JsonValue | null;
|
||||
participantIds?: string[];
|
||||
};
|
||||
|
||||
const include = {
|
||||
stage: { select: { id: true, kind: true } },
|
||||
stage: { select: { id: true, kind: true, boardId: true } },
|
||||
typeDef: { select: { id: true, name: true, icon: true, color: true } },
|
||||
person: { select: { id: true, firstName: true, lastName: true } },
|
||||
company: { select: { id: true, name: true } },
|
||||
@@ -109,7 +112,7 @@ export class ActivitiesService {
|
||||
|
||||
private async ctx(organizationId: string): Promise<DtoContext> {
|
||||
const [stages, types] = await Promise.all([
|
||||
this.stages.defaults(organizationId),
|
||||
this.stages.boardContext(organizationId),
|
||||
this.types.system(organizationId),
|
||||
]);
|
||||
return { stages, types };
|
||||
@@ -255,13 +258,20 @@ export class ActivitiesService {
|
||||
stageScope,
|
||||
typeRows,
|
||||
);
|
||||
const boardCtx = await this.stages.boardContext(organizationId);
|
||||
if (facets?.boardId) {
|
||||
const boardId = await this.stages.resolveBoardId(organizationId, facets.boardId);
|
||||
(where.AND as Prisma.ActivityWhereInput[]).push(
|
||||
this.stages.boardWhere(boardId, boardCtx.defaultBoardId),
|
||||
);
|
||||
}
|
||||
if (stageScope && stageScope.stageId !== 'all') {
|
||||
(where.AND as Prisma.ActivityWhereInput[]).push(
|
||||
await this.stages.whereForStage(organizationId, stageScope.stageId),
|
||||
);
|
||||
}
|
||||
const ctx: DtoContext = {
|
||||
stages: await this.stages.defaults(organizationId),
|
||||
stages: boardCtx,
|
||||
types: this.types.systemOf(typeRows),
|
||||
};
|
||||
const take = Math.min(Math.max(limit, 1), 200);
|
||||
@@ -293,7 +303,9 @@ export class ActivitiesService {
|
||||
desk?: 'mine' | 'team',
|
||||
facets?: ListFacets,
|
||||
): Promise<ActivityStageCountsDto> {
|
||||
const rows = await this.stages.rows(organizationId);
|
||||
const boardId = await this.stages.resolveBoardId(organizationId, facets?.boardId);
|
||||
const defaultBoard = await this.stages.ensureDefaultBoard(organizationId);
|
||||
const rows = await this.stages.rows(organizationId, boardId);
|
||||
const defaults = this.stages.defaultsOf(rows);
|
||||
const kindOf = new Map(rows.map((r) => [r.id, r.kind]));
|
||||
const where = this.buildWhere(
|
||||
@@ -308,6 +320,9 @@ export class ActivitiesService {
|
||||
{ stageId: 'all' },
|
||||
await this.types.rows(organizationId),
|
||||
);
|
||||
(where.AND as Prisma.ActivityWhereInput[]).push(
|
||||
this.stages.boardWhere(boardId, defaultBoard.id),
|
||||
);
|
||||
const groups = await this.prisma.activity.groupBy({
|
||||
by: ['status', 'stageId'],
|
||||
where,
|
||||
@@ -341,6 +356,7 @@ export class ActivitiesService {
|
||||
where: { id },
|
||||
data: {
|
||||
stageId: stage.id,
|
||||
boardId: stage.boardId,
|
||||
status: stage.kind,
|
||||
completedAt:
|
||||
stage.kind === 'done'
|
||||
@@ -401,6 +417,12 @@ export class ActivitiesService {
|
||||
const initialStage = initialStageId
|
||||
? await this.stages.get(organizationId, initialStageId)
|
||||
: null;
|
||||
const requestedBoardId = emptyToNull(dto.boardId ?? null);
|
||||
const boardId = initialStage
|
||||
? initialStage.boardId
|
||||
: requestedBoardId
|
||||
? await this.stages.resolveBoardId(organizationId, requestedBoardId)
|
||||
: null;
|
||||
const resolved = await this.types.resolve(organizationId, {
|
||||
typeDefId: dto.typeDefId,
|
||||
type: dto.type,
|
||||
@@ -414,6 +436,7 @@ export class ActivitiesService {
|
||||
const row = await this.prisma.activity.create({
|
||||
data: {
|
||||
organizationId,
|
||||
boardId,
|
||||
...(initialStage
|
||||
? {
|
||||
stageId: initialStage.id,
|
||||
@@ -513,6 +536,19 @@ export class ActivitiesService {
|
||||
});
|
||||
const data: Prisma.ActivityUncheckedUpdateInput = {};
|
||||
if (dto.title !== undefined) data.title = dto.title.trim();
|
||||
if ('boardId' in dto) {
|
||||
const nextBoardId = await this.stages.resolveBoardId(organizationId, dto.boardId);
|
||||
const current = await this.prisma.activity.findUnique({
|
||||
where: { id },
|
||||
select: { boardId: true },
|
||||
});
|
||||
const defaultBoard = await this.stages.ensureDefaultBoard(organizationId);
|
||||
if (nextBoardId !== (current?.boardId ?? defaultBoard.id)) {
|
||||
// ستون قبلی مال گروه دیگر است؛ فعالیت به ستون اولِ هموضعیت گروه جدید میرود
|
||||
data.boardId = nextBoardId;
|
||||
data.stageId = null;
|
||||
}
|
||||
}
|
||||
let addedParticipants: string[] = [];
|
||||
const typeChanged = 'typeDefId' in dto || dto.type !== undefined;
|
||||
if (typeChanged || dto.fields !== undefined) {
|
||||
@@ -922,6 +958,7 @@ export class ActivitiesService {
|
||||
? (row.fields as unknown as ActivityFieldValues)
|
||||
: {};
|
||||
return {
|
||||
boardId: this.stages.effectiveBoardId(row, ctx?.stages),
|
||||
stageId: this.stages.effectiveStageId(row, ctx?.stages),
|
||||
typeDef: row.typeDef
|
||||
? this.types.toSummary(row.typeDef)
|
||||
|
||||
Reference in New Issue
Block a user