fix: keep imported follow-ups out of current work

They stayed on the board when a non-prior deal was attached, even though the person was prior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-15 12:34:40 +03:30
co-authored by Cursor
parent 6bc5e1bec7
commit 6bff9d2888
9 changed files with 102 additions and 16 deletions
@@ -38,7 +38,7 @@ import {
type DtoContext = { stages?: StageDefaults; types?: SystemTypes };
type ActivityActor = Pick<Actor, 'userId' | 'role' | 'priorLens'>;
type ActivityActor = Pick<Actor, 'userId' | 'role' | 'priorLens' | 'priorCutoff'>;
/** stageId = شناسهٔ مرحله یا 'all'؛ وقتی ست باشد فیلتر زمانی وضعیت را تحمیل نمی‌کند (جز عقب‌افتاده) */
type StageScope = { stageId: string };
@@ -119,7 +119,7 @@ export class ActivitiesService {
): Promise<ActivitySummaryDto> {
const { start, end } = dayBounds();
const scope = activityAccessWhere(actor);
const prior = activityPriorWhere(actor.priorLens);
const prior = activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff);
const [todayCount, overdueCount] = await Promise.all([
this.prisma.activity.count({
where: {
@@ -700,7 +700,9 @@ export class ActivitiesService {
]);
if (search) andParts.push(search);
const onRecord = Boolean(entity?.personId || entity?.companyId || entity?.dealId);
if (!onRecord) andParts.push(activityPriorWhere(actor.priorLens));
if (!onRecord) {
andParts.push(activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff));
}
const base: Prisma.ActivityWhereInput = {
organizationId,
AND: andParts,
+1 -1
View File
@@ -406,7 +406,7 @@ Rules:
organizationId: orgId,
...assigneeWhere(actor),
...created,
...activityPriorWhere(actor.priorLens),
...activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff),
},
}),
ticket: () =>
+2 -1
View File
@@ -64,7 +64,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
dataVisibilityScopes: true,
priorArchiveEnabled: true,
priorArchiveLens: true,
organization: { select: { enabledModules: true } },
organization: { select: { enabledModules: true, priorArchiveEnabledAt: true } },
},
});
if (!membership) {
@@ -107,6 +107,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
visibleUserIds,
scopes,
priorLens: priorGranted ? priorLens : 'off',
priorCutoff: priorGranted ? membership.organization.priorArchiveEnabledAt : null,
};
}
+10 -3
View File
@@ -287,17 +287,24 @@ export class BillingService {
)
)
OR (
a."dealId" IS NULL
AND a."createdAt" < ${cutoff}
a."createdAt" < ${cutoff}
AND (
EXISTS (
SELECT 1 FROM "Person" AS p
WHERE p."id" = a."personId" AND p."importSource" = 'didar'
WHERE p."id" = a."personId"
AND (
p."importSource" = 'didar'
OR p."previousImportSource" = 'didar'
)
)
OR EXISTS (
SELECT 1 FROM "Company" AS c
WHERE c."id" = a."companyId" AND c."importSource" = 'didar'
)
OR EXISTS (
SELECT 1 FROM "Deal" AS d2
WHERE d2."id" = a."dealId" AND d2."importSource" = 'didar'
)
)
)
)
+43 -5
View File
@@ -25,6 +25,39 @@ function liveActivity(now: Date): Prisma.ActivityWhereInput {
};
}
/** فعالیت باز با موعد آینده، حتی اگر قبلی باشد، در کار جاری می‌ماند. */
function stillOpenAhead(now: Date): Prisma.ActivityWhereInput {
return { status: 'open', dueAt: { gt: now } };
}
/**
* فعالیت قبلی است اگر منبعش didar باشد،
* یا قبل از روشن شدن ماژول ساخته شده و به شخص/شرکت/معاملهٔ قبلی وصل باشد.
* معاملهٔ غیرقبلی مانع نمی‌شود؛ شخص قبلی (حتی اگر بعداً منبعش عوض شده) کافی است.
*/
function historicalActivity(cutoff: Date): Prisma.ActivityWhereInput {
return {
OR: [
{ importSource: PRIOR_IMPORT_SOURCE },
{
createdAt: { lt: cutoff },
OR: [
{
person: {
OR: [
{ importSource: PRIOR_IMPORT_SOURCE },
{ previousImportSource: PRIOR_IMPORT_SOURCE },
],
},
},
{ company: { importSource: PRIOR_IMPORT_SOURCE } },
{ deal: { importSource: PRIOR_IMPORT_SOURCE } },
],
},
],
};
}
export function personPriorWhere(lens: PriorLens | undefined): Prisma.PersonWhereInput {
if (lens === 'archive') return { importSource: PRIOR_IMPORT_SOURCE };
if (lens === 'current') return { importSource: { not: PRIOR_IMPORT_SOURCE } };
@@ -50,14 +83,19 @@ export function dealPriorWhere(lens: PriorLens | undefined, now = new Date()): P
export function activityPriorWhere(
lens: PriorLens | undefined,
now = new Date(),
cutoff?: Date | null,
): Prisma.ActivityWhereInput {
if (lens === 'current') {
return { OR: [{ importSource: { not: PRIOR_IMPORT_SOURCE } }, liveActivity(now)] };
}
if (lens === 'archive') {
if (lens !== 'current' && lens !== 'archive') return {};
if (!cutoff) {
if (lens === 'current') {
return { OR: [{ importSource: { not: PRIOR_IMPORT_SOURCE } }, liveActivity(now)] };
}
return { importSource: PRIOR_IMPORT_SOURCE, NOT: liveActivity(now) };
}
return {};
const prior = historicalActivity(cutoff);
const live = stillOpenAhead(now);
if (lens === 'current') return { OR: [{ NOT: prior }, live] };
return { AND: [prior, { NOT: live }] };
}
/** فهرست شخص/شرکت: در کار جاری، جستجو و فیلتر صریح منبع، قبلی‌ها را هم پیدا می‌کند. */
+2
View File
@@ -22,6 +22,8 @@ export type Actor = {
scopes?: Partial<Record<DataScope, ScopeVisibility>>;
/** off = فیلتر خاموش؛ current = کار جاری؛ archive = بایگانی اطلاعات قبل */
priorLens?: 'off' | 'current' | 'archive';
/** لحظهٔ روشن شدن ماژول برای این محیط؛ فعالیت‌های ساخته‌شده قبل از آن می‌توانند قبلی باشند */
priorCutoff?: Date | null;
};
export type ScopedActor = Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>;
+2 -2
View File
@@ -64,7 +64,7 @@ export class ReportsService {
const now = new Date();
const peoplePrior = personPriorWhere(actor.priorLens);
const dealsPrior = dealPriorWhere(actor.priorLens, now);
const activitiesPrior = activityPriorWhere(actor.priorLens, now);
const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff);
const { jy, jm } = dateToJalali(now);
const sixAgo = addJalaliMonths(jy, jm, -5);
const sixMonthStart = startOfJalaliMonth(sixAgo.jy, sixAgo.jm);
@@ -1553,7 +1553,7 @@ export class ReportsService {
weekAgo.setDate(weekAgo.getDate() - 7);
const peoplePrior = personPriorWhere(actor.priorLens);
const dealsPrior = dealPriorWhere(actor.priorLens, now);
const activitiesPrior = activityPriorWhere(actor.priorLens, now);
const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff);
const [
wonDeals,
+36
View File
@@ -36,6 +36,42 @@ describe('بایگانی اطلاعات قبل', () => {
});
});
it('فعالیت قدیمی روی شخص قبلی از کار جاری کنار می‌رود', () => {
const cutoff = new Date('2026-10-01T00:00:00.000Z');
const current = JSON.parse(JSON.stringify(activityPriorWhere('current', now, cutoff))) as {
OR: [
{
NOT: {
OR: [
{ importSource: string },
{
createdAt: { lt: string };
OR: Array<{ person?: { OR: Array<Record<string, string>> } }>;
},
];
};
},
{ status: string; dueAt: { gt: string } },
];
};
const linked = current.OR[0].NOT.OR[1];
assert.equal(linked.createdAt.lt, cutoff.toISOString());
assert.deepEqual(linked.OR[0], {
person: {
OR: [{ importSource: 'didar' }, { previousImportSource: 'didar' }],
},
});
assert.deepEqual(current.OR[1], { status: 'open', dueAt: { gt: now.toISOString() } });
const archive = JSON.parse(JSON.stringify(activityPriorWhere('archive', now, cutoff))) as {
AND: [unknown, { NOT: { status: string; dueAt: { gt: string } } }];
};
assert.deepEqual(archive.AND[0], current.OR[0].NOT);
assert.deepEqual(archive.AND[1], {
NOT: { status: 'open', dueAt: { gt: now.toISOString() } },
});
});
it('جستجوی کار جاری شخص قبلی را پیدا می‌کند', () => {
assert.equal(listPriorLens('current', 'علی', false), 'off');
assert.equal(listPriorLens('current', ' ', false), 'current');
@@ -26,7 +26,7 @@
رکورد قبلی یعنی:
- شخص، شرکت یا معامله‌ای که منبع ورودش `didar` است.
- فعالیتی که منبع ورودش `didar` است. فعالیت امروز این فیلد را ندارد؛ در پیاده‌سازی همان فیلد با پیش‌فرض `manual` اضافه می‌شود. پر کردن اولیه: اگر معاملهٔ مرتبط `didar` باشد، یا معامله نداشته باشد و شخص یا شرکت مرتبط `didar` باشد و تاریخ ساخت فعالیت قبل از لحظهٔ روشن شدن ماژول برای آن محیط باشد. فعالیتی که بعد از آن لحظه در منتکت ساخته شود جاری است، حتی اگر روی شخص قبلی باشد.
- فعالیتی که منبع ورودش `didar` است. فعالیت امروز این فیلد را ندارد؛ در پیاده‌سازی همان فیلد با پیش‌فرض `manual` اضافه می‌شود. پر کردن اولیه: اگر معاملهٔ مرتبط `didar` باشد، یا تاریخ ساخت فعالیت قبل از لحظهٔ روشن شدن ماژول باشد و شخص (حتی با منبع قبلی `didar`)، شرکت یا معاملهٔ مرتبط `didar` باشد. وجود معاملهٔ غیرقبلی این را لغو نمی‌کند. فعالیتی که بعد از آن لحظه در منتکت ساخته شود جاری است، حتی اگر روی شخص قبلی باشد.
استثنا فقط در حالت کار جاری: معامله یا فعالیت قبلی که هنوز باز است و موعدش در آینده است، در کار جاری می‌آید و در بایگانی نمی‌آید. معامله یا فعالیت باز بدون موعد، یا با موعد گذشته، بایگانی می‌ماند. با بسته شدن یا گذشتن موعد، از کار جاری خارج و فقط در بایگانی دیده می‌شود.