feat: routines module; super admin subscription badge, 7-day trial and lifetime plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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',
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user