diff --git a/src/components/reports/ReportsChartPanel.tsx b/src/components/reports/ReportsChartPanel.tsx index ab4195a..1f7f228 100644 --- a/src/components/reports/ReportsChartPanel.tsx +++ b/src/components/reports/ReportsChartPanel.tsx @@ -12,6 +12,26 @@ import { import type { ChartReportResponse, CurrencyTotal, ReportChartBucket } from "../../api/reports"; import { useTranslation } from "../../hooks/useTranslation"; +const FA_MONTHS = [ + "فروردین", + "اردیبهشت", + "خرداد", + "تیر", + "مرداد", + "شهریور", + "مهر", + "آبان", + "آذر", + "دی", + "بهمن", + "اسفند", +]; + +const normalizeDigits = (value: string) => + value + .replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit))) + .replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit))); + const toPersianDigits = (value: string) => value.replace(/\d/g, (digit) => "۰۱۲۳۴۵۶۷۸۹"[Number(digit)] || digit); @@ -42,26 +62,53 @@ const formatIsoDate = (value: Date) => { const monthKeyFromDate = (value: Date) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}`; -const labelFormatters = { - dayShort: (value: Date, lang: "en" | "fa") => - new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { - weekday: "short", - }).format(value), - dayLong: (value: Date, lang: "en" | "fa") => - new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { - weekday: "long", - month: "short", - day: "numeric", - }).format(value), - monthShort: (value: Date, lang: "en" | "fa") => - new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { - month: "short", - }).format(value), - monthLong: (value: Date, lang: "en" | "fa") => - new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { - month: "long", - year: "numeric", - }).format(value), +const getPersianDateParts = (value: Date) => { + const parts = new Intl.DateTimeFormat("fa-IR-u-ca-persian", { + year: "numeric", + month: "numeric", + day: "numeric", + }).formatToParts(value); + + return { + year: Number(normalizeDigits(parts.find((part) => part.type === "year")?.value || "")), + month: Number(normalizeDigits(parts.find((part) => part.type === "month")?.value || "")), + day: Number(normalizeDigits(parts.find((part) => part.type === "day")?.value || "")), + }; +}; + +const getDailyAxisLabel = (date: Date, lang: "en" | "fa") => { + if (lang === "fa") { + return toPersianDigits(String(getPersianDateParts(date).day)); + } + return String(date.getDate()); +}; + +const getDailyTooltipLabel = (date: Date, lang: "en" | "fa") => + new Intl.DateTimeFormat(lang === "fa" ? "fa-IR-u-ca-persian" : "en-US", { + weekday: "long", + month: "long", + day: "numeric", + }).format(date); + +const getMonthlyAxisLabel = (bucketKey: string, lang: "en" | "fa") => { + if (lang === "fa") { + const [, month] = bucketKey.split("-").map(Number); + return FA_MONTHS[(month || 1) - 1] || bucketKey; + } + const [year, month] = bucketKey.split("-").map(Number); + return new Intl.DateTimeFormat("en-US", { month: "short" }).format(new Date(year, (month || 1) - 1, 1)); +}; + +const getMonthlyTooltipLabel = (bucketKey: string, lang: "en" | "fa") => { + if (lang === "fa") { + const [year, month] = bucketKey.split("-").map(Number); + const monthName = FA_MONTHS[(month || 1) - 1] || bucketKey; + return `${monthName} ${toPersianDigits(String(year))}`; + } + const [year, month] = bucketKey.split("-").map(Number); + return new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format( + new Date(year, (month || 1) - 1, 1), + ); }; const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportChartBucket[], lang: "en" | "fa") => { @@ -76,7 +123,7 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha result.push( existingBucket ?? { bucket_key: key, - bucket_label: labelFormatters.dayShort(cursor, lang), + bucket_label: getDailyAxisLabel(cursor, lang), total_seconds: 0, total_duration: "00:00:00", }, @@ -86,12 +133,44 @@ const buildDailyBuckets = (fromDate: string, toDate: string, existing: ReportCha return result.map((bucket) => ({ ...bucket, - bucket_label: labelFormatters.dayShort(parseIsoDate(bucket.bucket_key), lang), + bucket_label: getDailyAxisLabel(parseIsoDate(bucket.bucket_key), lang), })); }; const buildMonthlyBuckets = (fromDate: string, toDate: string, existing: ReportChartBucket[], lang: "en" | "fa") => { const map = new Map(existing.map((bucket) => [bucket.bucket_key, bucket])); + + if (lang === "fa") { + const result: ReportChartBucket[] = []; + const start = getPersianDateParts(parseIsoDate(fromDate)); + const end = getPersianDateParts(parseIsoDate(toDate)); + + let year = start.year; + let month = start.month; + while (year < end.year || (year === end.year && month <= end.month)) { + const key = `${year}-${String(month).padStart(2, "0")}`; + const existingBucket = map.get(key); + result.push( + existingBucket ?? { + bucket_key: key, + bucket_label: getMonthlyAxisLabel(key, lang), + total_seconds: 0, + total_duration: "00:00:00", + }, + ); + month += 1; + if (month > 12) { + month = 1; + year += 1; + } + } + + return result.map((bucket) => ({ + ...bucket, + bucket_label: getMonthlyAxisLabel(bucket.bucket_key, lang), + })); + } + const result: ReportChartBucket[] = []; const start = parseIsoDate(fromDate); const end = parseIsoDate(toDate); @@ -104,7 +183,7 @@ const buildMonthlyBuckets = (fromDate: string, toDate: string, existing: ReportC result.push( existingBucket ?? { bucket_key: key, - bucket_label: labelFormatters.monthShort(cursor, lang), + bucket_label: getMonthlyAxisLabel(key, lang), total_seconds: 0, total_duration: "00:00:00", }, @@ -112,24 +191,19 @@ const buildMonthlyBuckets = (fromDate: string, toDate: string, existing: ReportC cursor.setMonth(cursor.getMonth() + 1); } - return result.map((bucket) => { - const [year, month] = bucket.bucket_key.split("-").map(Number); - const date = new Date(year, (month || 1) - 1, 1); - return { - ...bucket, - bucket_label: labelFormatters.monthShort(date, lang), - }; - }); + return result.map((bucket) => ({ + ...bucket, + bucket_label: getMonthlyAxisLabel(bucket.bucket_key, lang), + })); }; const formatTooltipLabel = (payload: ReportChartBucket | undefined, lang: "en" | "fa", period: string) => { if (!payload) return ""; const useMonth = period === "this_year" || period === "half_year_first" || period === "half_year_second"; if (useMonth) { - const [year, month] = payload.bucket_key.split("-").map(Number); - return labelFormatters.monthLong(new Date(year, (month || 1) - 1, 1), lang); + return getMonthlyTooltipLabel(payload.bucket_key, lang); } - return labelFormatters.dayLong(parseIsoDate(payload.bucket_key), lang); + return getDailyTooltipLabel(parseIsoDate(payload.bucket_key), lang); }; function ChartTooltip({ @@ -153,7 +227,7 @@ function ChartTooltip({ return (