import { Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts" import type { ChartReportResponse, ChartReportSeries, CurrencyTotal, ReportChartBucket } from "../../api/reports" import { useTranslation } from "../../hooks/useTranslation" const PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹" const SERIES_PALETTE = ["#0ea5e9", "#f97316", "#10b981", "#8b5cf6", "#ef4444", "#eab308", "#14b8a6", "#3b82f6"] type ChartRow = { bucket_key: string bucket_label: string tooltip_label: string [key: string]: string | number } const localizeDigits = (value: string, lang: "en" | "fa") => lang === "fa" ? value.replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)] || digit) : value const shouldTrimCurrencyDecimals = (currency?: string | null) => { const normalized = (currency || "").toUpperCase() return normalized === "IRR" || normalized === "IRT" } const formatAmount = (value: string, lang: "en" | "fa", currency?: string | null) => { const numeric = Number(value.replace(/,/g, "")) if (Number.isNaN(numeric)) { return localizeDigits(value, lang) } const [integerPart, fractionalPart] = value.replace(/,/g, "").split(".") const grouped = Math.abs(Number(integerPart)).toLocaleString("en-US") const signed = value.startsWith("-") ? `-${grouped}` : grouped const normalizedFraction = fractionalPart && !shouldTrimCurrencyDecimals(currency) ? fractionalPart.replace(/0+$/, "") : "" const formatted = normalizedFraction ? `${signed}.${normalizedFraction}` : signed return localizeDigits(formatted, lang) } const currencyLabel = (currency: string, lang: "en" | "fa") => { if (lang !== "fa") { return currency.toUpperCase() } return ( { USD: "دلار", EUR: "یورو", GBP: "پوند", IRR: "ریال", IRT: "تومان", AED: "درهم", TRY: "لیر", }[currency.toUpperCase()] || currency.toUpperCase() ) } const formatMoneyTotals = (totals: CurrencyTotal[], lang: "en" | "fa") => { if (!totals.length) { return "-" } return totals.map((item) => `${formatAmount(item.amount, lang, item.currency)} ${currencyLabel(item.currency, lang)}`).join(" | ") } const formatSecondsTick = (value: number, lang: "en" | "fa") => { const hours = value / 3600 const rounded = hours >= 10 ? hours.toFixed(0) : hours.toFixed(1) return localizeDigits(rounded, lang) } const parseIsoDate = (value: string) => { const [year, month, day] = value.split("-").map(Number) return new Date(year, (month || 1) - 1, day || 1) } const formatIsoDate = (value: Date) => { const year = value.getFullYear() const month = String(value.getMonth() + 1).padStart(2, "0") const day = String(value.getDate()).padStart(2, "0") return `${year}-${month}-${day}` } const monthKeyFromDate = (value: Date) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}` const getCalendarLocale = (lang: "en" | "fa") => (lang === "fa" ? "fa-IR-u-ca-persian" : "en-US") const getDailyAxisLabel = (date: Date, lang: "en" | "fa", period: string) => { if (period === "this_week") { return new Intl.DateTimeFormat(getCalendarLocale(lang), { weekday: "short" }).format(date) } return new Intl.DateTimeFormat(getCalendarLocale(lang), { day: "numeric" }).format(date) } const getDailyTooltipLabel = (date: Date, lang: "en" | "fa") => new Intl.DateTimeFormat(getCalendarLocale(lang), { weekday: "long", month: "long", day: "numeric", }).format(date) const getMonthlyAxisLabel = (bucketKey: string, lang: "en" | "fa") => { const [year, month] = bucketKey.split("-").map(Number) return new Intl.DateTimeFormat(getCalendarLocale(lang), { month: "short" }).format( new Date(year, (month || 1) - 1, 1), ) } const getMonthlyTooltipLabel = (bucketKey: string, lang: "en" | "fa") => { const [year, month] = bucketKey.split("-").map(Number) return new Intl.DateTimeFormat(getCalendarLocale(lang), { month: "long", year: "numeric" }).format( new Date(year, (month || 1) - 1, 1), ) } const buildDailyBuckets = ( fromDate: string, toDate: string, existing: ReportChartBucket[], lang: "en" | "fa", period: string, ) => { const map = new Map(existing.map((bucket) => [bucket.bucket_key, bucket])) const result: ReportChartBucket[] = [] const cursor = parseIsoDate(fromDate) const limit = parseIsoDate(toDate) while (cursor.getTime() <= limit.getTime()) { const key = formatIsoDate(cursor) const existingBucket = map.get(key) result.push( existingBucket ?? { bucket_key: key, bucket_label: getDailyAxisLabel(cursor, lang, period), total_seconds: 0, total_duration: "00:00:00", }, ) cursor.setDate(cursor.getDate() + 1) } return result.map((bucket) => ({ ...bucket, bucket_label: getDailyAxisLabel(parseIsoDate(bucket.bucket_key), lang, period), })) } const buildMonthlyBuckets = (fromDate: string, toDate: string, existing: ReportChartBucket[], lang: "en" | "fa") => { const map = new Map(existing.map((bucket) => [bucket.bucket_key, bucket])) const result: ReportChartBucket[] = [] const start = parseIsoDate(fromDate) const end = parseIsoDate(toDate) const cursor = new Date(start.getFullYear(), start.getMonth(), 1) const limit = new Date(end.getFullYear(), end.getMonth(), 1) while (cursor.getTime() <= limit.getTime()) { const key = monthKeyFromDate(cursor) const existingBucket = map.get(key) result.push( existingBucket ?? { bucket_key: key, bucket_label: getMonthlyAxisLabel(key, lang), total_seconds: 0, total_duration: "00:00:00", }, ) cursor.setMonth(cursor.getMonth() + 1) } return result.map((bucket) => ({ ...bucket, bucket_label: getMonthlyAxisLabel(bucket.bucket_key, lang), })) } const buildSeriesBuckets = ( series: ChartReportSeries, data: ChartReportResponse, lang: "en" | "fa", useMonthlyBuckets: boolean, ) => useMonthlyBuckets ? buildMonthlyBuckets(data.scope.from_date, data.scope.to_date, series.buckets, lang) : buildDailyBuckets(data.scope.from_date, data.scope.to_date, series.buckets, lang, data.scope.period) const createSeriesKey = (series: ChartReportSeries, index: number) => series.user?.id ?? `series_${index}` const buildChartRows = (data: ChartReportResponse, lang: "en" | "fa") => { const useMonthlyBuckets = data.scope.period === "this_year" || data.scope.period === "half_year_first" || data.scope.period === "half_year_second" const normalizedSeries = data.series.map((series) => buildSeriesBuckets(series, data, lang, useMonthlyBuckets)) const baseBuckets = normalizedSeries[0] ?? [] const rows: ChartRow[] = baseBuckets.map((bucket, bucketIndex) => { const tooltipLabel = useMonthlyBuckets ? getMonthlyTooltipLabel(bucket.bucket_key, lang) : getDailyTooltipLabel(parseIsoDate(bucket.bucket_key), lang) const row: ChartRow = { bucket_key: bucket.bucket_key, bucket_label: bucket.bucket_label, tooltip_label: tooltipLabel, } data.series.forEach((series, seriesIndex) => { const seriesKey = createSeriesKey(series, seriesIndex) row[seriesKey] = normalizedSeries[seriesIndex]?.[bucketIndex]?.total_seconds ?? 0 }) return row }) return { rows, useMonthlyBuckets } } function ChartTooltip({ active, payload, label, lang, totalHoursLabel, }: { active?: boolean payload?: ReadonlyArray<{ color?: string; dataKey?: string | number | ((obj: unknown) => unknown); name?: string | number; value?: unknown }> label: string lang: "en" | "fa" totalHoursLabel: string }) { if (!active || !payload?.length) { return null } return (