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.
This commit is contained in:
2026-11-03 16:51:51 +03:30
parent 631a3267d1
commit 26792f9a37
80 changed files with 19982 additions and 24020 deletions
@@ -4,6 +4,15 @@ 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<{
@@ -47,6 +56,39 @@ function heatLevel(d?: RoutineCalendarDayDto): number {
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 'بدون کار';
@@ -55,211 +97,78 @@ function title(c: (typeof cells.value)[number]) {
</script>
<template>
<div class="mgrid" :class="[mode, { compact }]">
<div v-for="w in ROUTINE_WEEKDAY_LABELS" :key="w" class="wd">{{ compact ? w.charAt(0) : w }}</div>
<template v-for="c in cells" :key="c.key">
<span v-if="c.blank" class="cell blank" />
<button
v-else
type="button"
class="cell"
:class="[
mode === 'state' ? `s-${c.future && !c.day?.total ? 'future' : (c.day?.state ?? 'empty')}` : `h-${heatLevel(c.day)}`,
{ sel: selected === c.key, today: c.key === today },
]"
:title="title(c)"
@click="emit('select', c.key)"
<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' }"
>
<span class="num">{{ toFaDigits(c.jd) }}</span>
<template v-if="!compact && c.day?.total">
<span class="frac">{{ toFaDigits(c.day.completed) }}/{{ toFaDigits(c.day.total) }}</span>
<i v-if="mode === 'state'" class="dot" />
</template>
</button>
</template>
</div>
{{ 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>
<style scoped>
.mgrid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 6px;
}
.wd {
text-align: center;
font-size: var(--fs-xs);
color: var(--muted);
font-weight: var(--fw-semibold);
padding-bottom: 2px;
}
.cell {
position: relative;
min-height: 74px;
padding: 8px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
gap: 4px;
cursor: pointer;
font: inherit;
text-align: right;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.cell.blank {
border: 0;
background: transparent;
cursor: default;
}
.cell:not(.blank):hover {
border-color: var(--line-strong);
}
.cell.sel {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.cell.today .num {
background: var(--accent);
color: #fff;
}
.num {
min-width: 24px;
height: 24px;
padding: 0 4px;
border-radius: 99px;
display: inline-grid;
place-items: center;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--ink);
}
.frac {
font-size: var(--fs-xs);
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.dot {
position: absolute;
top: 10px;
inset-inline-end: 10px;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--line-strong);
}
/* وضعیت روز */
.s-complete {
background: color-mix(in srgb, var(--success-soft) 45%, var(--surface));
}
.s-complete .dot {
background: var(--success);
}
.s-partial {
background: color-mix(in srgb, var(--warning-soft) 45%, var(--surface));
}
.s-partial .dot {
background: var(--warning);
}
.s-missed {
background: color-mix(in srgb, var(--danger-soft) 45%, var(--surface));
}
.s-missed .dot {
background: var(--danger);
}
.s-future {
background: var(--surface);
}
.s-empty {
background: var(--surface);
}
/* Heatmap */
.h-0 {
background: var(--surface-subtle);
}
.h-1 {
background: color-mix(in srgb, var(--accent) 14%, white);
}
.h-2 {
background: color-mix(in srgb, var(--accent) 32%, white);
}
.h-3 {
background: color-mix(in srgb, var(--accent) 55%, white);
}
.h-4 {
background: color-mix(in srgb, var(--accent) 80%, white);
}
.h-3 .num,
.h-4 .num,
.h-3 .frac,
.h-4 .frac {
color: #fff;
}
.compact {
gap: 4px;
}
.compact .cell {
min-height: 0;
aspect-ratio: 1;
padding: 2px;
align-items: center;
justify-content: center;
border-color: transparent;
}
.compact .num {
font-size: 0.7rem;
min-width: 0;
height: auto;
}
@media (max-width: 700px) {
.mgrid {
gap: 3px;
}
.cell {
min-height: 54px;
padding: 4px;
align-items: center;
}
.frac {
font-size: 0.62rem;
}
.dot {
position: static;
width: 7px;
height: 7px;
}
}
</style>