ERP-node/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarComponent.tsx

244 lines
8.1 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect, useMemo } from "react";
import { cn } from "@/lib/utils";
import { usePopEvent } from "@/hooks/pop";
import type { StatusBarConfig, StatusChipOption } from "./types";
import { DEFAULT_STATUS_BAR_CONFIG } from "./types";
interface PopStatusBarComponentProps {
config: StatusBarConfig;
label?: string;
screenId?: string;
componentId?: string;
}
export function PopStatusBarComponent({
config: rawConfig,
label,
screenId,
componentId,
}: PopStatusBarComponentProps) {
const config = { ...DEFAULT_STATUS_BAR_CONFIG, ...(rawConfig || {}) };
const { publish, subscribe } = usePopEvent(screenId || "");
const [selectedValue, setSelectedValue] = useState<string>("");
const [allRows, setAllRows] = useState<Record<string, unknown>[]>([]);
const [autoSubStatusColumn, setAutoSubStatusColumn] = useState<string | null>(null);
// all_rows 이벤트 구독
useEffect(() => {
if (!componentId) return;
const unsub = subscribe(
`__comp_input__${componentId}__all_rows`,
(payload: unknown) => {
const data = payload as { value?: unknown } | unknown;
const inner =
typeof data === "object" && data && "value" in data
? (data as { value: unknown }).value
: data;
if (
typeof inner === "object" &&
inner &&
!Array.isArray(inner) &&
"rows" in inner
) {
const envelope = inner as {
rows?: unknown;
subStatusColumn?: string | null;
};
if (Array.isArray(envelope.rows))
setAllRows(envelope.rows as Record<string, unknown>[]);
setAutoSubStatusColumn(envelope.subStatusColumn ?? null);
} else if (Array.isArray(inner)) {
setAllRows(inner as Record<string, unknown>[]);
setAutoSubStatusColumn(null);
}
}
);
return unsub;
}, [componentId, subscribe]);
// 외부에서 값 설정 이벤트 구독
useEffect(() => {
if (!componentId) return;
const unsub = subscribe(
`__comp_input__${componentId}__set_value`,
(payload: unknown) => {
const data = payload as { value?: unknown } | unknown;
const incoming =
typeof data === "object" && data && "value" in data
? (data as { value: unknown }).value
: data;
setSelectedValue(String(incoming ?? ""));
}
);
return unsub;
}, [componentId, subscribe]);
const emitFilter = useCallback(
(newValue: string) => {
setSelectedValue(newValue);
if (!componentId) return;
const baseColumn = config.filterColumn || config.countColumn || "";
const subActive = config.useSubCount && !!autoSubStatusColumn;
const filterColumns = subActive
? [...new Set([baseColumn, autoSubStatusColumn!].filter(Boolean))]
: [baseColumn].filter(Boolean);
publish(`__comp_output__${componentId}__filter_value`, {
fieldName: baseColumn,
filterColumns,
value: newValue,
filterMode: "equals",
_source: "status-bar",
});
},
[componentId, publish, config.filterColumn, config.countColumn, config.useSubCount, autoSubStatusColumn]
);
const chipCfg = config;
const showCount = chipCfg.showCount !== false;
const baseCountColumn = chipCfg.countColumn || "";
const useSubCount = chipCfg.useSubCount || false;
const hideUntilSubFilter = chipCfg.hideUntilSubFilter || false;
const allowAll = chipCfg.allowAll !== false;
const allLabel = chipCfg.allLabel || "전체";
const chipStyle = chipCfg.chipStyle || "tab";
const options: StatusChipOption[] = chipCfg.options || [];
// 하위 필터(공정) 활성 여부
const subFilterActive = useSubCount && !!autoSubStatusColumn;
// hideUntilSubFilter가 켜져있으면서 아직 공정 선택이 안 된 경우 숨김
const shouldHide = hideUntilSubFilter && !subFilterActive;
const effectiveCountColumn =
subFilterActive ? autoSubStatusColumn : baseCountColumn;
const counts = useMemo(() => {
if (!showCount || !effectiveCountColumn || allRows.length === 0)
return new Map<string, number>();
const map = new Map<string, number>();
for (const row of allRows) {
if (row == null || typeof row !== "object") continue;
const v = String(row[effectiveCountColumn] ?? "");
map.set(v, (map.get(v) || 0) + 1);
}
return map;
}, [allRows, effectiveCountColumn, showCount]);
const totalCount = allRows.length;
const chipItems = useMemo(() => {
const items: { value: string; label: string; count: number }[] = [];
if (allowAll) {
items.push({ value: "", label: allLabel, count: totalCount });
}
for (const opt of options) {
items.push({
value: opt.value,
label: opt.label,
count: counts.get(opt.value) || 0,
});
}
return items;
}, [options, counts, totalCount, allowAll, allLabel]);
const showLabel = !!label;
if (shouldHide) {
return (
<div className="flex h-full w-full items-center justify-center p-1.5">
<span className="text-[10px] text-muted-foreground/50">
{chipCfg.hiddenMessage || "조건을 선택하면 상태별 현황이 표시됩니다"}
</span>
</div>
);
}
if (chipStyle === "pill") {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-0.5 overflow-hidden p-1.5">
{showLabel && (
<span className="w-full shrink-0 truncate text-[10px] font-medium text-muted-foreground">
{label}
</span>
)}
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
{chipItems.map((item) => {
const isActive = selectedValue === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => emitFilter(item.value)}
className={cn(
"flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
)}
>
{item.label}
{showCount && (
<span
className={cn(
"ml-0.5 min-w-[18px] rounded-full px-1 py-0.5 text-center text-[10px] font-bold leading-none",
isActive
? "bg-primary-foreground/20 text-primary-foreground"
: "bg-background text-foreground"
)}
>
{item.count}
</span>
)}
</button>
);
})}
</div>
</div>
);
}
// tab 스타일 (기본)
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-0.5 overflow-hidden p-1.5">
{showLabel && (
<span className="w-full shrink-0 truncate text-[10px] font-medium text-muted-foreground">
{label}
</span>
)}
<div className="flex min-w-0 flex-1 items-center justify-center gap-2">
{chipItems.map((item) => {
const isActive = selectedValue === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => emitFilter(item.value)}
className={cn(
"flex min-w-[60px] flex-col items-center justify-center rounded-lg px-3 py-1.5 transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/60 text-muted-foreground hover:bg-accent"
)}
>
{showCount && (
<span className="text-lg font-bold leading-tight">
{item.count}
</span>
)}
<span className="text-[10px] font-medium leading-tight">
{item.label}
</span>
</button>
);
})}
</div>
</div>
);
}