Add activity priority desk filter and keep detail browsing inside the same column after stage changes.
When done/reopen/stage moves leave a column, advance to the next activity there or return to the list if empty. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -210,6 +210,7 @@ export class ActivitiesController {
|
||||
title: query.title,
|
||||
customer: query.customer,
|
||||
due: query.due,
|
||||
priority: query.priority,
|
||||
boardId: query.boardId,
|
||||
},
|
||||
);
|
||||
@@ -246,6 +247,7 @@ export class ActivitiesController {
|
||||
title: query.title,
|
||||
customer: query.customer,
|
||||
due: query.due,
|
||||
priority: query.priority,
|
||||
boardId: query.boardId,
|
||||
},
|
||||
query.stageId ? { stageId: query.stageId } : undefined,
|
||||
@@ -279,6 +281,7 @@ export class ActivitiesController {
|
||||
title: query.title,
|
||||
customer: query.customer,
|
||||
due: query.due,
|
||||
priority: query.priority,
|
||||
boardId: query.boardId,
|
||||
},
|
||||
query.stageId ? { stageId: query.stageId } : undefined,
|
||||
|
||||
@@ -123,6 +123,7 @@ type ListFacets = {
|
||||
customer?: string;
|
||||
/** set = زمان شروع دارد، unset = ندارد */
|
||||
due?: 'set' | 'unset';
|
||||
priority?: ActivityDto['priority'];
|
||||
/** گروه فعالیت؛ خالی = همهٔ گروهها */
|
||||
boardId?: string;
|
||||
};
|
||||
@@ -1369,11 +1370,13 @@ export class ActivitiesService {
|
||||
const facetTypeDef = facets?.typeDefId?.trim();
|
||||
const facetTitle = facets?.title?.trim();
|
||||
const facetCustomer = facets?.customer?.trim();
|
||||
const facetPriority = facets?.priority;
|
||||
const andParts: Prisma.ActivityWhereInput[] = [
|
||||
scope,
|
||||
...(facets?.type ? [{ type: facets.type }] : []),
|
||||
...(facetTypeDef ? [this.types.whereForType(typeRows, facetTypeDef)] : []),
|
||||
...(facets?.status ? [{ status: facets.status }] : []),
|
||||
...(facetPriority ? [{ priority: facetPriority }] : []),
|
||||
...(facetTitle ? [{ title: ci(facetTitle) }] : []),
|
||||
...(facetCustomer ? [this.customerWhere(facetCustomer)] : []),
|
||||
...(facetAssignee
|
||||
|
||||
@@ -93,6 +93,11 @@ export class ListActivitiesQueryDto {
|
||||
@IsString()
|
||||
typeDefId?: string;
|
||||
|
||||
/** اولویت فعالیت (کلید ثابت؛ برچسب در تنظیمات سازمان) */
|
||||
@IsOptional()
|
||||
@IsIn(ACTIVITY_PRIORITIES)
|
||||
priority?: (typeof ACTIVITY_PRIORITIES)[number];
|
||||
|
||||
/** شناسهٔ مرحله یا 'all' — حالت تب/کانبان مراحل */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -511,6 +511,7 @@ function activityQuery(id: string) {
|
||||
if (source.typeDefId) query.typeDefId = source.typeDefId;
|
||||
else if (source.type) query.type = source.type;
|
||||
if (source.status) query.status = source.status;
|
||||
if (source.priority) query.priority = source.priority;
|
||||
if (source.assigneeId) query.assigneeId = source.assigneeId;
|
||||
if (source.title) query.title = source.title;
|
||||
if (source.customer) query.customer = source.customer;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
export type DeskFilterField = 'title' | 'who' | 'customer' | 'type' | 'when' | 'status';
|
||||
export type DeskFilterField =
|
||||
| 'title'
|
||||
| 'who'
|
||||
| 'customer'
|
||||
| 'type'
|
||||
| 'priority'
|
||||
| 'when'
|
||||
| 'status';
|
||||
|
||||
const props = defineProps<{
|
||||
/** یک ستون، یا پنل کامل بالای صفحه */
|
||||
@@ -10,11 +17,13 @@ const props = defineProps<{
|
||||
customer: string;
|
||||
due: '' | 'set' | 'unset';
|
||||
typeKey: string;
|
||||
priorityKey: string;
|
||||
statusKey: string;
|
||||
assigneeId: string;
|
||||
showAssignee: boolean;
|
||||
users: Array<{ id: string; fullName: string }>;
|
||||
types: Array<{ key: string; label: string }>;
|
||||
priorities: Array<{ key: string; label: string; color?: string }>;
|
||||
statuses: Array<{ key: string; label: string }>;
|
||||
}>();
|
||||
|
||||
@@ -23,6 +32,7 @@ const emit = defineEmits<{
|
||||
customer: [value: string];
|
||||
due: [value: '' | 'set' | 'unset'];
|
||||
type: [value: string];
|
||||
priority: [value: string];
|
||||
status: [value: string];
|
||||
assignee: [value: string];
|
||||
clear: [];
|
||||
@@ -86,6 +96,7 @@ const hasAny = computed(
|
||||
() =>
|
||||
Boolean(props.title.trim() || props.customer.trim() || props.due || props.assigneeId) ||
|
||||
props.typeKey !== 'all' ||
|
||||
props.priorityKey !== 'all' ||
|
||||
props.statusKey !== 'all',
|
||||
);
|
||||
|
||||
@@ -101,6 +112,9 @@ const whoLabel = computed(
|
||||
const typeLabel = computed(
|
||||
() => props.types.find((item) => item.key === props.typeKey)?.label || 'همه',
|
||||
);
|
||||
const priorityLabel = computed(
|
||||
() => props.priorities.find((item) => item.key === props.priorityKey)?.label || 'همه',
|
||||
);
|
||||
const statusLabel = computed(
|
||||
() => props.statuses.find((item) => item.key === props.statusKey)?.label || 'همه',
|
||||
);
|
||||
@@ -144,6 +158,11 @@ function pickType(next: string) {
|
||||
finish();
|
||||
}
|
||||
|
||||
function pickPriority(next: string) {
|
||||
emit('priority', props.priorityKey === next && next !== 'all' ? 'all' : next);
|
||||
finish();
|
||||
}
|
||||
|
||||
function pickStatus(next: string) {
|
||||
emit('status', props.statusKey === next && next !== 'all' ? 'all' : next);
|
||||
finish();
|
||||
@@ -282,6 +301,36 @@ function pickAssignee(next: string) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="shows('priority')">
|
||||
<button
|
||||
v-if="field === 'all'"
|
||||
type="button"
|
||||
class="acc"
|
||||
:class="{ on: priorityKey !== 'all' }"
|
||||
@click="toggleSection('priority')"
|
||||
>
|
||||
<span>اولویت</span>
|
||||
<em>{{ priorityLabel }}</em>
|
||||
</button>
|
||||
<div v-if="opened('priority')" class="pick" role="listbox" aria-label="اولویت">
|
||||
<button
|
||||
v-for="opt in priorities"
|
||||
:key="opt.key"
|
||||
type="button"
|
||||
:class="{ on: priorityKey === opt.key }"
|
||||
@click="pickPriority(opt.key)"
|
||||
>
|
||||
<span
|
||||
v-if="opt.color && opt.key !== 'all'"
|
||||
class="prio-dot"
|
||||
:style="{ background: opt.color }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="shows('when')">
|
||||
<button
|
||||
v-if="field === 'all'"
|
||||
@@ -434,6 +483,9 @@ function pickAssignee(next: string) {
|
||||
}
|
||||
|
||||
.pick button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
text-align: start;
|
||||
border-radius: 8px;
|
||||
padding: 0.42rem 0.5rem;
|
||||
@@ -450,6 +502,13 @@ function pickAssignee(next: string) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.prio-dot {
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.seg {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
@@ -43,6 +43,7 @@ import type {
|
||||
ActivityBoardDto,
|
||||
ActivityDto,
|
||||
ActivityFilter,
|
||||
ActivityPriority,
|
||||
ActivityStageCountsDto,
|
||||
ActivityStageDto,
|
||||
ActivityStatus,
|
||||
@@ -51,7 +52,7 @@ import type {
|
||||
OrgUserDto,
|
||||
PaginatedResponse,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { ACTIVITY_PRIORITY_KEYS, brand } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canMutateActivity } from '@/lib/activity-access';
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
@@ -95,6 +96,7 @@ const actGridStyle = computed(() => ({
|
||||
|
||||
/** 'all' یا شناسهٔ نوع فعالیت */
|
||||
type TypeFilter = string;
|
||||
type PriorityFilter = 'all' | ActivityPriority;
|
||||
type StatusFilter = 'all' | ActivityStatus;
|
||||
type UiStatus = 'done' | 'progress' | 'planned' | 'overdue' | 'canceled';
|
||||
type DeskScope = 'mine' | 'team';
|
||||
@@ -122,6 +124,18 @@ const STATUS_FILTERS: Array<{ key: StatusFilter; label: string }> = [
|
||||
{ key: 'canceled', label: 'لغو شده' },
|
||||
];
|
||||
|
||||
const activityPriorities = useActivityPriorities();
|
||||
const PRIORITY_FILTERS = computed<
|
||||
Array<{ key: PriorityFilter; label: string; color?: string }>
|
||||
>(() => [
|
||||
{ key: 'all', label: 'همه اولویتها' },
|
||||
...activityPriorities.items.value.map((row) => ({
|
||||
key: row.key as PriorityFilter,
|
||||
label: row.label,
|
||||
color: row.color,
|
||||
})),
|
||||
]);
|
||||
|
||||
const pageSize = usePageSize('activities', 10);
|
||||
|
||||
const route = useRoute();
|
||||
@@ -181,7 +195,7 @@ const boardDeleteOpen = ref(false);
|
||||
|
||||
async function onTypesChanged() {
|
||||
await activityTypes.load(true);
|
||||
await useActivityPriorities().load(true);
|
||||
await activityPriorities.load(true);
|
||||
kanbanReload.value += 1;
|
||||
await refreshAll();
|
||||
}
|
||||
@@ -245,6 +259,13 @@ const statusFilter = computed<StatusFilter>(() => {
|
||||
: 'all';
|
||||
});
|
||||
|
||||
const priorityFilter = computed<PriorityFilter>(() => {
|
||||
const value = typeof route.query.priority === 'string' ? route.query.priority : 'all';
|
||||
return ACTIVITY_PRIORITY_KEYS.includes(value as ActivityPriority)
|
||||
? (value as ActivityPriority)
|
||||
: 'all';
|
||||
});
|
||||
|
||||
const assigneeFilter = computed(() =>
|
||||
typeof route.query.assigneeId === 'string' ? route.query.assigneeId : '',
|
||||
);
|
||||
@@ -323,6 +344,7 @@ const sharedParams = computed<Record<string, string | undefined>>(() => ({
|
||||
? legacyBaseType.value
|
||||
: undefined,
|
||||
status: statusFilter.value === 'all' ? undefined : statusFilter.value,
|
||||
priority: priorityFilter.value === 'all' ? undefined : priorityFilter.value,
|
||||
assigneeId: assigneeFilter.value || undefined,
|
||||
title: titleFilter.value.trim() || undefined,
|
||||
customer: customerFilter.value.trim() || undefined,
|
||||
@@ -335,6 +357,7 @@ function stageDotStyle(s: ActivityStageDto) {
|
||||
const activeFacetCount = computed(() => {
|
||||
let n = 0;
|
||||
if (typeFilter.value !== 'all') n += 1;
|
||||
if (priorityFilter.value !== 'all') n += 1;
|
||||
if (statusFilter.value !== 'all') n += 1;
|
||||
if (assigneeFilter.value) n += 1;
|
||||
if (titleFilter.value.trim()) n += 1;
|
||||
@@ -437,6 +460,7 @@ function deskDetailQuery(columnStageId?: string): Record<string, string> {
|
||||
if (search) query.q = search;
|
||||
if (typeFilter.value !== 'all') query.typeDefId = typeFilter.value;
|
||||
if (statusFilter.value !== 'all') query.status = statusFilter.value;
|
||||
if (priorityFilter.value !== 'all') query.priority = priorityFilter.value;
|
||||
if (assigneeFilter.value) query.assigneeId = assigneeFilter.value;
|
||||
if (titleFilter.value.trim()) query.title = titleFilter.value.trim();
|
||||
if (customerFilter.value.trim()) query.customer = customerFilter.value.trim();
|
||||
@@ -775,6 +799,13 @@ function setStatusFilter(next: string) {
|
||||
patchFacetQuery({ status: value === 'all' ? undefined : value });
|
||||
}
|
||||
|
||||
function setPriorityFilter(next: string) {
|
||||
const value: PriorityFilter = ACTIVITY_PRIORITY_KEYS.includes(next as ActivityPriority)
|
||||
? (next as ActivityPriority)
|
||||
: 'all';
|
||||
patchFacetQuery({ priority: value === 'all' ? undefined : value });
|
||||
}
|
||||
|
||||
function setAssigneeFilter(next: string) {
|
||||
patchFacetQuery({
|
||||
assigneeId: next || undefined,
|
||||
@@ -799,6 +830,7 @@ function clearFacets() {
|
||||
type: undefined,
|
||||
typeDefId: undefined,
|
||||
status: undefined,
|
||||
priority: undefined,
|
||||
assigneeId: undefined,
|
||||
title: undefined,
|
||||
customer: undefined,
|
||||
@@ -1164,6 +1196,7 @@ onMounted(() => {
|
||||
mobileQuery.addEventListener('change', syncMobile);
|
||||
void loadUsers();
|
||||
void activityTypes.load(true);
|
||||
void activityPriorities.load();
|
||||
void loadBoards()
|
||||
.then(() => loadStages())
|
||||
.then(() => {
|
||||
@@ -1461,16 +1494,19 @@ onUnmounted(() => {
|
||||
:customer="customerFilter"
|
||||
:due="dueFilter"
|
||||
:type-key="typeFilter"
|
||||
:priority-key="priorityFilter"
|
||||
:status-key="statusFilter"
|
||||
:assignee-id="assigneeFilter"
|
||||
:show-assignee="showDeskTabs"
|
||||
:users="users"
|
||||
:types="TYPE_FILTERS"
|
||||
:priorities="PRIORITY_FILTERS"
|
||||
:statuses="STATUS_FILTERS"
|
||||
@title="setTitleFilter"
|
||||
@customer="setCustomerFilter"
|
||||
@due="setDueFilter"
|
||||
@type="setTypeFilter"
|
||||
@priority="setPriorityFilter"
|
||||
@status="setStatusFilter"
|
||||
@assignee="setAssigneeFilter"
|
||||
@clear="clearFacets"
|
||||
@@ -1680,16 +1716,19 @@ onUnmounted(() => {
|
||||
:customer="customerFilter"
|
||||
:due="dueFilter"
|
||||
:type-key="typeFilter"
|
||||
:priority-key="priorityFilter"
|
||||
:status-key="statusFilter"
|
||||
:assignee-id="assigneeFilter"
|
||||
:show-assignee="showDeskTabs"
|
||||
:users="users"
|
||||
:types="TYPE_FILTERS"
|
||||
:priorities="PRIORITY_FILTERS"
|
||||
:statuses="STATUS_FILTERS"
|
||||
@title="setTitleFilter"
|
||||
@customer="setCustomerFilter"
|
||||
@due="setDueFilter"
|
||||
@type="setTypeFilter"
|
||||
@priority="setPriorityFilter"
|
||||
@status="setStatusFilter"
|
||||
@assignee="setAssigneeFilter"
|
||||
@done="onFacetDone"
|
||||
|
||||
@@ -52,8 +52,33 @@ import {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const ACTIVITY_LIST_QUERY = ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const;
|
||||
const NAV_API_QUERY = ['q', 'desk', 'type', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const;
|
||||
const ACTIVITY_LIST_QUERY = [
|
||||
'board',
|
||||
'view',
|
||||
'desk',
|
||||
'filter',
|
||||
'stage',
|
||||
'q',
|
||||
'typeDefId',
|
||||
'status',
|
||||
'priority',
|
||||
'assigneeId',
|
||||
'title',
|
||||
'customer',
|
||||
'due',
|
||||
] as const;
|
||||
const NAV_API_QUERY = [
|
||||
'q',
|
||||
'desk',
|
||||
'type',
|
||||
'typeDefId',
|
||||
'status',
|
||||
'priority',
|
||||
'assigneeId',
|
||||
'title',
|
||||
'customer',
|
||||
'due',
|
||||
] as const;
|
||||
|
||||
function activitiesListLocation() {
|
||||
const query: Record<string, string> = {};
|
||||
@@ -800,6 +825,7 @@ function moveToStage(stageId: string) {
|
||||
if (activity.value.stageId === stageId) return;
|
||||
const target = boardStages.value.find((stage) => stage.id === stageId);
|
||||
if (!target) return;
|
||||
const leftId = id.value;
|
||||
Modal.confirm({
|
||||
title: 'جابهجایی مرحله',
|
||||
content: `به ستون «${target.name}» منتقل میشود. انجام شود؟`,
|
||||
@@ -810,7 +836,7 @@ function moveToStage(stageId: string) {
|
||||
stageMoving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await api.patch<ActivityDto>(`/activities/${id.value}/stage`, { stageId });
|
||||
const { data } = await api.patch<ActivityDto>(`/activities/${leftId}/stage`, { stageId });
|
||||
if (data.status !== target.kind) {
|
||||
message.info(
|
||||
'سهم شما ثبت شد. فعالیت وقتی انجامشده میشود که همهٔ مسئولها سهم خود را بزنند.',
|
||||
@@ -818,8 +844,8 @@ function moveToStage(stageId: string) {
|
||||
await load();
|
||||
return;
|
||||
}
|
||||
activity.value = data;
|
||||
hydrateDraft(data);
|
||||
discardDrafts();
|
||||
await advanceAfterLeavingColumn(leftId);
|
||||
} catch {
|
||||
error.value = 'جابهجایی مرحله ناموفق بود.';
|
||||
throw new Error('stage-move-failed');
|
||||
@@ -877,20 +903,20 @@ async function loadUsers() {
|
||||
async function complete(kind: 'share' | 'force' | 'undo' = 'share') {
|
||||
if (!id.value) return;
|
||||
if (kind !== 'undo' && isDone.value) return;
|
||||
const leftId = id.value;
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const params =
|
||||
kind === 'force' ? { force: '1' } : kind === 'undo' ? { undo: '1' } : undefined;
|
||||
const { data } = await api.patch<ActivityDto>(
|
||||
`/activities/${id.value}/complete`,
|
||||
`/activities/${leftId}/complete`,
|
||||
undefined,
|
||||
{ params },
|
||||
);
|
||||
if (data.status === 'done') {
|
||||
// مثل بازگشت و پیشنمایش: فیلتر/نمای قبلی حفظ شود
|
||||
discardDrafts();
|
||||
leaveToActivitiesList();
|
||||
await advanceAfterLeavingColumn(leftId);
|
||||
return;
|
||||
}
|
||||
activity.value = data;
|
||||
@@ -928,11 +954,13 @@ async function reopen() {
|
||||
warnNoMutatePermission();
|
||||
return;
|
||||
}
|
||||
const leftId = id.value;
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.patch(`/activities/${id.value}`, { status: 'open' });
|
||||
await load();
|
||||
await api.patch(`/activities/${leftId}`, { status: 'open' });
|
||||
discardDrafts();
|
||||
await advanceAfterLeavingColumn(leftId);
|
||||
} catch {
|
||||
error.value = 'بازگشایی کار ناموفق بود.';
|
||||
} finally {
|
||||
@@ -1003,6 +1031,13 @@ const navIds = ref<string[]>([]);
|
||||
const deskBrowse = computed(
|
||||
() => route.query.from === 'desk' && typeof route.query.nav === 'string' && route.query.nav.length > 0,
|
||||
);
|
||||
const navColumnStageId = computed(() => {
|
||||
const fromQuery =
|
||||
typeof route.query.stage === 'string' && route.query.stage && route.query.stage !== 'all'
|
||||
? route.query.stage
|
||||
: '';
|
||||
return fromQuery || activity.value?.stageId || '';
|
||||
});
|
||||
const navIndex = computed(() => navIds.value.indexOf(id.value));
|
||||
const prevNavId = computed(() => (navIndex.value > 0 ? navIds.value[navIndex.value - 1]! : ''));
|
||||
const nextNavId = computed(() =>
|
||||
@@ -1012,13 +1047,13 @@ const nextNavId = computed(() =>
|
||||
);
|
||||
const showNav = computed(() => deskBrowse.value && navIndex.value >= 0);
|
||||
|
||||
function navStorageKey(token: string) {
|
||||
return `activity-nav:${token}`;
|
||||
function navStorageKey(token: string, stageId: string) {
|
||||
return `activity-nav:${token}:${stageId || 'none'}`;
|
||||
}
|
||||
|
||||
function readNavCache(token: string): string[] | null {
|
||||
function readNavCache(token: string, stageId: string): string[] | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(navStorageKey(token));
|
||||
const raw = sessionStorage.getItem(navStorageKey(token, stageId));
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as { ids?: unknown };
|
||||
if (!Array.isArray(parsed.ids) || parsed.ids.some((item) => typeof item !== 'string')) return null;
|
||||
@@ -1028,42 +1063,70 @@ function readNavCache(token: string): string[] | null {
|
||||
}
|
||||
}
|
||||
|
||||
function writeNavCache(token: string, stageId: string, ids: string[]) {
|
||||
try {
|
||||
sessionStorage.setItem(navStorageKey(token, stageId), JSON.stringify({ ids }));
|
||||
} catch {
|
||||
/* حالت خصوصی مرورگر */
|
||||
}
|
||||
}
|
||||
|
||||
/** بعد از ترک ستون: برو بعدیِ همان ستون، وگرنه قبلی؛ اگر خالی بود برو فهرست. */
|
||||
async function advanceAfterLeavingColumn(leftId: string) {
|
||||
commentOpen.value = false;
|
||||
postponeOpen.value = false;
|
||||
if (!deskBrowse.value) {
|
||||
leaveToActivitiesList();
|
||||
return;
|
||||
}
|
||||
const list = navIds.value;
|
||||
const idx = list.indexOf(leftId);
|
||||
const target =
|
||||
idx >= 0 && idx < list.length - 1
|
||||
? list[idx + 1]!
|
||||
: idx > 0
|
||||
? list[idx - 1]!
|
||||
: '';
|
||||
const remaining = list.filter((item) => item !== leftId);
|
||||
navIds.value = remaining;
|
||||
const token = typeof route.query.nav === 'string' ? route.query.nav : '';
|
||||
const stageId = navColumnStageId.value;
|
||||
if (token) writeNavCache(token, stageId, remaining);
|
||||
if (target) {
|
||||
await router.push({ name: 'activity-detail', params: { id: target }, query: route.query });
|
||||
return;
|
||||
}
|
||||
leaveToActivitiesList();
|
||||
}
|
||||
|
||||
let navSeq = 0;
|
||||
async function loadNav() {
|
||||
const seq = ++navSeq;
|
||||
const token = typeof route.query.nav === 'string' ? route.query.nav : '';
|
||||
const columnMissing =
|
||||
route.query.view === 'board' &&
|
||||
(typeof route.query.stage !== 'string' || !route.query.stage || route.query.stage === 'all');
|
||||
if (!deskBrowse.value || !token || columnMissing) {
|
||||
navIds.value = [];
|
||||
const stageId = navColumnStageId.value;
|
||||
if (!deskBrowse.value || !token || !stageId) {
|
||||
if (seq === navSeq) navIds.value = [];
|
||||
return;
|
||||
}
|
||||
const cached = readNavCache(token);
|
||||
const cached = readNavCache(token, stageId);
|
||||
if (cached) {
|
||||
if (seq === navSeq) navIds.value = cached;
|
||||
return;
|
||||
}
|
||||
const params: Record<string, string> = {
|
||||
filter: typeof route.query.filter === 'string' && route.query.filter ? route.query.filter : 'today',
|
||||
stageId,
|
||||
};
|
||||
for (const key of NAV_API_QUERY) {
|
||||
const value = route.query[key];
|
||||
if (typeof value === 'string' && value) params[key] = value;
|
||||
}
|
||||
if (typeof route.query.board === 'string' && route.query.board) params.boardId = route.query.board;
|
||||
if (typeof route.query.stage === 'string' && route.query.stage && route.query.stage !== 'all') {
|
||||
params.stageId = route.query.stage;
|
||||
}
|
||||
try {
|
||||
const { data } = await api.get<{ ids: string[] }>('/activities/nav-ids', { params });
|
||||
if (seq !== navSeq) return;
|
||||
const ids = Array.isArray(data.ids) ? data.ids : [];
|
||||
try {
|
||||
sessionStorage.setItem(navStorageKey(token), JSON.stringify({ ids }));
|
||||
} catch {
|
||||
/* حالت خصوصی مرورگر */
|
||||
}
|
||||
writeNavCache(token, stageId, ids);
|
||||
navIds.value = ids;
|
||||
} catch {
|
||||
if (seq === navSeq) navIds.value = [];
|
||||
@@ -1083,7 +1146,11 @@ watch(id, () => {
|
||||
postponeOpen.value = false;
|
||||
void load();
|
||||
});
|
||||
watch(() => route.query.nav, () => void loadNav(), { immediate: true });
|
||||
watch(
|
||||
() => [route.query.nav, navColumnStageId.value] as const,
|
||||
() => void loadNav(),
|
||||
{ immediate: true },
|
||||
);
|
||||
/** بدون next() — در Vue Router 4 گارد async+next باعث رد شدن بدون پرسش میشد */
|
||||
onBeforeRouteLeave(async () => resolveDirty());
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user