fix: align activity badge and person profile with prior lens

Count nav badge for desk=mine, always apply activity prior filter on person records, and stop loading prior=all deals on profiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 18:56:44 +03:30
co-authored by Cursor
parent 0ce2533066
commit 3fb5f32ac3
4 changed files with 72 additions and 66 deletions
@@ -57,8 +57,12 @@ export class ActivitiesController {
) {} ) {}
@Get('summary') @Get('summary')
summary(@CurrentUser() user: Actor) { summary(
return this.activitiesService.summary(user.organizationId, user); @CurrentUser() user: Actor,
@Query('desk') desk?: string,
) {
const scope = desk === 'mine' || desk === 'team' ? desk : undefined;
return this.activitiesService.summary(user.organizationId, user, scope);
} }
@Get('stages') @Get('stages')
+12 -5
View File
@@ -118,9 +118,19 @@ export class ActivitiesService {
async summary( async summary(
organizationId: string, organizationId: string,
actor: ActivityActor, actor: ActivityActor,
desk?: 'mine' | 'team',
): Promise<ActivitySummaryDto> { ): Promise<ActivitySummaryDto> {
const { start, end } = dayBounds(); const { start, end } = dayBounds();
const scope = activityAccessWhere(actor); const scope: Prisma.ActivityWhereInput =
desk === 'mine'
? {
OR: [
{ assigneeId: actor.userId },
{ assignees: { some: { id: actor.userId } } },
{ participantIds: { has: actor.userId } },
],
}
: activityAccessWhere(actor);
const prior = activityPriorWhere(actor.priorLens, actor.priorCutoff); const prior = activityPriorWhere(actor.priorLens, actor.priorCutoff);
const [todayCount, overdueCount] = await Promise.all([ const [todayCount, overdueCount] = await Promise.all([
this.prisma.activity.count({ this.prisma.activity.count({
@@ -733,10 +743,7 @@ export class ActivitiesService {
...this.types.searchByName(typeRows, t), ...this.types.searchByName(typeRows, t),
]); ]);
if (search) andParts.push(search); if (search) andParts.push(search);
const onRecord = Boolean(entity?.personId || entity?.companyId || entity?.dealId); andParts.push(activityPriorWhere(actor.priorLens, actor.priorCutoff));
if (!onRecord) {
andParts.push(activityPriorWhere(actor.priorLens, actor.priorCutoff));
}
const base: Prisma.ActivityWhereInput = { const base: Prisma.ActivityWhereInput = {
organizationId, organizationId,
AND: andParts, AND: andParts,
+3 -1
View File
@@ -305,7 +305,9 @@ async function refreshNotifs() {
async function loadActivityBadge() { async function loadActivityBadge() {
try { try {
const { data } = await api.get<ActivitySummaryDto>('/activities/summary'); const { data } = await api.get<ActivitySummaryDto>('/activities/summary', {
params: { desk: 'mine' },
});
activityBadge.value = data.todayCount; activityBadge.value = data.todayCount;
} catch { } catch {
activityBadge.value = 0; activityBadge.value = 0;
+51 -58
View File
@@ -184,6 +184,7 @@ const previousSourceLabel = computed(() => {
const auth = useAuthStore(); const auth = useAuthStore();
const priorArchiveOn = computed(() => Boolean(auth.user?.priorArchive)); const priorArchiveOn = computed(() => Boolean(auth.user?.priorArchive));
const priorLens = computed(() => auth.user?.priorArchive?.lens ?? null);
function isPriorActivity(row: ActivityDto) { function isPriorActivity(row: ActivityDto) {
const cutoff = auth.user?.priorArchive?.cutoffAt; const cutoff = auth.user?.priorArchive?.cutoffAt;
@@ -199,8 +200,24 @@ function isPriorDeal(row: DealDto) {
return new Date(row.createdAt).getTime() < new Date(cutoff).getTime(); return new Date(row.createdAt).getTime() < new Date(cutoff).getTime();
} }
/** با لنز کار جاری فقط جاری؛ با بایگانی فقط قبلی؛ بدون قابلیت همه. */
function inActiveLensActivity(row: ActivityDto) {
if (!priorArchiveOn.value || !auth.user?.priorArchive?.cutoffAt) return true;
const prior = isPriorActivity(row);
return priorLens.value === 'archive' ? prior : !prior;
}
function inActiveLensDeal(row: DealDto) {
if (!priorArchiveOn.value || !auth.user?.priorArchive?.cutoffAt) return true;
const prior = isPriorDeal(row);
return priorLens.value === 'archive' ? prior : !prior;
}
const lensActivities = computed(() => activities.value.filter(inActiveLensActivity));
const lensDeals = computed(() => deals.value.filter(inActiveLensDeal));
const lastCallAt = computed(() => { const lastCallAt = computed(() => {
const calls = activities.value const calls = lensActivities.value
.filter((a) => a.type === 'call') .filter((a) => a.type === 'call')
.map((a) => .map((a) =>
a.status === 'done' ? a.completedAt || a.dueAt || a.createdAt : a.createdAt, a.status === 'done' ? a.completedAt || a.dueAt || a.createdAt : a.createdAt,
@@ -217,23 +234,23 @@ const daysSinceCall = computed(() =>
const nextActivity = computed(() => { const nextActivity = computed(() => {
const now = Date.now(); const now = Date.now();
return ( return (
activities.value lensActivities.value
.filter((a) => a.status === 'open' && a.dueAt) .filter((a) => a.status === 'open' && a.dueAt)
.sort((a, b) => new Date(a.dueAt!).getTime() - new Date(b.dueAt!).getTime()) .sort((a, b) => new Date(a.dueAt!).getTime() - new Date(b.dueAt!).getTime())
.find((a) => new Date(a.dueAt!).getTime() >= now - 60 * 60 * 1000) ?? .find((a) => new Date(a.dueAt!).getTime() >= now - 60 * 60 * 1000) ??
activities.value lensActivities.value
.filter((a) => a.status === 'open' && a.dueAt) .filter((a) => a.status === 'open' && a.dueAt)
.sort((a, b) => new Date(a.dueAt!).getTime() - new Date(b.dueAt!).getTime())[0] ?? .sort((a, b) => new Date(a.dueAt!).getTime() - new Date(b.dueAt!).getTime())[0] ??
null null
); );
}); });
const openDeals = computed(() => deals.value.filter((d) => d.status === 'open')); const openDeals = computed(() => lensDeals.value.filter((d) => d.status === 'open'));
const currentDeals = computed(() => const currentDeals = computed(() =>
priorArchiveOn.value ? deals.value.filter((d) => !isPriorDeal(d)) : deals.value, priorLens.value === 'archive' ? [] : lensDeals.value,
); );
const priorDeals = computed(() => const priorDeals = computed(() =>
priorArchiveOn.value ? deals.value.filter((d) => isPriorDeal(d)) : [], priorLens.value === 'archive' ? lensDeals.value : [],
); );
const lastQuote = computed( const lastQuote = computed(
@@ -323,7 +340,7 @@ function timelineIcon(tone: TimelineTone) {
const timeline = computed<TimelineRow[]>(() => { const timeline = computed<TimelineRow[]>(() => {
const rows: TimelineRow[] = []; const rows: TimelineRow[] = [];
for (const a of activities.value) { for (const a of lensActivities.value) {
const at = new Date(a.dueAt || a.createdAt).getTime(); const at = new Date(a.dueAt || a.createdAt).getTime();
rows.push({ rows.push({
key: `a-${a.id}`, key: `a-${a.id}`,
@@ -336,27 +353,30 @@ const timeline = computed<TimelineRow[]>(() => {
prior: isPriorActivity(a), prior: isPriorActivity(a),
}); });
} }
for (const n of notes.value) { // یادداشت و پیشنهاد در هر دو لنز می‌مانند (فقط معامله/فعالیت جدا می‌شوند)
rows.push({ if (priorLens.value !== 'archive') {
key: `n-${n.id}`, for (const n of notes.value) {
title: n.title?.trim() || 'یادداشت', rows.push({
body: n.body, key: `n-${n.id}`,
when: historyWhen(n.createdAt), title: n.title?.trim() || 'یادداشت',
tone: 'note', body: n.body,
href: '/notes', when: historyWhen(n.createdAt),
at: new Date(n.createdAt).getTime(), tone: 'note',
}); href: '/notes',
} at: new Date(n.createdAt).getTime(),
for (const q of quotes.value) { });
rows.push({ }
key: `q-${q.id}`, for (const q of quotes.value) {
title: 'ایمیل ارسال شد', rows.push({
body: `پیشنهاد شماره ${q.number} با مبلغ ${formatMoney(q.totalAmount, q.currency)} ارسال شد`, key: `q-${q.id}`,
when: historyWhen(q.createdAt), title: 'ایمیل ارسال شد',
tone: 'quote', body: `پیشنهاد شماره ${q.number} با مبلغ ${formatMoney(q.totalAmount, q.currency)} ارسال شد`,
href: `/quotes/${q.id}`, when: historyWhen(q.createdAt),
at: new Date(q.createdAt).getTime(), tone: 'quote',
}); href: `/quotes/${q.id}`,
at: new Date(q.createdAt).getTime(),
});
}
} }
return rows.sort((a, b) => b.at - a.at); return rows.sort((a, b) => b.at - a.at);
}); });
@@ -366,17 +386,7 @@ const visibleTimeline = computed(() => {
historyFilter.value === 'all' historyFilter.value === 'all'
? timeline.value ? timeline.value
: timeline.value.filter((row) => row.tone === historyFilter.value); : timeline.value.filter((row) => row.tone === historyFilter.value);
const current = priorArchiveOn.value ? rows.filter((row) => !row.prior) : rows; return rows.slice(0, 8);
return current.slice(0, 8);
});
const priorTimeline = computed(() => {
if (!priorArchiveOn.value) return [];
const rows =
historyFilter.value === 'all'
? timeline.value
: timeline.value.filter((row) => row.tone === historyFilter.value);
return rows.filter((row) => row.prior).slice(0, 8);
}); });
function fileKind(name: string, mime: string): AttachmentRow['kind'] { function fileKind(name: string, mime: string): AttachmentRow['kind'] {
@@ -478,7 +488,7 @@ async function load() {
api.get<PaginatedResponse<MemoryItemDto>>('/memory', { api.get<PaginatedResponse<MemoryItemDto>>('/memory', {
params: { personId: id.value, limit: 50 }, params: { personId: id.value, limit: 50 },
}), }),
api.get<PipelineBoardDto>('/pipelines/board', { params: { prior: 'all' } }), api.get<PipelineBoardDto>('/pipelines/board'),
]); ]);
person.value = personRes.data; person.value = personRes.data;
activities.value = actRes.data.items; activities.value = actRes.data.items;
@@ -868,24 +878,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
</RouterLink> </RouterLink>
</li> </li>
</ol> </ol>
<p v-else-if="!priorTimeline.length" class="empty">موردی با این فیلتر نیست.</p> <p v-else class="empty">موردی با این فیلتر نیست.</p>
<template v-if="priorTimeline.length">
<h3 class="prior-head">بایگانی اطلاعات قبل</h3>
<ol class="tl">
<li v-for="row in priorTimeline" :key="row.key">
<RouterLink :to="row.href">
<span class="dot" :class="row.tone">
<UiIcon :name="timelineIcon(row.tone)" :size="14" />
</span>
<div>
<time>{{ row.when }}</time>
<strong>{{ row.title }}</strong>
<p v-if="row.body">{{ row.body }}</p>
</div>
</RouterLink>
</li>
</ol>
</template>
<RouterLink class="see-all" :to="{ name: 'activities', query: { filter: 'all' } }"> <RouterLink class="see-all" :to="{ name: 'activities', query: { filter: 'all' } }">
مشاهده همه فعالیت‌ها مشاهده همه فعالیت‌ها
</RouterLink> </RouterLink>