import { Fragment } from "react"; import { ChevronDown, ChevronUp, FileSpreadsheet, FileText } from "lucide-react"; import type { BreakdownRow, DayDetailsResponse, TableReportResponse } from "../../api/reports"; import { useTranslation } from "../../hooks/useTranslation"; const toPersianDigits = (value: string) => value.replace(/\d/g, (digit) => "۰۱۲۳۴۵۶۷۸۹"[Number(digit)] || digit); const localizeDigits = (value: string, lang: "en" | "fa") => (lang === "fa" ? toPersianDigits(value) : value); const formatAmount = (value: string, lang: "en" | "fa") => { const trimmed = value.trim(); if (!trimmed) return trimmed; const numeric = Number(trimmed.replace(/,/g, "")); if (Number.isNaN(numeric)) return localizeDigits(trimmed, lang); const [integerPart, fractionalPart] = trimmed.replace(/,/g, "").split("."); const grouped = Math.abs(Number(integerPart)).toLocaleString("en-US"); const signed = trimmed.startsWith("-") ? `-${grouped}` : grouped; const normalized = fractionalPart ? `${signed}.${fractionalPart}` : signed; return localizeDigits(normalized, lang); }; const currencyLabel = (currency: string, lang: "en" | "fa") => { const normalized = currency.toUpperCase(); if (lang !== "fa") return normalized; return ( { USD: "دلار آمریکا", EUR: "یورو", GBP: "پوند", IRR: "ریال", IRT: "تومان", AED: "درهم", TRY: "لیر", }[normalized] || normalized ); }; const formatMoneyTotals = (totals: { currency: string; amount: string }[], lang: "en" | "fa") => { if (!totals.length) return "-"; return totals.map((item) => `${formatAmount(item.amount, lang)} ${currencyLabel(item.currency, lang)}`).join(" | "); }; const formatHourlyRate = ( rate: { currency: string; amount: string } | null, lang: "en" | "fa", ) => { if (!rate) return "-"; return `${formatAmount(rate.amount, lang)} ${currencyLabel(rate.currency, lang)}`; }; const formatDisplayDate = (value: string, lang: "en" | "fa") => { const parsed = new Date(`${value}T00:00:00`); return new Intl.DateTimeFormat(lang === "fa" ? "fa-IR" : "en-US", { dateStyle: "medium", }).format(parsed); }; const formatDisplayDateTime = (value: string, lang: "en" | "fa") => { const parsed = new Date(value); return new Intl.DateTimeFormat(lang === "fa" ? "fa-IR" : "en-US", { dateStyle: "medium", timeStyle: "short", }).format(parsed); }; function BreakdownCards({ title, rows, labels, lang, }: { title: string; rows: BreakdownRow[]; labels: Record; lang: "en" | "fa"; }) { return (
{title}
{rows.map((row) => (
{row.name}
{labels.billableHours}
{localizeDigits(row.billable_duration, lang)}
{labels.nonBillableHours}
{localizeDigits(row.non_billable_duration, lang)}
{labels.totalIncome}
{formatMoneyTotals(row.income_totals, lang)}
))}
{rows.map((row) => ( ))}
{labels.name} {labels.billableHours} {labels.nonBillableHours} {labels.totalIncome}
{row.name} {localizeDigits(row.billable_duration, lang)} {localizeDigits(row.non_billable_duration, lang)} {formatMoneyTotals(row.income_totals, lang)}
); } export function ReportsTablePanel({ data, dayDetails, openDay, onToggleDay, onExport, exportState, labels, }: { data: TableReportResponse | null; dayDetails: DayDetailsResponse | null; openDay: string | null; onToggleDay: (day: string) => void; onExport: (type: "excel" | "pdf") => void; exportState: { excel: { pending: boolean; cooldownSeconds: number }; pdf: { pending: boolean; cooldownSeconds: number }; }; labels: Record; }) { const { lang } = useTranslation(); if (!data) return null; const days = Array.isArray(data.days) ? data.days : []; const clients = Array.isArray(data.clients) ? data.clients : []; const projects = Array.isArray(data.projects) ? data.projects : []; const tags = Array.isArray(data.tags) ? data.tags : []; const entries = Array.isArray(dayDetails?.entries) ? dayDetails.entries : []; const summary = data.summary ?? { billable_duration: "00:00:00", non_billable_duration: "00:00:00", income_totals: [], }; return (
{labels.details}
{days.map((day) => { const isOpen = openDay === day.date; return (
{formatDisplayDate(day.date, lang)}
{labels.totalHours}: {localizeDigits(day.total_duration, lang)}
{labels.billableHours}
{localizeDigits(day.billable_duration, lang)}
{labels.nonBillableHours}
{localizeDigits(day.non_billable_duration, lang)}
{labels.hourlyRate}
{formatHourlyRate(day.latest_hourly_rate, lang)}
{labels.totalIncome}
{formatMoneyTotals(day.income_totals, lang)}
{isOpen && dayDetails?.day === day.date ? (
{entries.map((entry) => (
{entry.description || labels.noDescription}
{entry.project?.name || "-"} • {localizeDigits(entry.duration, lang)}
{formatDisplayDateTime(entry.start_time, lang)}
))}
) : null}
); })}
{labels.total}
{labels.billableHours}: {localizeDigits(summary.billable_duration, lang)}
{labels.nonBillableHours}: {localizeDigits(summary.non_billable_duration, lang)}
{labels.totalIncome}: {formatMoneyTotals(summary.income_totals, lang)}
{days.map((day) => { const isOpen = openDay === day.date; return ( {isOpen && dayDetails?.day === day.date ? ( ) : null} ); })}
{labels.date} {labels.billableHours} {labels.nonBillableHours} {labels.hourlyRate} {labels.totalIncome} {labels.details}
{formatDisplayDate(day.date, lang)} {localizeDigits(day.billable_duration, lang)} {localizeDigits(day.non_billable_duration, lang)} {formatHourlyRate(day.latest_hourly_rate, lang)} {formatMoneyTotals(day.income_totals, lang)}
{entries.map((entry) => (
{entry.description || labels.noDescription} {entry.project ? {entry.project.name} : null} {localizeDigits(entry.duration, lang)} {formatDisplayDateTime(entry.start_time, lang)}
))}
{labels.total} {localizeDigits(summary.billable_duration, lang)} {localizeDigits(summary.non_billable_duration, lang)} - {formatMoneyTotals(summary.income_totals, lang)}
); }