Files
CRM_Montzar/apps/web/src/views/routines/RoutinesShellView.vue
T

232 lines
6.8 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, provide, ref, watch } from 'vue';
import { RouterLink, RouterView, useRoute, useRouter } from 'vue-router';
import type { RoutineContextDto } from '@mento/shared';
import UiIcon from '@/components/UiIcon.vue';
import RoutineTaskDrawer from '@/components/routines/RoutineTaskDrawer.vue';
import QuickTaskModal from '@/components/routines/QuickTaskModal.vue';
import { UiButton, UiPageHeader } from '@/components/ui';
import { api } from '@/lib/api';
import { toFaDigits } from '@/lib/jalali';
import { ROUTINES_CTX, apiError } from '@/lib/routines';
const route = useRoute();
const router = useRouter();
const context = ref<RoutineContextDto | null>(null);
const contextError = ref('');
const refreshTick = ref(0);
const overdueCount = ref(0);
const quickOpen = ref(false);
async function loadContext() {
try {
const { data } = await api.get<RoutineContextDto>('/routines/context');
context.value = data;
contextError.value = '';
} catch (e) {
contextError.value = apiError(e, 'بارگذاری اطلاعات کارهای روتین ممکن نشد.');
}
}
async function loadBadge() {
try {
const { data } = await api.get<{ remaining: number; overdue: number }>('/routine-tasks/badge');
overdueCount.value = data.overdue;
} catch {
overdueCount.value = 0;
}
}
function notifyChanged() {
refreshTick.value += 1;
void loadBadge();
}
function openTask(id: string) {
void router.replace({ query: { ...route.query, task: id } });
}
function closeTask() {
const query = { ...route.query };
delete query.task;
void router.replace({ query });
}
provide(ROUTINES_CTX, { context, refreshTick, notifyChanged, openTask });
onMounted(() => {
void loadContext();
void loadBadge();
});
const taskId = computed(() => (typeof route.query.task === 'string' ? route.query.task : null));
const isManager = computed(() => !!context.value?.isManager);
type Tab = { to: string; name: string; label: string; icon: InstanceType<typeof UiIcon>['$props']['name']; manager?: boolean };
const tabs: Tab[] = [
{ to: '/routines', name: 'routines-today', label: 'کارهای امروز', icon: 'calendar-sun' },
{ to: '/routines/overdue', name: 'routines-overdue', label: 'عقب‌افتاده', icon: 'warning' },
{ to: '/routines/calendar', name: 'routines-calendar', label: 'تقویم', icon: 'calendar' },
{ to: '/routines/list', name: 'routines-list', label: 'روتین‌ها', icon: 'refresh' },
{ to: '/routines/me', name: 'routines-me', label: 'عملکرد من', icon: 'chart' },
{ to: '/routines/team', name: 'routines-team', label: 'عملکرد تیم', icon: 'users', manager: true },
{ to: '/routines/manage', name: 'routines-manage', label: 'مدیریت روتین‌ها', icon: 'sliders', manager: true },
];
const visibleTabs = computed(() => tabs.filter((t) => !t.manager || isManager.value));
const managerOnlyRoutes = new Set(['routines-team', 'routines-manage', 'routine-new', 'routine-edit']);
watch(
[() => route.name, context],
([name, ctx]) => {
if (ctx && !ctx.isManager && managerOnlyRoutes.has(String(name))) {
void router.replace('/routines');
}
},
{ immediate: true },
);
function tabActive(tab: Tab) {
const name = String(route.name ?? '');
if (tab.name === 'routines-manage') return name === 'routines-manage' || name === 'routine-new' || name === 'routine-edit';
return name === tab.name;
}
const hideHeader = computed(() => route.name === 'routine-new' || route.name === 'routine-edit');
</script>
<template>
<section class="crm-page routines-shell">
<template v-if="!hideHeader">
<UiPageHeader title="کارهای روتین" lead="برنامه روزانه، روتین‌های تکرارشونده و پیگیری انجام کارها">
<template #actions>
<RouterLink to="/routines/settings" class="icon-link" title="تنظیمات یادآوری و اعلان‌ها">
<UiIcon name="bell" :size="17" />
</RouterLink>
<UiButton v-if="isManager" variant="outline" @click="router.push('/routines/manage/new')">
<template #icon><UiIcon name="refresh" :size="16" /></template>
روتین جدید
</UiButton>
<UiButton @click="quickOpen = true">
<template #icon><UiIcon name="plus" :size="16" /></template>
کار جدید
</UiButton>
</template>
</UiPageHeader>
<nav class="r-tabs" aria-label="بخش‌های کارهای روتین">
<RouterLink
v-for="tab in visibleTabs"
:key="tab.name"
:to="tab.to"
class="r-tab"
:class="{ on: tabActive(tab) }"
>
<UiIcon :name="tab.icon" :size="15" />
<span>{{ tab.label }}</span>
<em v-if="tab.name === 'routines-overdue' && overdueCount > 0" class="count">
{{ toFaDigits(overdueCount) }}
</em>
</RouterLink>
</nav>
</template>
<p v-if="contextError" class="error">{{ contextError }}</p>
<RouterView v-slot="{ Component }">
<component :is="Component" :key="String(route.name)" />
</RouterView>
<RoutineTaskDrawer :task-id="taskId" @close="closeTask" />
<QuickTaskModal :open="quickOpen" @close="quickOpen = false" />
</section>
</template>
<style scoped>
.routines-shell {
gap: var(--sp-4);
}
.icon-link {
width: var(--control-h);
height: var(--control-h);
display: inline-grid;
place-items: center;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
color: var(--ink-secondary);
flex: 0 0 auto !important;
}
.icon-link:hover,
.icon-link.router-link-active {
border-color: var(--accent);
color: var(--accent-deep);
background: var(--accent-subtle);
}
.r-tabs {
display: flex;
gap: var(--sp-5);
border-bottom: 1px solid var(--line);
overflow-x: auto;
scrollbar-width: none;
}
.r-tabs::-webkit-scrollbar {
display: none;
}
.r-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0.6rem 0.15rem;
margin-bottom: -1px;
border-bottom: 2px solid transparent;
color: var(--muted-light);
font-weight: var(--fw-bold);
font-size: var(--fs-sm);
white-space: nowrap;
transition: color var(--transition-fast), border-color var(--transition-fast);
}
.r-tab:hover {
color: var(--ink-secondary);
}
.r-tab.on {
color: var(--accent-hover);
border-bottom-color: var(--accent);
}
.count {
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: var(--danger);
color: #fff;
font-style: normal;
font-size: 0.68rem;
display: inline-grid;
place-items: center;
}
.error {
margin: 0;
}
@media (max-width: 900px) {
.r-tabs {
gap: var(--sp-4);
margin-inline: calc(-1 * var(--sp-4));
padding-inline: var(--sp-4);
}
}
</style>