232 lines
6.8 KiB
Vue
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>
|