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

175 lines
5.3 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed } from 'vue';
import type { RoutineCalendarDayDto } from '@mento/shared';
import { ROUTINE_WEEKDAY_LABELS } from '@mento/shared';
import { dateToJalali, toFaDigits } from '@/lib/jalali';
import { DAY_STATE_LABELS, addDaysKey, keyToDate, todayKey, weekdayIndex } from '@/lib/routines';
import {
Button as AButton,
Flex as AFlex,
Typography,
theme,
} from 'ant-design-vue';
const { Text: ATypographyText } = Typography;
const { token } = theme.useToken();
const props = withDefaults(
defineProps<{
from: string;
to: string;
days: RoutineCalendarDayDto[];
selected?: string | null;
/** heat = رنگ بر اساس درصد (گزارش ماهانه) */
mode?: 'state' | 'heat';
compact?: boolean;
}>(),
{ selected: null, mode: 'state', compact: false },
);
const emit = defineEmits<{ select: [day: string] }>();
const byDate = computed(() => new Map(props.days.map((d) => [d.date, d])));
const today = todayKey();
const cells = computed(() => {
const out: Array<{ key: string; blank: boolean; day?: RoutineCalendarDayDto; jd: number; future: boolean }> = [];
const lead = weekdayIndex(props.from);
for (let i = 0; i < lead; i++) out.push({ key: `b${i}`, blank: true, jd: 0, future: false });
for (let k = props.from; k <= props.to; k = addDaysKey(k, 1)) {
out.push({
key: k,
blank: false,
day: byDate.value.get(k),
jd: dateToJalali(keyToDate(k)).jd,
future: k > today,
});
}
return out;
});
function heatLevel(d?: RoutineCalendarDayDto): number {
if (!d || !d.total || d.percent == null) return 0;
if (d.percent >= 90) return 4;
if (d.percent >= 70) return 3;
if (d.percent >= 40) return 2;
return 1;
}
const STATE_BG: Record<string, string> = {
complete: 'var(--ant-color-success-bg)',
partial: 'var(--ant-color-warning-bg)',
missed: 'var(--ant-color-error-bg)',
empty: 'var(--ant-color-bg-container)',
future: 'var(--ant-color-fill-quaternary)',
};
const STATE_DOT: Record<string, string> = {
complete: 'var(--ant-color-success)',
partial: 'var(--ant-color-warning)',
missed: 'var(--ant-color-error)',
empty: 'var(--ant-color-text-quaternary)',
future: 'var(--ant-color-text-quaternary)',
};
const HEAT_BG: Record<number, string> = {
0: 'var(--ant-color-fill-quaternary)',
1: 'var(--ant-color-info-bg)',
2: 'var(--ant-color-primary-bg)',
3: 'var(--ant-color-success-bg)',
4: 'var(--ant-color-success)',
};
function cellKey(c: (typeof cells.value)[number]) {
return c.future && !c.day?.total ? 'future' : (c.day?.state ?? 'empty');
}
function cellBackground(c: (typeof cells.value)[number]) {
if (props.mode === 'heat') return HEAT_BG[heatLevel(c.day)];
return STATE_BG[cellKey(c)] ?? STATE_BG.empty;
}
function title(c: (typeof cells.value)[number]) {
const d = c.day;
if (!d || !d.total) return 'بدون کار';
return `${DAY_STATE_LABELS[d.state]} — ${toFaDigits(d.completed)} از ${toFaDigits(d.total)} انجام شده`;
}
</script>
<template>
<a-flex vertical :gap="6">
<a-flex :gap="6">
<a-typography-text
v-for="w in ROUTINE_WEEKDAY_LABELS"
:key="w"
type="secondary"
strong
:style="{ flex: '1 1 0', minWidth: 0, textAlign: 'center' }"
>
{{ compact ? w.charAt(0) : w }}
</a-typography-text>
</a-flex>
<a-flex wrap="wrap" :gap="6">
<template v-for="c in cells" :key="c.key">
<a-flex
v-if="c.blank"
:style="{ flex: '0 0 calc((100% - 36px) / 7)', minWidth: 0 }"
/>
<a-button
v-else
:title="title(c)"
:style="{
flex: '0 0 calc((100% - 36px) / 7)',
minWidth: 0,
height: compact ? '44px' : '74px',
padding: '8px',
background: cellBackground(c),
borderColor:
selected === c.key
? token.colorPrimary
: c.key === today
? token.colorPrimaryBorder
: undefined,
boxShadow: selected === c.key ? `0 0 0 1px ${token.colorPrimary}` : undefined,
}"
@click="emit('select', c.key)"
>
<a-flex
vertical
align="start"
justify="space-between"
:style="{ width: '100%', height: '100%' }"
>
<a-typography-text
strong
:style="c.key === today ? { color: 'var(--ant-color-primary)' } : undefined"
>
{{ toFaDigits(c.jd) }}
</a-typography-text>
<a-flex
v-if="!compact && c.day?.total"
justify="space-between"
align="center"
:style="{ width: '100%' }"
>
<a-typography-text type="secondary">
{{ toFaDigits(c.day.completed) }}/{{ toFaDigits(c.day.total) }}
</a-typography-text>
<a-flex
v-if="mode === 'state'"
:style="{
width: '8px',
height: '8px',
borderRadius: token.borderRadiusLG,
background: STATE_DOT[cellKey(c)],
flexShrink: 0,
}"
/>
</a-flex>
</a-flex>
</a-button>
</template>
</a-flex>
</a-flex>
</template>