fix(routines): keep the daily desk for internal team work

Customer-linked tasks stay on their record and no longer appear in today, calendar, score, or reminders.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-29 20:53:26 +03:30
co-authored by Cursor
parent 99d686fefb
commit a17c7dbabc
11 changed files with 115 additions and 90 deletions
@@ -30,6 +30,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { type Actor, elevated } from '../common/record-scope';
import { dayKeyToDate, tehranDayKey, addDays } from '../routines/routine-dates';
import { internalRoutineTaskWhere } from '../routines/internal-routine';
import { normalizeLevels, periodStart, previousPeriod, sanitizeConditions } from './gamification-engine';
import { GamificationService, parsePeriod, toRuleDto } from './gamification.service';
import type {
@@ -751,6 +752,7 @@ export class GamificationAdminService {
organizationId,
status: { not: 'cancelled' },
scheduledFor: { gte: dayKeyToDate(fromKey), lte: dayKeyToDate(toKey) },
...internalRoutineTaskWhere,
};
const [total, done] = await Promise.all([
this.prisma.routineTask.count({ where }),
+26
View File
@@ -0,0 +1,26 @@
import { Prisma, type RoutineTaskSource } from '@prisma/client';
/**
* کارهای روتین فقط کار داخلی تیم است.
* پیگیری، لید، مشتری و معامله روی پرونده خودشان می‌مانند و وارد میز روتین نمی‌شوند.
*/
export const internalRoutineTaskWhere: Prisma.RoutineTaskWhereInput = {
source: { in: ['routine', 'manual'] },
leadId: null,
personId: null,
companyId: null,
dealId: null,
invoiceId: null,
activityId: null,
};
/** فیلتر میز روتین. منبع مشتری حتی اگر صریحاً خواسته شود وارد لیست نمی‌شود. */
export function deskRoutineTaskWhere(
source?: RoutineTaskSource | null,
): Prisma.RoutineTaskWhereInput {
if (source === 'routine' || source === 'manual') {
return { ...internalRoutineTaskWhere, source };
}
if (source) return { AND: [internalRoutineTaskWhere, { source }] };
return internalRoutineTaskWhere;
}
@@ -3,6 +3,7 @@ import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { dayKeyToDate, tehranDayKey, tehranHour, toFaDigits } from './routine-dates';
import { internalRoutineTaskWhere } from './internal-routine';
const TIME_FMT = new Intl.DateTimeFormat('fa-IR', {
timeZone: 'Asia/Tehran',
@@ -44,6 +45,7 @@ export class RoutineRemindersService {
status: { in: ['todo', 'in_progress'] },
reminderAt: { lte: now },
reminderSentAt: null,
...internalRoutineTaskWhere,
},
select: { id: true, organizationId: true, assigneeId: true, title: true, dueAt: true, reminderAt: true },
take: 300,
@@ -76,7 +78,7 @@ export class RoutineRemindersService {
/** هشدار Task اجباری که مهلتش گذشته («… هنوز انجام نشده») */
private async sendOverdueAlerts() {
const rows = await this.prisma.routineTask.findMany({
where: { status: 'overdue', isMandatory: true, overdueNotifiedAt: null },
where: { status: 'overdue', isMandatory: true, overdueNotifiedAt: null, ...internalRoutineTaskWhere },
select: { id: true, organizationId: true, assigneeId: true, title: true },
take: 300,
});
@@ -114,7 +116,11 @@ export class RoutineRemindersService {
const groups = await this.prisma.routineTask.groupBy({
by: ['organizationId', 'assigneeId'],
where: { scheduledFor: todayDate, status: { in: ['todo', 'in_progress'] } },
where: {
scheduledFor: todayDate,
status: { in: ['todo', 'in_progress'] },
...internalRoutineTaskWhere,
},
_count: { _all: true },
});
if (!groups.length) return;
@@ -18,6 +18,7 @@ import type {
} from '@mento/shared';
import type { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { deskRoutineTaskWhere, internalRoutineTaskWhere } from './internal-routine';
import type { RoutineRangeQueryDto } from './dto';
import { describeRecurrence } from './recurrence';
import { routineRule } from './routine-mapper';
@@ -118,6 +119,7 @@ export class RoutineStatsService {
organizationId,
assigneeId: userId,
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(day) },
...internalRoutineTaskWhere,
},
select: { scheduledFor: true, status: true },
});
@@ -155,6 +157,7 @@ export class RoutineStatsService {
isMandatory: true,
status: { not: 'cancelled' },
scheduledFor: { gte: dayKeyToDate(addDays(today, -180)), lte: dayKeyToDate(today) },
...internalRoutineTaskWhere,
},
_count: { _all: true },
});
@@ -437,8 +440,8 @@ export class RoutineStatsService {
organizationId,
assigneeId: { in: userIds },
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(to) },
...deskRoutineTaskWhere(q.source),
...(q.categoryId ? { categoryId: q.categoryId } : {}),
...(q.source ? { source: q.source } : {}),
...(q.status ? { status: q.status } : {}),
},
select: statSelect,
+16 -42
View File
@@ -47,6 +47,7 @@ import {
tehranDateTime,
tehranDayKey,
} from './routine-dates';
import { deskRoutineTaskWhere, internalRoutineTaskWhere } from './internal-routine';
const OPEN_STATUSES: RoutineTaskStatus[] = ['todo', 'in_progress', 'overdue'];
@@ -84,6 +85,7 @@ export class RoutineTasksService {
assigneeId: target,
scheduledFor: dayDate,
status: { not: 'cancelled' },
...internalRoutineTaskWhere,
...deep,
},
include: routineTaskInclude,
@@ -95,6 +97,7 @@ export class RoutineTasksService {
assigneeId: target,
status: 'overdue',
scheduledFor: { lt: dayDate },
...internalRoutineTaskWhere,
...deep,
},
include: { ...routineTaskInclude, routine: { select: { ...routineTaskInclude.routine.select, carryOverOverdue: true } } },
@@ -141,10 +144,16 @@ export class RoutineTasksService {
assigneeId: actor.userId,
scheduledFor: day,
status: { in: ['todo', 'in_progress'] },
...internalRoutineTaskWhere,
},
}),
this.prisma.routineTask.count({
where: { organizationId: actor.organizationId, assigneeId: actor.userId, status: 'overdue' },
where: {
organizationId: actor.organizationId,
assigneeId: actor.userId,
status: 'overdue',
...internalRoutineTaskWhere,
},
}),
]);
return { remaining, overdue };
@@ -158,6 +167,7 @@ export class RoutineTasksService {
where: {
organizationId: actor.organizationId,
status: 'overdue',
...internalRoutineTaskWhere,
...filter,
...this.deepWhere(actor),
},
@@ -179,9 +189,9 @@ export class RoutineTasksService {
scheduledFor: { gte: dayKeyToDate(from), lte: dayKeyToDate(to) },
...filter,
...this.deepWhere(actor),
...deskRoutineTaskWhere(q.source),
...(q.categoryId ? { categoryId: q.categoryId } : {}),
...(q.status ? { status: q.status } : {}),
...(q.source ? { source: q.source } : {}),
};
const rows = await this.prisma.routineTask.findMany({
where,
@@ -311,46 +321,10 @@ export class RoutineTasksService {
return toTaskDto(row);
}
async createFromActivity(actor: Actor, activityId: string): Promise<RoutineTaskDto> {
const activity = await this.prisma.activity.findFirst({
where: { id: activityId, organizationId: actor.organizationId },
select: {
id: true,
title: true,
description: true,
dueAt: true,
priority: true,
personId: true,
companyId: true,
dealId: true,
assigneeId: true,
},
});
if (!activity) throw new NotFoundException('پیگیری پیدا نشد');
const existing = await this.prisma.routineTask.findFirst({
where: { organizationId: actor.organizationId, activityId, status: { not: 'cancelled' } },
include: routineTaskInclude,
});
if (existing) return toTaskDto(existing);
const scope = await this.scope(actor);
const assigneeId =
activity.assigneeId && canManage(scope, activity.assigneeId)
? activity.assigneeId
: actor.userId;
return this.create(actor, {
title: activity.title,
description: activity.description,
dueAt: activity.dueAt?.toISOString() ?? null,
priority: activity.priority,
assigneeId,
personId: activity.personId,
companyId: activity.companyId,
dealId: activity.dealId,
activityId: activity.id,
source: 'follow_up',
});
async createFromActivity(_actor: Actor, _activityId: string): Promise<RoutineTaskDto> {
throw new BadRequestException(
'پیگیری مشتری در کارهای روتین ثبت نمی‌شود. کارهای روتین فقط برای کار داخلی تیم است.',
);
}
async update(actor: Actor, id: string, dto: UpdateRoutineTaskDto): Promise<RoutineTaskDto> {