Files
CRM_Montzar/apps/web/src/components/routines/TeamPerformanceTable.vue
T

135 lines
4.1 KiB
Vue
Raw Normal View History

<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>