feat: routines module; super admin subscription badge, 7-day trial and lifetime plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user