feat: MES 상태 탭을 카드 컴포넌트에 내장
별도 pop-status-bar 컴포넌트 배치/연결 없이 카드 상단에 MES 고정 상태 탭(전체/대기/접수가능/진행/완료)을 내장한다. [내장 상태 탭 UI] - MES_STATUS_TABS 상수: waiting/acceptable/in_progress/completed 고정 - 카드 그리드 상단에 pill 형태 탭 렌더링 - 상태별 카운트 + "전체"는 클론 제외 originalCount 표시 - showStatusTabs && hasProcessFlow 조건부 표시 [자체 카운트/필터] - statusCounts useMemo: filteredRows 기준 VIRTUAL_SUB_STATUS 집계 - statusFilteredRows useMemo: 선택 탭으로 내부 필터 - displayCards/hasMoreCards/totalPages가 statusFilteredRows 기준으로 동작 - 탭 전환 시 currentPage=1 리셋 [외부 status-bar 하위 호환] - effectiveExternalFilters: showStatusTabs일 때 _source="status-bar" 필터 무시 (내장 탭으로 대체, 충돌 방지) - all_rows 이벤트 발행 유지 (기존 pop-status-bar 연결 안 깨짐) - rowsForStatusCount: showStatusTabs일 때 filteredRows 직접 반환 [설정 패널] - 고급 설정에 "상태 탭 내장" Switch 토글 추가 - showStatusTabs?: boolean 타입 추가 (기본 false)
This commit is contained in:
parent
1d85de8bf6
commit
17fb815513
|
|
@ -61,6 +61,14 @@ const LazyPopWorkDetail = dynamic(
|
|||
|
||||
type RowData = Record<string, unknown>;
|
||||
|
||||
const MES_STATUS_TABS = [
|
||||
{ value: "", label: "전체" },
|
||||
{ value: "waiting", label: "대기", color: "#94a3b8", bg: "#f8fafc" },
|
||||
{ value: "acceptable", label: "접수가능", color: "#2563eb", bg: "#eff6ff" },
|
||||
{ value: "in_progress", label: "진행중", color: "#d97706", bg: "#fffbeb" },
|
||||
{ value: "completed", label: "완료", color: "#059669", bg: "#ecfdf5" },
|
||||
] as const;
|
||||
|
||||
function calculateMaxQty(
|
||||
row: RowData,
|
||||
processId: string | number | undefined,
|
||||
|
|
@ -357,6 +365,9 @@ export function PopCardListV2Component({
|
|||
return true;
|
||||
}, [selectMode, selectModeStatus]);
|
||||
|
||||
// 내장 상태 탭
|
||||
const [selectedStatusTab, setSelectedStatusTab] = useState("");
|
||||
|
||||
// 확장/페이지네이션
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
|
@ -585,23 +596,57 @@ export function PopCardListV2Component({
|
|||
return col ? subTableKeys.has(col) : false;
|
||||
}, [subTableKeys]);
|
||||
|
||||
// showStatusTabs일 때 외부 status-bar 필터 무시 (내장 탭으로 대체)
|
||||
const effectiveExternalFilters = useMemo(() => {
|
||||
if (!config?.showStatusTabs) return externalFilters;
|
||||
const filtered = new Map(
|
||||
[...externalFilters.entries()].filter(([, f]) => f._source !== "status-bar")
|
||||
);
|
||||
return filtered;
|
||||
}, [externalFilters, config?.showStatusTabs]);
|
||||
|
||||
// 외부 필터 (자동 분류: 컬럼이 processFlow에 있으면 subFilter)
|
||||
const filteredRows = useMemo(() => {
|
||||
if (externalFilters.size === 0) return duplicateAcceptableCards(rows);
|
||||
if (effectiveExternalFilters.size === 0) return duplicateAcceptableCards(rows);
|
||||
|
||||
const allFilters = [...externalFilters.values()];
|
||||
const allFilters = [...effectiveExternalFilters.values()];
|
||||
const mainFilters = allFilters.filter((f) => !isSubTableColumn(f));
|
||||
const subFilters = allFilters.filter((f) => isSubTableColumn(f));
|
||||
|
||||
const afterDuplicate = applySubFilterAndDuplicate(rows, subFilters);
|
||||
return applyMainFilters(afterDuplicate, mainFilters, subFilters.length > 0);
|
||||
}, [rows, externalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
|
||||
}, [rows, effectiveExternalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
|
||||
|
||||
// 하위 필터 활성 여부
|
||||
const hasActiveSubFilter = useMemo(() => {
|
||||
if (externalFilters.size === 0) return false;
|
||||
return [...externalFilters.values()].some((f) => isSubTableColumn(f));
|
||||
}, [externalFilters, isSubTableColumn]);
|
||||
if (effectiveExternalFilters.size === 0) return false;
|
||||
return [...effectiveExternalFilters.values()].some((f) => isSubTableColumn(f));
|
||||
}, [effectiveExternalFilters, isSubTableColumn]);
|
||||
|
||||
// 내장 상태 탭: 카운트 계산 (filteredRows 기준, 상태 필터 적용 전)
|
||||
const hasProcessFlow = useMemo(
|
||||
() => rows.some((r) => r[VIRTUAL_SUB_STATUS] !== undefined),
|
||||
[rows],
|
||||
);
|
||||
const statusCounts = useMemo(() => {
|
||||
if (!config?.showStatusTabs || !hasProcessFlow) return null;
|
||||
const map = new Map<string, number>();
|
||||
let originalTotal = 0;
|
||||
for (const row of filteredRows) {
|
||||
const v = String(row[VIRTUAL_SUB_STATUS] ?? "");
|
||||
if (v) map.set(v, (map.get(v) || 0) + 1);
|
||||
if (!row.__isAcceptClone) originalTotal++;
|
||||
}
|
||||
return { counts: map, total: originalTotal };
|
||||
}, [filteredRows, config?.showStatusTabs, hasProcessFlow]);
|
||||
|
||||
// 내장 상태 탭: 필터 적용
|
||||
const statusFilteredRows = useMemo(() => {
|
||||
if (!config?.showStatusTabs || !selectedStatusTab) return filteredRows;
|
||||
return filteredRows.filter((row) =>
|
||||
String(row[VIRTUAL_SUB_STATUS] ?? "") === selectedStatusTab
|
||||
);
|
||||
}, [filteredRows, selectedStatusTab, config?.showStatusTabs]);
|
||||
|
||||
// 선택 모드 일괄 처리
|
||||
const handleSelectModeAction = useCallback(async (btnConfig: SelectModeButtonConfig) => {
|
||||
|
|
@ -680,8 +725,9 @@ export function PopCardListV2Component({
|
|||
}
|
||||
}, [selectedRowIds, filteredRows, exitSelectMode]);
|
||||
|
||||
// status-bar 필터를 제외한 rows (카운트 집계용)
|
||||
// status-bar 필터를 제외한 rows (외부 status-bar 카운트 집계용, 하위 호환)
|
||||
const rowsForStatusCount = useMemo(() => {
|
||||
if (config?.showStatusTabs) return filteredRows;
|
||||
const hasStatusBarFilter = [...externalFilters.values()].some((f) => f._source === "status-bar");
|
||||
if (!hasStatusBarFilter) return filteredRows;
|
||||
|
||||
|
|
@ -696,7 +742,7 @@ export function PopCardListV2Component({
|
|||
|
||||
const afterDuplicate = applySubFilterAndDuplicate(rows, subFilters);
|
||||
return applyMainFilters(afterDuplicate, mainFilters, subFilters.length > 0);
|
||||
}, [rows, filteredRows, externalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
|
||||
}, [rows, filteredRows, externalFilters, config?.showStatusTabs, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
|
||||
|
||||
// 카운트 집계용 rows 발행 (status-bar 필터 제외)
|
||||
// originalCount: 복제 카드를 제외한 원본 카드 수
|
||||
|
|
@ -713,7 +759,7 @@ export function PopCardListV2Component({
|
|||
const overflowCfg = effectiveConfig?.overflow;
|
||||
const baseVisibleCount = overflowCfg?.visibleCount ?? gridColumns * gridRows;
|
||||
const visibleCardCount = useMemo(() => Math.max(1, baseVisibleCount), [baseVisibleCount]);
|
||||
const hasMoreCards = filteredRows.length > visibleCardCount;
|
||||
const hasMoreCards = statusFilteredRows.length > visibleCardCount;
|
||||
const expandedCardsPerPage = useMemo(() => {
|
||||
if (overflowCfg?.mode === "pagination" && overflowCfg.pageSize) return overflowCfg.pageSize;
|
||||
if (overflowCfg?.mode === "loadMore" && overflowCfg.loadMoreCount) return overflowCfg.loadMoreCount + visibleCardCount;
|
||||
|
|
@ -726,7 +772,7 @@ export function PopCardListV2Component({
|
|||
const ownerFilterMode = config?.ownerFilterMode || "priority";
|
||||
|
||||
const displayCards = useMemo(() => {
|
||||
let source = filteredRows;
|
||||
let source = statusFilteredRows;
|
||||
|
||||
if (ownerSortColumn && currentUserId) {
|
||||
const mine: RowData[] = [];
|
||||
|
|
@ -744,9 +790,9 @@ export function PopCardListV2Component({
|
|||
if (!isExpanded) return source.slice(0, visibleCardCount);
|
||||
const start = (currentPage - 1) * expandedCardsPerPage;
|
||||
return source.slice(start, start + expandedCardsPerPage);
|
||||
}, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, ownerFilterMode, currentUserId]);
|
||||
}, [statusFilteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, ownerFilterMode, currentUserId]);
|
||||
|
||||
const totalPages = isExpanded ? Math.ceil(filteredRows.length / expandedCardsPerPage) : 1;
|
||||
const totalPages = isExpanded ? Math.ceil(statusFilteredRows.length / expandedCardsPerPage) : 1;
|
||||
const needsPagination = isExpanded && totalPages > 1;
|
||||
|
||||
const toggleExpand = () => {
|
||||
|
|
@ -1244,7 +1290,7 @@ export function PopCardListV2Component({
|
|||
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
|
||||
<p className="text-sm text-muted-foreground">데이터 소스를 설정해주세요.</p>
|
||||
</div>
|
||||
) : effectiveConfig?.hideUntilFiltered && externalFilters.size === 0 ? (
|
||||
) : effectiveConfig?.hideUntilFiltered && effectiveExternalFilters.size === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
|
||||
<p className="text-sm text-muted-foreground">필터를 선택하면 데이터가 표시됩니다.</p>
|
||||
</div>
|
||||
|
|
@ -1290,10 +1336,10 @@ export function PopCardListV2Component({
|
|||
<div className="flex shrink-0 items-center gap-3 border-b px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedKeys.size === filteredRows.length && filteredRows.length > 0}
|
||||
checked={selectedKeys.size === statusFilteredRows.length && statusFilteredRows.length > 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedKeys(new Set(filteredRows.map((r) => String(r.__cart_id ?? ""))));
|
||||
setSelectedKeys(new Set(statusFilteredRows.map((r) => String(r.__cart_id ?? ""))));
|
||||
} else {
|
||||
setSelectedKeys(new Set());
|
||||
}
|
||||
|
|
@ -1306,6 +1352,41 @@ export function PopCardListV2Component({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 내장 MES 상태 탭 */}
|
||||
{config?.showStatusTabs && statusCounts && hasProcessFlow && !selectMode && (
|
||||
<div className="flex shrink-0 items-center gap-2 border-b bg-background px-4 py-2">
|
||||
{MES_STATUS_TABS.map((tab) => {
|
||||
const isActive = selectedStatusTab === tab.value;
|
||||
const count = tab.value === ""
|
||||
? statusCounts.total
|
||||
: (statusCounts.counts.get(tab.value) || 0);
|
||||
return (
|
||||
<button
|
||||
key={tab.value}
|
||||
type="button"
|
||||
onClick={() => { setSelectedStatusTab(tab.value); setCurrentPage(1); }}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-full px-4 py-2 text-sm font-semibold transition-colors",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "bg-muted text-muted-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
<span className={cn(
|
||||
"min-w-[20px] rounded-full px-1.5 py-0.5 text-center text-xs font-bold leading-none",
|
||||
isActive
|
||||
? "bg-primary-foreground/20 text-primary-foreground"
|
||||
: "bg-background text-foreground"
|
||||
)}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollAreaRef}
|
||||
className={`min-h-0 flex-1 grid ${scrollClassName}`}
|
||||
|
|
|
|||
|
|
@ -3235,6 +3235,21 @@ function TabActions({
|
|||
</button>
|
||||
{advancedOpen && (
|
||||
<div className="mt-2 space-y-3 rounded border bg-muted/10 p-2">
|
||||
{/* 내장 상태 탭 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">상태 탭 내장</Label>
|
||||
<Switch
|
||||
checked={!!cfg.showStatusTabs}
|
||||
onCheckedChange={(checked) => onUpdate({ showStatusTabs: checked })}
|
||||
/>
|
||||
</div>
|
||||
{cfg.showStatusTabs && (
|
||||
<p className="text-[9px] text-muted-foreground -mt-2 pl-1">
|
||||
카드 상단에 MES 상태 탭(전체/대기/접수가능/진행/완료)이 표시됩니다.
|
||||
별도 상태 바 컴포넌트가 필요 없습니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 필터 전 비표시 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">필터 전 데이터 숨김</Label>
|
||||
|
|
|
|||
|
|
@ -1006,6 +1006,7 @@ export interface PopCardListV2Config {
|
|||
ownerSortColumn?: string;
|
||||
ownerFilterMode?: "priority" | "only";
|
||||
workDetailConfig?: PopWorkDetailConfig;
|
||||
showStatusTabs?: boolean;
|
||||
}
|
||||
|
||||
/** 카드 컴포넌트가 하위 필터 적용 시 주입하는 가상 컬럼 키 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue