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