676 lines
22 KiB
TypeScript
676 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
|
import { ComponentRendererProps } from "@/types/component";
|
|
import { AggregationWidgetConfig, AggregationItem, AggregationResult, AggregationType, FilterCondition, DataSourceType } from "./types";
|
|
import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown, Loader2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
interface AggregationWidgetComponentProps extends ComponentRendererProps {
|
|
config?: AggregationWidgetConfig;
|
|
// 외부에서 데이터를 직접 전달받을 수 있음
|
|
externalData?: any[];
|
|
// 폼 데이터 (필터 조건용)
|
|
formData?: Record<string, any>;
|
|
// 선택된 행 데이터
|
|
selectedRows?: any[];
|
|
}
|
|
|
|
/**
|
|
* 필터 조건을 적용하여 데이터 필터링
|
|
*/
|
|
function applyFilters(
|
|
data: any[],
|
|
filters: FilterCondition[],
|
|
filterLogic: "AND" | "OR",
|
|
formData: Record<string, any>,
|
|
selectedRows: any[]
|
|
): any[] {
|
|
if (!filters || filters.length === 0) {
|
|
return data;
|
|
}
|
|
|
|
const enabledFilters = filters.filter((f) => f.enabled && f.columnName);
|
|
|
|
if (enabledFilters.length === 0) {
|
|
return data;
|
|
}
|
|
|
|
return data.filter((row) => {
|
|
const results = enabledFilters.map((filter) => {
|
|
const rowValue = row[filter.columnName];
|
|
|
|
// 값 소스에 따라 비교 값 결정
|
|
let compareValue: any;
|
|
switch (filter.valueSourceType) {
|
|
case "static":
|
|
compareValue = filter.staticValue;
|
|
break;
|
|
case "formField":
|
|
compareValue = formData?.[filter.formFieldName || ""];
|
|
break;
|
|
case "selection":
|
|
// 선택된 행에서 값 가져오기 (첫 번째 선택 행 기준)
|
|
compareValue = selectedRows?.[0]?.[filter.sourceColumnName || ""];
|
|
break;
|
|
case "urlParam":
|
|
if (typeof window !== "undefined") {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
compareValue = urlParams.get(filter.urlParamName || "");
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 연산자에 따른 비교
|
|
switch (filter.operator) {
|
|
case "eq":
|
|
return rowValue == compareValue;
|
|
case "neq":
|
|
return rowValue != compareValue;
|
|
case "gt":
|
|
return Number(rowValue) > Number(compareValue);
|
|
case "gte":
|
|
return Number(rowValue) >= Number(compareValue);
|
|
case "lt":
|
|
return Number(rowValue) < Number(compareValue);
|
|
case "lte":
|
|
return Number(rowValue) <= Number(compareValue);
|
|
case "like":
|
|
return String(rowValue || "").toLowerCase().includes(String(compareValue || "").toLowerCase());
|
|
case "in":
|
|
const inValues = String(compareValue || "").split(",").map((v) => v.trim());
|
|
return inValues.includes(String(rowValue));
|
|
case "isNull":
|
|
return rowValue === null || rowValue === undefined || rowValue === "";
|
|
case "isNotNull":
|
|
return rowValue !== null && rowValue !== undefined && rowValue !== "";
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// AND/OR 논리 적용
|
|
return filterLogic === "AND" ? results.every((r) => r) : results.some((r) => r);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 집계 위젯 컴포넌트
|
|
* 연결된 테이블 리스트나 리피터의 데이터를 집계하여 표시
|
|
*/
|
|
export function AggregationWidgetComponent({
|
|
component,
|
|
isDesignMode = false,
|
|
config: propsConfig,
|
|
externalData,
|
|
formData = {},
|
|
selectedRows = [],
|
|
}: AggregationWidgetComponentProps) {
|
|
// 다국어 지원
|
|
const { getText } = useScreenMultiLang();
|
|
|
|
const componentConfig: AggregationWidgetConfig = {
|
|
dataSourceType: "table",
|
|
items: [],
|
|
layout: "horizontal",
|
|
showLabels: true,
|
|
showIcons: true,
|
|
gap: "16px",
|
|
...propsConfig,
|
|
...component?.config,
|
|
};
|
|
|
|
// 다국어 라벨 가져오기
|
|
const getItemLabel = (item: AggregationItem): string => {
|
|
if (item.labelLangKey) {
|
|
const translated = getText(item.labelLangKey);
|
|
if (translated && translated !== item.labelLangKey) {
|
|
return translated;
|
|
}
|
|
}
|
|
return item.columnLabel || item.columnName || "컬럼";
|
|
};
|
|
|
|
const {
|
|
dataSourceType,
|
|
dataSourceComponentId,
|
|
tableName,
|
|
customTableName,
|
|
useCustomTable,
|
|
filters,
|
|
filterLogic = "AND",
|
|
items,
|
|
layout,
|
|
showLabels,
|
|
showIcons,
|
|
gap,
|
|
backgroundColor,
|
|
borderRadius,
|
|
padding,
|
|
fontSize,
|
|
labelFontSize,
|
|
valueFontSize,
|
|
labelColor,
|
|
valueColor,
|
|
autoRefresh,
|
|
refreshInterval,
|
|
refreshOnFormChange,
|
|
} = componentConfig;
|
|
|
|
// 데이터 상태
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// 사용할 테이블명 결정
|
|
const effectiveTableName = useCustomTable && customTableName ? customTableName : tableName;
|
|
|
|
// Refs로 최신 값 참조 (의존성 배열에서 제외하여 무한 루프 방지)
|
|
const filtersRef = React.useRef(filters);
|
|
const formDataRef = React.useRef(formData);
|
|
const selectedRowsRef = React.useRef(selectedRows);
|
|
|
|
// 값이 변경될 때마다 ref 업데이트
|
|
React.useEffect(() => {
|
|
filtersRef.current = filters;
|
|
}, [filters]);
|
|
|
|
React.useEffect(() => {
|
|
formDataRef.current = formData;
|
|
}, [formData]);
|
|
|
|
React.useEffect(() => {
|
|
selectedRowsRef.current = selectedRows;
|
|
}, [selectedRows]);
|
|
|
|
// 테이블에서 데이터 조회 (dataSourceType === "table"일 때)
|
|
const fetchTableData = useCallback(async () => {
|
|
if (isDesignMode || !effectiveTableName || dataSourceType !== "table") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// 테이블 데이터 조회 API 호출
|
|
// 멀티테넌시: company_code 자동 필터링 활성화
|
|
const response = await apiClient.post(`/table-management/tables/${effectiveTableName}/data`, {
|
|
size: 10000, // 집계용이므로 충분한 데이터 조회
|
|
page: 1,
|
|
autoFilter: {
|
|
enabled: true,
|
|
filterColumn: "company_code",
|
|
userField: "companyCode",
|
|
},
|
|
});
|
|
|
|
// 응답 구조: { success: true, data: { data: [...], total: ... } }
|
|
const raw = response.data?.data || response.data;
|
|
const rows = raw?.data || raw || [];
|
|
|
|
if (Array.isArray(rows)) {
|
|
// 필터 적용
|
|
const filteredData = applyFilters(
|
|
rows,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
} catch (err: any) {
|
|
console.error("집계 위젯 데이터 조회 오류:", err);
|
|
setError(err.message || "데이터 조회 실패");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [effectiveTableName, dataSourceType, isDesignMode, filterLogic]);
|
|
|
|
// 테이블 데이터 조회 (초기 로드)
|
|
useEffect(() => {
|
|
if (dataSourceType === "table" && effectiveTableName && !isDesignMode) {
|
|
fetchTableData();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dataSourceType, effectiveTableName, isDesignMode]);
|
|
|
|
// 폼 데이터 변경 시 재조회 (refreshOnFormChange가 true일 때)
|
|
const formDataKey = JSON.stringify(formData);
|
|
useEffect(() => {
|
|
if (dataSourceType === "table" && refreshOnFormChange && !isDesignMode && effectiveTableName) {
|
|
// 초기 로드 후에만 재조회
|
|
const timeoutId = setTimeout(() => {
|
|
fetchTableData();
|
|
}, 100);
|
|
return () => clearTimeout(timeoutId);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [formDataKey, refreshOnFormChange]);
|
|
|
|
// 자동 새로고침
|
|
useEffect(() => {
|
|
if (dataSourceType === "table" && autoRefresh && refreshInterval && !isDesignMode) {
|
|
const interval = setInterval(fetchTableData, refreshInterval * 1000);
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [dataSourceType, autoRefresh, refreshInterval, isDesignMode, fetchTableData]);
|
|
|
|
// 선택된 행 집계 (dataSourceType === "selection"일 때)
|
|
// props로 전달된 selectedRows 사용
|
|
const selectedRowsKey = JSON.stringify(selectedRows);
|
|
useEffect(() => {
|
|
if (dataSourceType === "selection" && Array.isArray(selectedRows) && selectedRows.length > 0) {
|
|
setData(selectedRows);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dataSourceType, selectedRowsKey]);
|
|
|
|
// 전역 선택 이벤트 수신 (dataSourceType === "selection"일 때)
|
|
useEffect(() => {
|
|
if (dataSourceType !== "selection" || isDesignMode) return;
|
|
|
|
// 테이블리스트에서 발생하는 선택 이벤트 수신
|
|
// tableListDataChange 이벤트의 data가 선택된 행들임
|
|
const handleTableListDataChange = (event: CustomEvent) => {
|
|
const { data: eventData, selectedRows: eventSelectedRows } = event.detail || {};
|
|
// data가 선택된 행 데이터 배열
|
|
const rows = eventData || [];
|
|
|
|
if (Array.isArray(rows)) {
|
|
// 필터 적용
|
|
const filteredData = applyFilters(
|
|
rows,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
};
|
|
|
|
// 리피터에서 발생하는 이벤트
|
|
const handleRepeaterDataChange = (event: CustomEvent) => {
|
|
const { data: eventData, selectedData } = event.detail || {};
|
|
const rows = selectedData || eventData || [];
|
|
|
|
if (Array.isArray(rows)) {
|
|
const filteredData = applyFilters(
|
|
rows,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
};
|
|
|
|
// 일반 선택 이벤트
|
|
const handleSelectionChange = (event: CustomEvent) => {
|
|
const { selectedRows: eventSelectedRows, selectedData, checkedRows, selectedItems } = event.detail || {};
|
|
const rows = selectedData || eventSelectedRows || checkedRows || selectedItems || [];
|
|
|
|
if (Array.isArray(rows)) {
|
|
const filteredData = applyFilters(
|
|
rows,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
};
|
|
|
|
// 다양한 선택 이벤트 수신
|
|
window.addEventListener("tableListDataChange" as any, handleTableListDataChange);
|
|
window.addEventListener("repeaterDataChange" as any, handleRepeaterDataChange);
|
|
window.addEventListener("selectionChange" as any, handleSelectionChange);
|
|
window.addEventListener("tableSelectionChange" as any, handleSelectionChange);
|
|
window.addEventListener("rowSelectionChange" as any, handleSelectionChange);
|
|
window.addEventListener("checkboxSelectionChange" as any, handleSelectionChange);
|
|
|
|
return () => {
|
|
window.removeEventListener("tableListDataChange" as any, handleTableListDataChange);
|
|
window.removeEventListener("repeaterDataChange" as any, handleRepeaterDataChange);
|
|
window.removeEventListener("selectionChange" as any, handleSelectionChange);
|
|
window.removeEventListener("tableSelectionChange" as any, handleSelectionChange);
|
|
window.removeEventListener("rowSelectionChange" as any, handleSelectionChange);
|
|
window.removeEventListener("checkboxSelectionChange" as any, handleSelectionChange);
|
|
};
|
|
}, [dataSourceType, isDesignMode, filterLogic]);
|
|
|
|
// 외부 데이터가 있으면 사용
|
|
const externalDataKey = externalData ? JSON.stringify(externalData.slice(0, 5)) : null; // 첫 5개만 비교
|
|
useEffect(() => {
|
|
if (externalData && Array.isArray(externalData)) {
|
|
// 필터 적용
|
|
const filteredData = applyFilters(
|
|
externalData,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [externalDataKey, filterLogic]);
|
|
|
|
// 컴포넌트 데이터 변경 이벤트 리스닝 (dataSourceType === "component"일 때)
|
|
useEffect(() => {
|
|
if (dataSourceType !== "component" || !dataSourceComponentId || isDesignMode) return;
|
|
|
|
const handleDataChange = (event: CustomEvent) => {
|
|
const { componentId, data: eventData } = event.detail || {};
|
|
if (componentId === dataSourceComponentId && Array.isArray(eventData)) {
|
|
// 필터 적용
|
|
const filteredData = applyFilters(
|
|
eventData,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
};
|
|
|
|
// 선택 변경 이벤트 (체크박스 선택 등)
|
|
const handleSelectionChange = (event: CustomEvent) => {
|
|
const { componentId, selectedData } = event.detail || {};
|
|
if (componentId === dataSourceComponentId && Array.isArray(selectedData)) {
|
|
// 선택된 데이터만 집계
|
|
const filteredData = applyFilters(
|
|
selectedData,
|
|
filtersRef.current || [],
|
|
filterLogic,
|
|
formDataRef.current,
|
|
selectedRowsRef.current
|
|
);
|
|
setData(filteredData);
|
|
}
|
|
};
|
|
|
|
// 리피터 데이터 변경 이벤트
|
|
window.addEventListener("repeaterDataChange" as any, handleDataChange);
|
|
// 테이블 리스트 데이터 변경 이벤트
|
|
window.addEventListener("tableListDataChange" as any, handleDataChange);
|
|
// 선택 변경 이벤트
|
|
window.addEventListener("selectionChange" as any, handleSelectionChange);
|
|
|
|
return () => {
|
|
window.removeEventListener("repeaterDataChange" as any, handleDataChange);
|
|
window.removeEventListener("tableListDataChange" as any, handleDataChange);
|
|
window.removeEventListener("selectionChange" as any, handleSelectionChange);
|
|
};
|
|
}, [dataSourceType, dataSourceComponentId, isDesignMode, filterLogic]);
|
|
|
|
// 집계 계산
|
|
const aggregationResults = useMemo((): AggregationResult[] => {
|
|
if (!items || items.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return items.map((item) => {
|
|
const values = data
|
|
.map((row) => {
|
|
const val = row[item.columnName];
|
|
const parsed = typeof val === "number" ? val : parseFloat(val) || 0;
|
|
return parsed;
|
|
})
|
|
.filter((v) => !isNaN(v));
|
|
|
|
|
|
let value: number = 0;
|
|
|
|
switch (item.type) {
|
|
case "sum":
|
|
value = values.reduce((acc, v) => acc + v, 0);
|
|
break;
|
|
case "avg":
|
|
value = values.length > 0 ? values.reduce((acc, v) => acc + v, 0) / values.length : 0;
|
|
break;
|
|
case "count":
|
|
value = data.length;
|
|
break;
|
|
case "max":
|
|
value = values.length > 0 ? Math.max(...values) : 0;
|
|
break;
|
|
case "min":
|
|
value = values.length > 0 ? Math.min(...values) : 0;
|
|
break;
|
|
}
|
|
|
|
// 포맷팅
|
|
let formattedValue = value.toFixed(item.decimalPlaces ?? 0);
|
|
|
|
if (item.format === "currency") {
|
|
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
|
} else if (item.format === "percent") {
|
|
formattedValue = `${(value * 100).toFixed(item.decimalPlaces ?? 1)}%`;
|
|
} else if (item.format === "number") {
|
|
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
|
}
|
|
|
|
if (item.prefix) {
|
|
formattedValue = `${item.prefix}${formattedValue}`;
|
|
}
|
|
if (item.suffix) {
|
|
formattedValue = `${formattedValue}${item.suffix}`;
|
|
}
|
|
|
|
return {
|
|
id: item.id,
|
|
label: getItemLabel(item),
|
|
value,
|
|
formattedValue,
|
|
type: item.type,
|
|
};
|
|
});
|
|
}, [data, items, getText]);
|
|
|
|
// 집계 타입에 따른 아이콘
|
|
const getIcon = (type: AggregationType) => {
|
|
switch (type) {
|
|
case "sum":
|
|
return <Calculator className="h-4 w-4" />;
|
|
case "avg":
|
|
return <TrendingUp className="h-4 w-4" />;
|
|
case "count":
|
|
return <Hash className="h-4 w-4" />;
|
|
case "max":
|
|
return <ArrowUp className="h-4 w-4" />;
|
|
case "min":
|
|
return <ArrowDown className="h-4 w-4" />;
|
|
}
|
|
};
|
|
|
|
// 집계 타입 라벨
|
|
const getTypeLabel = (type: AggregationType) => {
|
|
switch (type) {
|
|
case "sum":
|
|
return "합계";
|
|
case "avg":
|
|
return "평균";
|
|
case "count":
|
|
return "개수";
|
|
case "max":
|
|
return "최대";
|
|
case "min":
|
|
return "최소";
|
|
}
|
|
};
|
|
|
|
// 데이터 소스 타입 라벨
|
|
const getDataSourceLabel = (type: DataSourceType) => {
|
|
switch (type) {
|
|
case "table":
|
|
return "테이블";
|
|
case "component":
|
|
return "컴포넌트";
|
|
case "selection":
|
|
return "선택 데이터";
|
|
default:
|
|
return "수동";
|
|
}
|
|
};
|
|
|
|
// 디자인 모드 미리보기
|
|
if (isDesignMode) {
|
|
const previewItems: AggregationResult[] =
|
|
items.length > 0
|
|
? items.map((item) => ({
|
|
id: item.id,
|
|
label: getItemLabel(item),
|
|
value: 0,
|
|
formattedValue: item.prefix ? `${item.prefix}0${item.suffix || ""}` : `0${item.suffix || ""}`,
|
|
type: item.type,
|
|
}))
|
|
: [
|
|
{ id: "1", label: "총 수량", value: 150, formattedValue: "150", type: "sum" },
|
|
{ id: "2", label: "총 금액", value: 1500000, formattedValue: "₩1,500,000", type: "sum" },
|
|
{ id: "3", label: "건수", value: 5, formattedValue: "5건", type: "count" },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
{/* 디자인 모드에서 데이터 소스 표시 */}
|
|
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
|
<span>[{getDataSourceLabel(dataSourceType)}]</span>
|
|
{dataSourceType === "table" && effectiveTableName && (
|
|
<span>{effectiveTableName}</span>
|
|
)}
|
|
{dataSourceType === "component" && dataSourceComponentId && (
|
|
<span>{dataSourceComponentId}</span>
|
|
)}
|
|
{(filters || []).length > 0 && (
|
|
<span className="text-blue-500">필터 {filters?.length}개</span>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"flex items-center rounded-md border bg-slate-50 p-3",
|
|
layout === "vertical" ? "flex-col items-stretch" : "flex-row flex-wrap"
|
|
)}
|
|
style={{
|
|
gap: gap || "12px",
|
|
backgroundColor: backgroundColor || undefined,
|
|
borderRadius: borderRadius || undefined,
|
|
padding: padding || undefined,
|
|
fontSize: fontSize || undefined,
|
|
}}
|
|
>
|
|
{previewItems.map((result, index) => (
|
|
<div
|
|
key={result.id || index}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border bg-white px-3 py-2 shadow-sm",
|
|
layout === "vertical" ? "w-full justify-between" : ""
|
|
)}
|
|
>
|
|
{showIcons && (
|
|
<span className="text-muted-foreground">{getIcon(result.type)}</span>
|
|
)}
|
|
{showLabels && (
|
|
<span
|
|
className="text-muted-foreground text-xs"
|
|
style={{ fontSize: labelFontSize, color: labelColor }}
|
|
>
|
|
{result.label} ({getTypeLabel(result.type)}):
|
|
</span>
|
|
)}
|
|
<span
|
|
className="font-semibold"
|
|
style={{ fontSize: valueFontSize, color: valueColor }}
|
|
>
|
|
{result.formattedValue}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 로딩 상태
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center rounded-md border bg-slate-50 p-4">
|
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
<span className="ml-2 text-sm text-muted-foreground">데이터 집계 중...</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 에러 상태
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center rounded-md border border-destructive bg-destructive/10 p-4 text-sm text-destructive">
|
|
{error}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 실제 렌더링
|
|
if (aggregationResults.length === 0) {
|
|
return (
|
|
<div className="flex items-center justify-center rounded-md border border-dashed bg-slate-50 p-4 text-sm text-muted-foreground">
|
|
집계 항목을 설정해주세요
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center rounded-md border bg-slate-50 p-3",
|
|
layout === "vertical" ? "flex-col items-stretch" : "flex-row flex-wrap"
|
|
)}
|
|
style={{
|
|
gap: gap || "12px",
|
|
backgroundColor: backgroundColor || undefined,
|
|
borderRadius: borderRadius || undefined,
|
|
padding: padding || undefined,
|
|
fontSize: fontSize || undefined,
|
|
}}
|
|
>
|
|
{aggregationResults.map((result, index) => (
|
|
<div
|
|
key={result.id || index}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border bg-white px-3 py-2 shadow-sm",
|
|
layout === "vertical" ? "w-full justify-between" : ""
|
|
)}
|
|
>
|
|
{showIcons && (
|
|
<span className="text-muted-foreground">{getIcon(result.type)}</span>
|
|
)}
|
|
{showLabels && (
|
|
<span
|
|
className="text-muted-foreground text-xs"
|
|
style={{ fontSize: labelFontSize, color: labelColor }}
|
|
>
|
|
{result.label} ({getTypeLabel(result.type)}):
|
|
</span>
|
|
)}
|
|
<span
|
|
className="font-semibold"
|
|
style={{ fontSize: valueFontSize, color: valueColor }}
|
|
>
|
|
{result.formattedValue}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const AggregationWidgetWrapper = AggregationWidgetComponent;
|
|
|