Add desk-style task workflow on Activities and Project boards.
Managers can switch my desk vs team, reassign activities, and filter project cards to their work; assignees can see and move cards.
This commit is contained in:
@@ -28,9 +28,12 @@ import type {
|
||||
PaginatedResponse,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canViewAllRecords } from '@/lib/roles';
|
||||
|
||||
type TypeFilter = 'all' | ActivityType;
|
||||
type UiStatus = 'done' | 'progress' | 'planned' | 'overdue' | 'canceled';
|
||||
type DeskScope = 'mine' | 'team';
|
||||
|
||||
const TABS: Array<{ key: ActivityFilter; label: string }> = [
|
||||
{ key: 'today', label: 'امروز' },
|
||||
@@ -40,6 +43,11 @@ const TABS: Array<{ key: ActivityFilter; label: string }> = [
|
||||
{ key: 'all', label: 'همه' },
|
||||
];
|
||||
|
||||
const DESK_TABS: Array<{ key: DeskScope; label: string }> = [
|
||||
{ key: 'mine', label: 'میز من' },
|
||||
{ key: 'team', label: 'همه تیم' },
|
||||
];
|
||||
|
||||
const TYPE_FILTERS: Array<{ key: TypeFilter; label: string }> = [
|
||||
{ key: 'all', label: 'همه انواع' },
|
||||
{ key: 'call', label: 'تماس' },
|
||||
@@ -52,6 +60,8 @@ const PAGE_SIZE = 6;
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const showDeskTabs = computed(() => canViewAllRecords(auth.user?.role));
|
||||
|
||||
const filter = computed<ActivityFilter>(() => {
|
||||
const value = typeof route.query.filter === 'string' ? route.query.filter : 'today';
|
||||
@@ -61,6 +71,11 @@ const filter = computed<ActivityFilter>(() => {
|
||||
return 'today';
|
||||
});
|
||||
|
||||
const desk = computed<DeskScope>(() => {
|
||||
if (!showDeskTabs.value) return 'mine';
|
||||
return route.query.desk === 'team' ? 'team' : 'mine';
|
||||
});
|
||||
|
||||
const page = computed(() => {
|
||||
const raw = Number(route.query.page);
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : 1;
|
||||
@@ -178,10 +193,11 @@ function subtitle(item: ActivityDto) {
|
||||
|
||||
async function loadCounts() {
|
||||
const keys: ActivityFilter[] = ['today', 'overdue', 'upcoming', 'done', 'all'];
|
||||
const deskParam = showDeskTabs.value ? desk.value : undefined;
|
||||
const results = await Promise.all(
|
||||
keys.map((key) =>
|
||||
api.get<PaginatedResponse<ActivityDto>>('/activities', {
|
||||
params: { filter: key, limit: 1 },
|
||||
params: { filter: key, limit: 1, desk: deskParam },
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -203,6 +219,7 @@ async function load() {
|
||||
q: q.value || undefined,
|
||||
page: page.value,
|
||||
limit: PAGE_SIZE,
|
||||
desk: showDeskTabs.value ? desk.value : undefined,
|
||||
},
|
||||
});
|
||||
data.value = res;
|
||||
@@ -217,6 +234,17 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function setDesk(next: DeskScope) {
|
||||
void router.replace({
|
||||
name: 'activities',
|
||||
query: {
|
||||
...route.query,
|
||||
desk: next === 'mine' ? undefined : next,
|
||||
page: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setFilter(next: ActivityFilter) {
|
||||
filterOpen.value = false;
|
||||
void router.replace({
|
||||
@@ -318,10 +346,11 @@ function onImportSettled() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.query.filter, route.query.q, route.query.page],
|
||||
() => [route.query.filter, route.query.q, route.query.page, route.query.desk],
|
||||
() => {
|
||||
q.value = typeof route.query.q === 'string' ? route.query.q : '';
|
||||
void load();
|
||||
void loadCounts();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -348,12 +377,12 @@ onUnmounted(() => {
|
||||
<nav class="crumbs" aria-label="مسیر">
|
||||
<RouterLink :to="{ name: 'home' }">خانه</RouterLink>
|
||||
<span>‹</span>
|
||||
<span>فعالیتها</span>
|
||||
<span>میز کار</span>
|
||||
</nav>
|
||||
|
||||
<UiPageHeader
|
||||
title="فعالیتها"
|
||||
lead="تماس، جلسه و وظیفه — امروز و عقبافتادهها یکجا."
|
||||
title="میز کار و فعالیتها"
|
||||
lead="کارهای امروز، ارجاع به همکار و پیگیری وضعیت."
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="ghost" type="button" :disabled="busy" @click.stop="exportExcel">
|
||||
@@ -387,6 +416,20 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
|
||||
<div v-if="showDeskTabs" class="desk-tabs" role="tablist" aria-label="محدوده میز کار">
|
||||
<button
|
||||
v-for="tab in DESK_TABS"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
role="tab"
|
||||
:class="{ on: desk === tab.key }"
|
||||
:aria-selected="desk === tab.key"
|
||||
@click="setDesk(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs" role="tablist" aria-label="فیلتر فعالیت">
|
||||
<button
|
||||
v-for="tab in TABS"
|
||||
@@ -763,6 +806,33 @@ h1 {
|
||||
width: min(100%, 320px);
|
||||
}
|
||||
|
||||
.desk-tabs {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 12px;
|
||||
background: #f1f5f9;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.desk-tabs button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font: inherit;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.desk-tabs button.on {
|
||||
background: #fff;
|
||||
color: #0f766e;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
|
||||
Reference in New Issue
Block a user