Files
CRM_Montzar/apps/web/src/lib/routineCharts.ts
T

249 lines
8.0 KiB
TypeScript

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)}<br/>درصد انجام: <strong>${p.percent == null ? '—' : `${toFaDigits(p.percent)}٪`}</strong><br/>${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}: <strong>${toFaDigits(p.value)}</strong> (${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}<br/>${toFaDigits(r.completed)} از ${toFaDigits(r.total)} · <strong>${r.percent == null ? '—' : `${toFaDigits(r.percent)}٪`}</strong>`;
},
},
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)}<br/>انجام شده: <strong>${toFaDigits(d.completed)}</strong> از ${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}<br/>${toFaDigits(m.completed)} از ${toFaDigits(m.total)} · <strong>${m.percent == null ? '—' : `${toFaDigits(m.percent)}٪`}</strong>`;
},
},
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',
},
})),
},
],
};
}