import type { RoutineCalendarDayDto, RoutineCategoryStatDto, RoutineMemberStatDto, RoutineStatusBreakdownDto, RoutineTrendPointDto, } from '@mento/shared'; import type { UiChartOption } from '@/components/ui'; import { toFaDigits } from '@/lib/jalali'; import { formatDayNumeric } from '@/lib/routines'; const FONT = 'IRANSans, Tahoma, sans-serif'; const AXIS_LABEL = { color: '#64748b', fontFamily: FONT, fontSize: 11 }; const SPLIT = { lineStyle: { color: '#f1f5f9', type: 'dashed' as const } }; const LEGEND = { bottom: 0, left: 'center', textStyle: { fontFamily: FONT, fontSize: 11, color: '#475569' } }; export const STATUS_COLORS = { completed: '#10b981', notDone: '#94a3b8', overdue: '#ef4444', skipped: '#f59e0b', cancelled: '#cbd5e1', }; /* eslint-disable @typescript-eslint/no-explicit-any */ /** روند درصد انجام روزانه */ export function trendOption(trend: RoutineTrendPointDto[]): UiChartOption { return { textStyle: { fontFamily: FONT }, tooltip: { trigger: 'axis', formatter: (params: any) => { const item = Array.isArray(params) ? params[0] : params; const p = trend[item?.dataIndex ?? -1]; if (!p) return ''; return `${formatDayNumeric(p.date)}
درصد انجام: ${p.percent == null ? '—' : `${toFaDigits(p.percent)}٪`}
${toFaDigits(p.completed)} از ${toFaDigits(p.total)}`; }, }, grid: { left: 8, right: 16, top: 16, bottom: 8, containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: trend.map((p) => formatDayNumeric(p.date)), axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { ...AXIS_LABEL, hideOverlap: true }, }, yAxis: { type: 'value', min: 0, max: 100, splitLine: SPLIT, axisLabel: { ...AXIS_LABEL, formatter: (v: number) => `${toFaDigits(v)}٪` }, }, series: [ { name: 'درصد انجام', type: 'line', smooth: true, connectNulls: true, showSymbol: false, itemStyle: { color: '#14b8a6' }, lineStyle: { width: 3, color: '#0d9488' }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(20, 184, 166, 0.3)' }, { offset: 1, color: 'rgba(20, 184, 166, 0.01)' }, ], }, }, data: trend.map((p) => p.percent), }, ], }; } /** دونات وضعیت کارها */ export function statusDonut(s: RoutineStatusBreakdownDto): UiChartOption { const rows = [ { name: 'انجام شده', value: s.completed, color: STATUS_COLORS.completed }, { name: 'انجام نشده', value: s.notDone, color: STATUS_COLORS.notDone }, { name: 'عقب‌افتاده', value: s.overdue, color: STATUS_COLORS.overdue }, { name: 'رد شده', value: s.skipped, color: STATUS_COLORS.skipped }, ].filter((r) => r.value > 0); return { textStyle: { fontFamily: FONT }, tooltip: { trigger: 'item', formatter: (p: any) => `${p.name}: ${toFaDigits(p.value)} (${toFaDigits(p.percent)}٪)`, }, legend: LEGEND, series: [ { name: 'وضعیت', type: 'pie', radius: ['48%', '72%'], center: ['50%', '44%'], itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 }, label: { show: false }, data: rows.map((r) => ({ name: r.name, value: r.value, itemStyle: { color: r.color } })), }, ], }; } /** میله‌ای انجام به تفکیک دسته */ export function categoryBar(rows: RoutineCategoryStatDto[]): UiChartOption { return { textStyle: { fontFamily: FONT }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params: any) => { const item = Array.isArray(params) ? params[0] : params; const r = rows[item?.dataIndex ?? -1]; if (!r) return ''; return `${r.name}
${toFaDigits(r.completed)} از ${toFaDigits(r.total)} · ${r.percent == null ? '—' : `${toFaDigits(r.percent)}٪`}`; }, }, grid: { left: 8, right: 16, top: 16, bottom: 8, containLabel: true }, xAxis: { type: 'category', data: rows.map((r) => r.name), axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { ...AXIS_LABEL, interval: 0 }, }, yAxis: { type: 'value', min: 0, max: 100, splitLine: SPLIT, axisLabel: { ...AXIS_LABEL, formatter: (v: number) => `${toFaDigits(v)}٪` }, }, series: [ { name: 'درصد انجام', type: 'bar', barMaxWidth: 36, data: rows.map((r) => ({ value: r.percent ?? 0, itemStyle: { color: r.color, borderRadius: [6, 6, 0, 0] }, })), }, ], }; } /** میله‌ای انجام‌شده/کل به تفکیک روز */ export function daysBar(days: RoutineCalendarDayDto[], labels?: string[]): UiChartOption { return { textStyle: { fontFamily: FONT }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params: any) => { const list = Array.isArray(params) ? params : [params]; const d = days[list[0]?.dataIndex ?? -1]; if (!d) return ''; return `${formatDayNumeric(d.date)}
انجام شده: ${toFaDigits(d.completed)} از ${toFaDigits(d.total)}`; }, }, legend: LEGEND, grid: { left: 8, right: 16, top: 16, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: labels ?? days.map((d) => formatDayNumeric(d.date)), axisLine: { lineStyle: { color: '#cbd5e1' } }, axisLabel: { ...AXIS_LABEL, hideOverlap: true }, }, yAxis: { type: 'value', minInterval: 1, splitLine: SPLIT, axisLabel: { ...AXIS_LABEL, formatter: (v: number) => toFaDigits(v) }, }, series: [ { name: 'انجام شده', type: 'bar', stack: 'all', barMaxWidth: 28, itemStyle: { color: STATUS_COLORS.completed }, data: days.map((d) => d.completed), }, { name: 'باقی‌مانده', type: 'bar', stack: 'all', barMaxWidth: 28, itemStyle: { color: '#e2e8f0', borderRadius: [6, 6, 0, 0] }, data: days.map((d) => Math.max(0, d.total - d.completed)), }, ], }; } /** میله‌ای افقی مقایسه اعضا */ export function membersBar(members: RoutineMemberStatDto[]): UiChartOption { const rows = [...members].filter((m) => m.total > 0).sort((a, b) => (a.percent ?? 0) - (b.percent ?? 0)); return { textStyle: { fontFamily: FONT }, tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (params: any) => { const item = Array.isArray(params) ? params[0] : params; const m = rows[item?.dataIndex ?? -1]; if (!m) return ''; return `${m.user.fullName}
${toFaDigits(m.completed)} از ${toFaDigits(m.total)} · ${m.percent == null ? '—' : `${toFaDigits(m.percent)}٪`}`; }, }, grid: { left: 8, right: 24, top: 8, bottom: 8, containLabel: true }, xAxis: { type: 'value', min: 0, max: 100, splitLine: SPLIT, axisLabel: { ...AXIS_LABEL, formatter: (v: number) => `${toFaDigits(v)}٪` }, }, yAxis: { type: 'category', data: rows.map((m) => m.user.fullName), axisLabel: AXIS_LABEL, axisLine: { show: false }, axisTick: { show: false }, }, series: [ { name: 'درصد انجام', type: 'bar', barMaxWidth: 18, data: rows.map((m) => ({ value: m.percent ?? 0, itemStyle: { borderRadius: [0, 6, 6, 0], color: (m.percent ?? 0) >= 85 ? '#10b981' : (m.percent ?? 0) >= 60 ? '#f59e0b' : '#ef4444', }, })), }, ], }; }