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:
2026-09-28 12:28:14 +03:30
parent 8effd4cbc6
commit 32e27d45f7
13 changed files with 303 additions and 50 deletions
+97 -5
View File
@@ -11,11 +11,15 @@ import UiIcon from '@/components/UiIcon.vue';
import UserAvatar from '@/components/UserAvatar.vue';
import ActivityCommentSheet from '@/components/activities/ActivityCommentSheet.vue';
import ActivityPostponeSheet from '@/components/activities/ActivityPostponeSheet.vue';
import type { ActivityDto, ActivityPriority, ActivityType } from '@mento/shared';
import type { ActivityDto, ActivityPriority, ActivityType, OrgUserDto } from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
import { canReassignRecords } from '@/lib/roles';
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const id = computed(() => (typeof route.params.id === 'string' ? route.params.id : ''));
const canReassign = computed(() => canReassignRecords(auth.user?.role));
const loading = ref(true);
const saving = ref(false);
@@ -25,6 +29,8 @@ const moreOpen = ref(false);
const commentOpen = ref(false);
const postponeOpen = ref(false);
const noteOpen = ref(true);
const users = ref<OrgUserDto[]>([]);
const assignUserId = ref('');
const typeLabel: Record<ActivityType, string> = {
call: 'تماس',
@@ -89,6 +95,7 @@ async function load() {
try {
const { data } = await api.get<ActivityDto>(`/activities/${id.value}`);
activity.value = data;
assignUserId.value = data.assignee?.id ?? '';
} catch {
error.value = 'بارگذاری جزئیات فعالیت ناموفق بود.';
activity.value = null;
@@ -97,6 +104,16 @@ async function load() {
}
}
async function loadUsers() {
if (!canReassign.value) return;
try {
const { data } = await api.get<OrgUserDto[]>('/users');
users.value = data;
} catch {
users.value = [];
}
}
async function complete() {
if (!id.value || isDone.value) return;
saving.value = true;
@@ -111,6 +128,36 @@ async function complete() {
}
}
async function reopen() {
if (!id.value || !activity.value || activity.value.status === 'open') return;
saving.value = true;
error.value = '';
try {
await api.patch(`/activities/${id.value}`, { status: 'open' });
await load();
} catch {
error.value = 'بازگشایی کار ناموفق بود.';
} finally {
saving.value = false;
}
}
async function reassign() {
if (!id.value || !canReassign.value) return;
saving.value = true;
error.value = '';
try {
await api.patch(`/activities/${id.value}`, {
assigneeId: assignUserId.value || null,
});
await load();
} catch {
error.value = 'ارجاع کار ناموفق بود.';
} finally {
saving.value = false;
}
}
async function postponeTo(iso: string) {
if (!id.value || !activity.value || isDone.value) return;
saving.value = true;
@@ -160,7 +207,10 @@ watch(id, () => {
postponeOpen.value = false;
void load();
});
onMounted(load);
onMounted(() => {
void load();
void loadUsers();
});
</script>
<template>
@@ -224,8 +274,23 @@ onMounted(load);
مسئول
</dt>
<dd class="person">
<UserAvatar v-if="activity.assignee" :name="activity.assignee.fullName" :size="28" />
{{ activity.assignee?.fullName || '—' }}
<template v-if="canReassign">
<select
v-model="assignUserId"
class="assign-select"
:disabled="saving"
@change="reassign"
>
<option value="">بدون مسئول</option>
<option v-for="u in users" :key="u.id" :value="u.id">
{{ u.fullName }}
</option>
</select>
</template>
<template v-else>
<UserAvatar v-if="activity.assignee" :name="activity.assignee.fullName" :size="28" />
{{ activity.assignee?.fullName || '—' }}
</template>
</dd>
</div>
<div>
@@ -295,7 +360,23 @@ onMounted(load);
</template>
<footer v-if="activity && !commentOpen && !postponeOpen" class="dock">
<button class="act primary" type="button" :disabled="saving || isDone" @click="complete">
<button
v-if="isDone || activity.status === 'canceled'"
class="act primary"
type="button"
:disabled="saving"
@click="reopen"
>
<UiIcon name="clock" :size="16" />
بازگشایی
</button>
<button
v-else
class="act primary"
type="button"
:disabled="saving || isDone"
@click="complete"
>
<UiIcon name="check" :size="16" />
انجام شد
</button>
@@ -508,6 +589,17 @@ dd {
gap: 0.45rem;
}
.assign-select {
min-height: 2.2rem;
min-width: 10rem;
max-width: 100%;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 0 0.55rem;
font: inherit;
background: #fff;
}
.flag.low,
.priority.low {
color: #64748b;