Files
CRM_Montzar/apps/web/src/views/routines/RoutinesListView.vue
T
Mahdi 26792f9a37 feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
2026-11-03 16:51:51 +03:30

177 lines
6.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, inject, onMounted, ref, watch } from 'vue';
import type { RoutineDto } from '@mento/shared';
import { api } from '@/lib/api';
import { toFaDigits } from '@/lib/jalali';
import {
PRIORITY_LABELS,
PRIORITY_VARIANTS,
ROUTINES_CTX,
apiError,
formatDayNumeric,
formatPercent,
percentTone,
} from '@/lib/routines';
import {
Alert as AAlert,
Avatar as AAvatar,
Card as ACard,
Col as ACol,
Divider as ADivider,
Empty as AEmpty,
Flex as AFlex,
Input as AInput,
Progress as AProgress,
Row as ARow,
Space as ASpace,
Spin as ASpin,
Tag as ATag,
Typography,
theme,
} from 'ant-design-vue';
import { SearchOutlined, SyncOutlined } from '@ant-design/icons-vue';
const { Title: ATypographyTitle, Text: ATypographyText, Paragraph: ATypographyParagraph } = Typography;
const { token } = theme.useToken();
const shell = inject(ROUTINES_CTX)!;
const items = ref<RoutineDto[]>([]);
const loading = ref(true);
const error = ref('');
const q = ref('');
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<RoutineDto[]>('/routines', { params: { scope: 'mine' } });
items.value = data;
} catch (e) {
error.value = apiError(e, 'بارگذاری روتین‌ها ممکن نشد.');
} finally {
loading.value = false;
}
}
onMounted(load);
watch(shell.refreshTick, load);
const filtered = computed(() => {
const term = q.value.trim();
if (!term) return items.value;
return items.value.filter((r) => r.title.includes(term) || (r.description ?? '').includes(term));
});
const PRIORITY_TAG_COLORS: Record<string, string> = {
neutral: 'default',
info: 'processing',
warning: 'warning',
danger: 'error',
success: 'success',
accent: 'cyan',
};
function priorityColor(priority: RoutineDto['priority']) {
return PRIORITY_TAG_COLORS[PRIORITY_VARIANTS[priority]] ?? 'default';
}
const TONE_COLORS: Record<string, string> = {
good: 'var(--ant-color-success)',
mid: 'var(--ant-color-warning)',
low: 'var(--ant-color-error)',
none: 'var(--ant-color-text-quaternary)',
};
</script>
<template>
<a-card :bordered="false">
<a-flex vertical :gap="16">
<a-row :gutter="[16, 12]" align="middle">
<a-col :xs="24" :md="16">
<a-typography-text type="secondary">
روتین‌هایی که به شما یا تیم شما سپرده شده‌اند. کارهای هر روز از روی این‌ها ساخته می‌شوند.
</a-typography-text>
</a-col>
<a-col :xs="24" :md="8">
<a-input v-model:value="q" allow-clear placeholder="جستجوی روتین…">
<template #prefix><SearchOutlined /></template>
</a-input>
</a-col>
</a-row>
<a-alert v-if="error" type="error" show-icon :message="error" />
<a-spin :spinning="loading">
<a-empty
v-if="!loading && !filtered.length"
:description="
q
? 'روتینی با این عنوان پیدا نشد'
: 'هنوز روتینی برای شما تعریف نشده؛ روتین‌ها توسط مدیر تیم تعریف می‌شوند و هر روز به‌صورت خودکار در «کارهای امروز» ظاهر می‌شوند.'
"
/>
<a-row v-else :gutter="[16, 16]">
<a-col v-for="r in filtered" :key="r.id" :xs="24" :sm="12" :xl="8">
<a-card :bordered="false" :style="{ height: '100%', background: token.colorFillQuaternary }">
<a-flex vertical :gap="12" :style="{ height: '100%' }">
<a-flex align="center" :gap="12">
<a-avatar
shape="square"
:style="{
background: r.category?.color ?? token.colorPrimary,
color: token.colorWhite,
flexShrink: 0,
}"
>
<template #icon><SyncOutlined /></template>
</a-avatar>
<a-flex vertical :gap="0" :style="{ minWidth: 0 }">
<a-typography-title :level="5" :style="{ margin: 0 }">
{{ r.title }}
</a-typography-title>
<a-typography-text type="secondary">
{{ r.recurrenceLabel }} · ساعت {{ toFaDigits(r.timeOfDay) }}
</a-typography-text>
</a-flex>
</a-flex>
<a-typography-paragraph v-if="r.description" type="secondary" :style="{ margin: 0 }">
{{ r.description }}
</a-typography-paragraph>
<a-space :size="6" wrap>
<a-tag v-if="r.category" :bordered="false">{{ r.category.name }}</a-tag>
<a-tag :color="priorityColor(r.priority)" :bordered="false">
{{ PRIORITY_LABELS[r.priority] }}
</a-tag>
<a-tag v-if="r.isMandatory" color="warning" :bordered="false">اجباری</a-tag>
<a-tag v-if="r.reportRequired" color="processing" :bordered="false">گزارش الزامی</a-tag>
<a-tag v-if="r.team" color="cyan" :bordered="false">تیم {{ r.team.name }}</a-tag>
</a-space>
<a-flex vertical :gap="4" :style="{ marginTop: 'auto' }">
<a-divider :style="{ margin: 0 }" />
<a-flex justify="space-between" align="center" :gap="8">
<a-typography-text type="secondary">اجرا در ۳۰ روز اخیر</a-typography-text>
<a-typography-text strong>{{ formatPercent(r.executionRate) }}</a-typography-text>
</a-flex>
<a-progress
:percent="Math.max(0, Math.min(100, Math.round(r.executionRate ?? 0)))"
:show-info="false"
:stroke-color="TONE_COLORS[percentTone(r.executionRate)]"
/>
<a-typography-text type="secondary">
از {{ formatDayNumeric(r.startDate) }}{{ r.endDate ? ` تا ${formatDayNumeric(r.endDate)}` : '' }}
</a-typography-text>
</a-flex>
</a-flex>
</a-card>
</a-col>
</a-row>
</a-spin>
</a-flex>
</a-card>
</template>