Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
135 lines
4.1 KiB
Vue
135 lines
4.1 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import type { RoutineMemberStatDto } from '@mento/shared';
|
||
import UserAvatar from '@/components/UserAvatar.vue';
|
||
import { toFaDigits } from '@/lib/jalali';
|
||
import { formatMinutes, formatPercent, percentTone } from '@/lib/routines';
|
||
import {
|
||
Empty as AEmpty,
|
||
Flex as AFlex,
|
||
Progress as AProgress,
|
||
Space as ASpace,
|
||
Table as ATable,
|
||
Typography,
|
||
} from 'ant-design-vue';
|
||
import type { TableColumnsType } from 'ant-design-vue';
|
||
|
||
const { Text: ATypographyText } = Typography;
|
||
|
||
const props = defineProps<{ members: RoutineMemberStatDto[] }>();
|
||
const emit = defineEmits<{ select: [userId: string] }>();
|
||
|
||
type SortKey = 'name' | 'total' | 'completed' | 'overdue' | 'skipped' | 'percent' | 'streak' | 'avg';
|
||
|
||
function value(m: RoutineMemberStatDto, key: SortKey): number | string {
|
||
switch (key) {
|
||
case 'name':
|
||
return m.user.fullName;
|
||
case 'percent':
|
||
return m.percent ?? -1;
|
||
case 'avg':
|
||
return m.avgCompletionMinutes ?? Number.MAX_SAFE_INTEGER;
|
||
default:
|
||
return m[key];
|
||
}
|
||
}
|
||
|
||
function sorterFor(key: SortKey) {
|
||
return (a: RoutineMemberStatDto, b: RoutineMemberStatDto) => {
|
||
const va = value(a, key);
|
||
const vb = value(b, key);
|
||
if (typeof va === 'string' && typeof vb === 'string') return va.localeCompare(vb, 'fa');
|
||
return (va as number) - (vb as number);
|
||
};
|
||
}
|
||
|
||
const columns = computed<TableColumnsType<RoutineMemberStatDto>>(() => [
|
||
{ title: 'کاربر', key: 'name', sorter: sorterFor('name'), defaultSortOrder: undefined },
|
||
{
|
||
title: 'کل کارها',
|
||
key: 'total',
|
||
sorter: sorterFor('total'),
|
||
customRender: ({ record }) => toFaDigits(record.total),
|
||
},
|
||
{
|
||
title: 'انجام شده',
|
||
key: 'completed',
|
||
sorter: sorterFor('completed'),
|
||
customRender: ({ record }) => toFaDigits(record.completed),
|
||
},
|
||
{
|
||
title: 'عقبافتاده',
|
||
key: 'overdue',
|
||
sorter: sorterFor('overdue'),
|
||
customRender: ({ record }) => toFaDigits(record.overdue),
|
||
},
|
||
{
|
||
title: 'رد شده',
|
||
key: 'skipped',
|
||
sorter: sorterFor('skipped'),
|
||
customRender: ({ record }) => toFaDigits(record.skipped),
|
||
},
|
||
{ title: 'درصد انجام', key: 'percent', sorter: sorterFor('percent'), defaultSortOrder: 'descend' },
|
||
{
|
||
title: 'روز پیاپی کامل',
|
||
key: 'streak',
|
||
sorter: sorterFor('streak'),
|
||
customRender: ({ record }) => toFaDigits(record.streak),
|
||
},
|
||
{
|
||
title: 'میانگین زمان انجام',
|
||
key: 'avg',
|
||
sorter: sorterFor('avg'),
|
||
customRender: ({ record }) => formatMinutes(record.avgCompletionMinutes),
|
||
},
|
||
]);
|
||
|
||
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)',
|
||
};
|
||
|
||
function rowProps(record: RoutineMemberStatDto) {
|
||
return {
|
||
onClick: () => emit('select', record.user.id),
|
||
style: { cursor: 'pointer' },
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<a-table
|
||
:columns="columns"
|
||
:data-source="props.members"
|
||
:pagination="false"
|
||
:row-key="(r: RoutineMemberStatDto) => r.user.id"
|
||
:custom-row="rowProps"
|
||
:scroll="{ x: 'max-content' }"
|
||
>
|
||
<template #emptyText>
|
||
<a-empty description="دادهای برای این بازه نیست." />
|
||
</template>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'name'">
|
||
<a-space :size="8">
|
||
<UserAvatar :name="record.user.fullName" :user-id="record.user.id" :size="28" />
|
||
<a-typography-text strong>{{ record.user.fullName }}</a-typography-text>
|
||
</a-space>
|
||
</template>
|
||
<template v-else-if="column.key === 'percent'">
|
||
<a-flex align="center" :gap="8" :style="{ minWidth: '8rem' }">
|
||
<a-typography-text strong>{{ formatPercent(record.percent) }}</a-typography-text>
|
||
<a-progress
|
||
:percent="Math.max(0, Math.min(100, Math.round(record.percent ?? 0)))"
|
||
:show-info="false"
|
||
:stroke-color="TONE_COLORS[percentTone(record.percent)]"
|
||
:style="{ flex: 1 }"
|
||
/>
|
||
</a-flex>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</template>
|