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:
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user