249 lines
8.0 KiB
TypeScript
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',
|
|
},
|
|
})),
|
|
},
|
|
],
|
|
};
|
|
}
|