import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; import type { PivotConfig, PivotFieldId, PivotMeasureId, PivotZone } from "../../shared/types"; import { getDynamicFilterValues } from "../../services/reportService"; const ALL_FIELDS: PivotFieldId[] = ["year", "month", "type", "level1", "level2"]; const ALL_MEASURES: PivotMeasureId[] = ["periodic", "ytd"]; interface DynamicReportPanelProps { config: PivotConfig; onChange: (config: PivotConfig) => void; dateFrom?: string; dateTo?: string; } export default function DynamicReportPanel({ config, onChange, dateFrom, dateTo }: DynamicReportPanelProps) { const { t } = useTranslation(); const [menuTarget, setMenuTarget] = useState<{ id: string; type: "field" | "measure"; x: number; y: number } | null>(null); const [filterValues, setFilterValues] = useState>({}); const menuRef = useRef(null); // Fields currently assigned somewhere const assignedFields = new Set([...config.rows, ...config.columns, ...Object.keys(config.filters) as PivotFieldId[]]); const assignedMeasures = new Set(config.values); const availableFields = ALL_FIELDS.filter((f) => !assignedFields.has(f)); const availableMeasures = ALL_MEASURES.filter((m) => !assignedMeasures.has(m)); // Load filter values when a field is added to filters const filterFieldIds = Object.keys(config.filters) as PivotFieldId[]; useEffect(() => { for (const fieldId of filterFieldIds) { if (!filterValues[fieldId]) { getDynamicFilterValues(fieldId as PivotFieldId, dateFrom, dateTo).then((vals) => { setFilterValues((prev) => ({ ...prev, [fieldId]: vals })); }); } } }, [filterFieldIds.join(","), dateFrom, dateTo]); // Close menu on outside click useEffect(() => { if (!menuTarget) return; const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuTarget(null); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [menuTarget]); const handleFieldClick = (id: string, type: "field" | "measure", e: React.MouseEvent) => { setMenuTarget({ id, type, x: e.clientX, y: e.clientY }); }; const assignTo = useCallback((zone: PivotZone) => { if (!menuTarget) return; const next = { ...config, rows: [...config.rows], columns: [...config.columns], filters: { ...config.filters }, values: [...config.values] }; if (menuTarget.type === "measure") { if (zone === "values") { next.values = [...next.values, menuTarget.id as PivotMeasureId]; } } else { const fieldId = menuTarget.id as PivotFieldId; if (zone === "rows") next.rows = [...next.rows, fieldId]; else if (zone === "columns") next.columns = [...next.columns, fieldId]; else if (zone === "filters") next.filters = { ...next.filters, [fieldId]: [] }; } setMenuTarget(null); onChange(next); }, [menuTarget, config, onChange]); const removeFrom = (zone: PivotZone, id: string) => { const next = { ...config, rows: [...config.rows], columns: [...config.columns], filters: { ...config.filters }, values: [...config.values] }; if (zone === "rows") next.rows = next.rows.filter((f) => f !== id); else if (zone === "columns") next.columns = next.columns.filter((f) => f !== id); else if (zone === "filters") { const { [id]: _, ...rest } = next.filters; next.filters = rest; } else if (zone === "values") next.values = next.values.filter((m) => m !== id); onChange(next); }; const toggleFilterValue = (fieldId: string, value: string) => { const current = config.filters[fieldId] || []; const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; onChange({ ...config, filters: { ...config.filters, [fieldId]: next } }); }; const fieldLabel = (id: string) => t(`reports.pivot.${id === "level1" ? "level1" : id === "level2" ? "level2" : id === "type" ? "categoryType" : id}`); const measureLabel = (id: string) => t(`reports.pivot.${id}`); const zoneOptions: { zone: PivotZone; label: string; forMeasure: boolean }[] = [ { zone: "rows", label: t("reports.pivot.rows"), forMeasure: false }, { zone: "columns", label: t("reports.pivot.columns"), forMeasure: false }, { zone: "filters", label: t("reports.pivot.filters"), forMeasure: false }, { zone: "values", label: t("reports.pivot.values"), forMeasure: true }, ]; return (
{/* Available Fields */}

{t("reports.pivot.availableFields")}

{availableFields.map((f) => ( ))} {availableMeasures.map((m) => ( ))} {availableFields.length === 0 && availableMeasures.length === 0 && ( )}
{/* Rows */} removeFrom("rows", id)} /> {/* Columns */} removeFrom("columns", id)} /> {/* Filters */}

{t("reports.pivot.filters")}

{filterFieldIds.length === 0 ? ( ) : (
{filterFieldIds.map((fieldId) => (
{fieldLabel(fieldId)}
{(filterValues[fieldId] || []).map((val) => { const selected = (config.filters[fieldId] || []).includes(val); const isActiveFilter = (config.filters[fieldId] || []).length > 0; return ( ); })}
))}
)}
{/* Values */} removeFrom("values", id)} accent /> {/* Context menu */} {menuTarget && (
{t("reports.pivot.addTo")}
{zoneOptions .filter((opt) => (menuTarget.type === "measure") === opt.forMeasure) .map((opt) => ( ))}
)}
); } function ZoneSection({ label, items, getLabel, onRemove, accent, }: { label: string; items: string[]; getLabel: (id: string) => string; onRemove: (id: string) => void; accent?: boolean; }) { return (

{label}

{items.length === 0 ? ( ) : (
{items.map((id) => ( {getLabel(id)} ))}
)}
); }