From 32e27d45f76e7fedb10cb3cd82eaaf5449e7958c Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 28 Sep 2026 12:28:14 +0330 Subject: [PATCH] Add desk-style task workflow on Activities and Project boards. Managers can switch my desk vs team, reassign activities, and filter project cards to their work; assignees can see and move cards. --- .../src/activities/activities.controller.ts | 1 + apps/api/src/activities/activities.service.ts | 7 +- apps/api/src/activities/dto.ts | 4 + apps/api/src/common/record-scope.ts | 14 +++ .../project-boards/project-boards.service.ts | 16 +-- .../project-cards-excel.service.ts | 4 +- apps/web/src/layouts/AppLayout.vue | 6 +- apps/web/src/lib/nav-hubs.ts | 16 +-- apps/web/src/lib/pipeline-board.ts | 57 +++++++--- apps/web/src/views/ActivitiesView.vue | 80 +++++++++++++- apps/web/src/views/ActivityDetailView.vue | 102 +++++++++++++++++- apps/web/src/views/ProjectBoardView.vue | 41 ++++++- docs/وضعیت-پیشرفت.md | 5 +- 13 files changed, 303 insertions(+), 50 deletions(-) diff --git a/apps/api/src/activities/activities.controller.ts b/apps/api/src/activities/activities.controller.ts index 5093296..a4312f3 100644 --- a/apps/api/src/activities/activities.controller.ts +++ b/apps/api/src/activities/activities.controller.ts @@ -62,6 +62,7 @@ export class ActivitiesController { from: query.from, to: query.to, }, + query.desk, ); } diff --git a/apps/api/src/activities/activities.service.ts b/apps/api/src/activities/activities.service.ts index d5f603a..41d493e 100644 --- a/apps/api/src/activities/activities.service.ts +++ b/apps/api/src/activities/activities.service.ts @@ -128,6 +128,7 @@ export class ActivitiesService { from?: string; to?: string; }, + desk?: 'mine' | 'team', ): Promise> { const where = this.buildWhere( organizationId, @@ -136,6 +137,7 @@ export class ActivitiesService { q, entity, range, + desk, ); const take = Math.min(Math.max(limit, 1), 200); const [rows, total] = await this.prisma.$transaction([ @@ -324,11 +326,14 @@ export class ActivitiesService { from?: string; to?: string; }, + desk?: 'mine' | 'team', ): Prisma.ActivityWhereInput { const { start, end } = dayBounds(); + const scope = + desk === 'mine' ? { assigneeId: actor.userId } : assigneeWhere(actor); const base: Prisma.ActivityWhereInput = { organizationId, - ...assigneeWhere(actor), + ...scope, ...(entity?.personId ? { personId: entity.personId } : {}), ...(entity?.companyId ? { companyId: entity.companyId } : {}), ...(entity?.dealId ? { dealId: entity.dealId } : {}), diff --git a/apps/api/src/activities/dto.ts b/apps/api/src/activities/dto.ts index 57b21b6..1678a1a 100644 --- a/apps/api/src/activities/dto.ts +++ b/apps/api/src/activities/dto.ts @@ -28,6 +28,10 @@ export class ListActivitiesQueryDto { @IsString() q?: string; + @IsOptional() + @IsIn(['mine', 'team']) + desk?: 'mine' | 'team'; + @IsOptional() @IsString() personId?: string; diff --git a/apps/api/src/common/record-scope.ts b/apps/api/src/common/record-scope.ts index fed59dd..b6a0475 100644 --- a/apps/api/src/common/record-scope.ts +++ b/apps/api/src/common/record-scope.ts @@ -62,6 +62,20 @@ export function dealAccessWhere( }; } +/** کارت پروژه: مالک یا یکی از مسئولان در محدوده دید */ +export function projectCardAccessWhere( + actor: Pick, +): Prisma.ProjectCardWhereInput { + const ids = scopedOwnerIds(actor); + if (!ids) return {}; + return { + OR: [ + { ownerId: { in: ids } }, + { assignees: { some: { id: { in: ids } } } }, + ], + }; +} + /** فعالیت‌ها بر اساس مسئول */ export function assigneeWhere( actor: Pick, diff --git a/apps/api/src/project-boards/project-boards.service.ts b/apps/api/src/project-boards/project-boards.service.ts index 2e5114b..22b697e 100644 --- a/apps/api/src/project-boards/project-boards.service.ts +++ b/apps/api/src/project-boards/project-boards.service.ts @@ -18,6 +18,7 @@ import { canReassignOwner, dealAccessWhere, ownerWhere, + projectCardAccessWhere, resolveOwnerId, } from '../common/record-scope'; import { normalizeTags } from '../common/tags'; @@ -32,7 +33,10 @@ const cardInclude = { assignees: { select: { id: true, fullName: true } }, } satisfies Prisma.ProjectCardInclude; -type CardActor = Pick; +type CardActor = Pick< + Actor, + 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' +>; @Injectable() export class ProjectBoardsService { @@ -113,7 +117,7 @@ export class ProjectBoardsService { where: { organizationId, boardId: resolved.id, - ...(actor ? ownerWhere(actor) : {}), + ...(actor ? projectCardAccessWhere(actor) : {}), }, include: cardInclude, orderBy: [{ position: 'asc' }, { updatedAt: 'desc' }], @@ -143,7 +147,7 @@ export class ProjectBoardsService { actor: CardActor, ): Promise { const row = await this.prisma.projectCard.findFirst({ - where: { id, organizationId, ...ownerWhere(actor) }, + where: { id, organizationId, ...projectCardAccessWhere(actor) }, include: cardInclude, }); if (!row) throw new NotFoundException('کارت پیدا نشد'); @@ -225,7 +229,7 @@ export class ProjectBoardsService { dto: UpdateProjectCardRequest, ): Promise { const existing = await this.prisma.projectCard.findFirst({ - where: { id, organizationId, ...ownerWhere(actor) }, + where: { id, organizationId, ...projectCardAccessWhere(actor) }, }); if (!existing) throw new NotFoundException('کارت پیدا نشد'); @@ -279,7 +283,7 @@ export class ProjectBoardsService { dto: MoveProjectCardRequest, ): Promise { const card = await this.prisma.projectCard.findFirst({ - where: { id, organizationId, ...ownerWhere(actor) }, + where: { id, organizationId, ...projectCardAccessWhere(actor) }, select: { id: true, boardId: true }, }); if (!card) throw new NotFoundException('کارت پیدا نشد'); @@ -312,7 +316,7 @@ export class ProjectBoardsService { actor: CardActor, ): Promise { const found = await this.prisma.projectCard.findFirst({ - where: { id, organizationId, ...ownerWhere(actor) }, + where: { id, organizationId, ...projectCardAccessWhere(actor) }, select: { id: true }, }); if (!found) throw new NotFoundException('کارت پیدا نشد'); diff --git a/apps/api/src/project-boards/project-cards-excel.service.ts b/apps/api/src/project-boards/project-cards-excel.service.ts index d9ba0ce..1dd465b 100644 --- a/apps/api/src/project-boards/project-cards-excel.service.ts +++ b/apps/api/src/project-boards/project-cards-excel.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import type { ImportContactsResult } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import type { Actor } from '../common/record-scope'; -import { ownerWhere } from '../common/record-scope'; +import { projectCardAccessWhere } from '../common/record-scope'; import { normalizePhone } from '../contacts/normalize'; import { excelFieldsFromColumns, @@ -68,7 +68,7 @@ export class ProjectCardsExcelService { async export(actor: Actor): Promise { const rows = await this.prisma.projectCard.findMany({ - where: { organizationId: actor.organizationId, ...ownerWhere(actor) }, + where: { organizationId: actor.organizationId, ...projectCardAccessWhere(actor) }, include: { person: { select: { id: true, firstName: true, lastName: true, mobile: true }, diff --git a/apps/web/src/layouts/AppLayout.vue b/apps/web/src/layouts/AppLayout.vue index adb27d6..964fbea 100644 --- a/apps/web/src/layouts/AppLayout.vue +++ b/apps/web/src/layouts/AppLayout.vue @@ -512,13 +512,13 @@ onUnmounted(() => {