import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { X } from "lucide-react"; import type { PivotConfig, PivotFieldId, PivotFilterEntry, PivotMeasureId, PivotZone } from "../../shared/types"; import { getDynamicFilterValues } from "../../services/reportService"; const ALL_FIELDS: PivotFieldId[] = ["year", "month", "type", "level1", "level2", "level3"]; const ALL_MEASURES: PivotMeasureId[] = ["periodic", "ytd"]; interface DynamicReportPanelProps { config: PivotConfig; onChange: (config: PivotConfig) => void; } export default function DynamicReportPanel({ config, onChange }: 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); // A field is only "exhausted" if it's in all 3 zones (rows + columns + filters) const inRows = new Set(config.rows); const inColumns = new Set(config.columns); const inFilters = new Set(Object.keys(config.filters) as PivotFieldId[]); const assignedFields = new Set( ALL_FIELDS.filter((f) => inRows.has(f) && inColumns.has(f) && inFilters.has(f)) ); 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).then((vals) => { setFilterValues((prev) => ({ ...prev, [fieldId]: vals })); }); } } }, [filterFieldIds.join(",")]); // 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]: { include: [], exclude: [] } }; } 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 toggleFilterInclude = (fieldId: string, value: string) => { const entry: PivotFilterEntry = config.filters[fieldId] || { include: [], exclude: [] }; const isIncluded = entry.include.includes(value); const newInclude = isIncluded ? entry.include.filter((v) => v !== value) : [...entry.include, value]; // Remove from exclude if adding to include const newExclude = isIncluded ? entry.exclude : entry.exclude.filter((v) => v !== value); onChange({ ...config, filters: { ...config.filters, [fieldId]: { include: newInclude, exclude: newExclude } } }); }; const toggleFilterExclude = (fieldId: string, value: string) => { const entry: PivotFilterEntry = config.filters[fieldId] || { include: [], exclude: [] }; const isExcluded = entry.exclude.includes(value); const newExclude = isExcluded ? entry.exclude.filter((v) => v !== value) : [...entry.exclude, value]; // Remove from include if adding to exclude const newInclude = isExcluded ? entry.include : entry.include.filter((v) => v !== value); onChange({ ...config, filters: { ...config.filters, [fieldId]: { include: newInclude, exclude: newExclude } } }); }; const fieldLabel = (id: string) => t(`reports.pivot.${id === "level1" ? "level1" : id === "level2" ? "level2" : id === "level3" ? "level3" : id === "type" ? "categoryType" : id}`); const measureLabel = (id: string) => t(`reports.pivot.${id}`); // Context menu only shows zones where the field is NOT already assigned const getAvailableZones = (fieldId: string): PivotZone[] => { const zones: PivotZone[] = []; if (!inRows.has(fieldId as PivotFieldId)) zones.push("rows"); if (!inColumns.has(fieldId as PivotFieldId)) zones.push("columns"); if (!inFilters.has(fieldId as PivotFieldId)) zones.push("filters"); return zones; }; const zoneLabels: Record = { rows: t("reports.pivot.rows"), columns: t("reports.pivot.columns"), filters: t("reports.pivot.filters"), values: t("reports.pivot.values"), }; 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) => { const entry = config.filters[fieldId] || { include: [], exclude: [] }; const hasActive = entry.include.length > 0 || entry.exclude.length > 0; return (
{fieldLabel(fieldId)}
{(filterValues[fieldId] || []).map((val) => { const isIncluded = entry.include.includes(val); const isExcluded = entry.exclude.includes(val); return ( ); })}
); })}
)}
{/* Values */} removeFrom("values", id)} accent /> {/* Context menu */} {menuTarget && (
{t("reports.pivot.addTo")}
{menuTarget.type === "measure" ? ( ) : ( getAvailableZones(menuTarget.id).map((zone) => ( )) )}
)}
); } 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)} ))}
)}
); }