Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
177 lines
6.3 KiB
Vue
177 lines
6.3 KiB
Vue
<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>
|