2a23cadb41
- Added `end_date` field to user management for better tracking of user status. - Updated SQL queries in `adminController` to include `end_date` during user save operations. - Improved purchase report data handling by refining the logic for received quantities. - Enhanced file preview functionality to streamline file path handling. - Updated outbound and receiving controllers to ensure accurate updates to shipment and purchase order details. These changes aim to improve the overall functionality and user experience in managing user data and reporting processes.
630 lines
25 KiB
TypeScript
630 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo, useEffect, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
// Card 제거 — rounded-lg border bg-card 패턴 사용
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import {
|
|
Search,
|
|
RotateCcw,
|
|
Package,
|
|
ClipboardList,
|
|
Factory,
|
|
MapPin,
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
Loader2,
|
|
Inbox,
|
|
Settings2,
|
|
} from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
getWorkOrders,
|
|
getMaterialStatus,
|
|
getWarehouses,
|
|
type WorkOrder,
|
|
type MaterialData,
|
|
type WarehouseData,
|
|
} from "@/lib/api/materialStatus";
|
|
import { useTableSettings } from "@/hooks/useTableSettings";
|
|
import { TableSettingsModal } from "@/components/common/TableSettingsModal";
|
|
|
|
const GRID_COLUMNS = [
|
|
{ key: "plan_no", label: "계획번호" },
|
|
{ key: "item_code", label: "품목코드" },
|
|
{ key: "item_name", label: "품목명" },
|
|
{ key: "plan_qty", label: "수량" },
|
|
{ key: "plan_date", label: "일자" },
|
|
{ key: "status", label: "상태" },
|
|
];
|
|
|
|
const formatDate = (date: Date) => {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
return `${y}-${m}-${d}`;
|
|
};
|
|
|
|
const getStatusLabel = (status: string) => {
|
|
const map: Record<string, string> = {
|
|
planned: "계획",
|
|
in_progress: "진행중",
|
|
completed: "완료",
|
|
pending: "대기",
|
|
cancelled: "취소",
|
|
};
|
|
return map[status] || status;
|
|
};
|
|
|
|
const getStatusStyle = (status: string) => {
|
|
const map: Record<string, string> = {
|
|
planned: "bg-secondary text-secondary-foreground border-border",
|
|
pending: "bg-secondary text-secondary-foreground border-border",
|
|
in_progress: "bg-primary/10 text-primary border-primary/20",
|
|
completed: "bg-accent text-accent-foreground border-accent/50",
|
|
cancelled: "bg-muted text-muted-foreground border-border",
|
|
};
|
|
return map[status] || "bg-muted text-muted-foreground border-border";
|
|
};
|
|
|
|
export default function MaterialStatusPage() {
|
|
const ts = useTableSettings("c16-material-status", "work_instruction", GRID_COLUMNS);
|
|
const today = new Date();
|
|
const monthAgo = new Date(today);
|
|
monthAgo.setMonth(today.getMonth() - 1);
|
|
|
|
const [searchDateFrom, setSearchDateFrom] = useState(formatDate(monthAgo));
|
|
const [searchDateTo, setSearchDateTo] = useState(formatDate(today));
|
|
const [searchItemCode, setSearchItemCode] = useState("");
|
|
const [searchItemName, setSearchItemName] = useState("");
|
|
|
|
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
|
const [workOrdersLoading, setWorkOrdersLoading] = useState(false);
|
|
const [checkedWoIds, setCheckedWoIds] = useState<string[]>([]);
|
|
const [selectedWoId, setSelectedWoId] = useState<string | null>(null);
|
|
|
|
const [warehouses, setWarehouses] = useState<WarehouseData[]>([]);
|
|
const [warehouse, setWarehouse] = useState("");
|
|
const [materialSearch, setMaterialSearch] = useState("");
|
|
const [showShortageOnly, setShowShortageOnly] = useState(false);
|
|
const [materials, setMaterials] = useState<MaterialData[]>([]);
|
|
const [materialsLoading, setMaterialsLoading] = useState(false);
|
|
|
|
// 창고 목록 초기 로드
|
|
useEffect(() => {
|
|
(async () => {
|
|
const res = await getWarehouses();
|
|
if (res.success && res.data) {
|
|
setWarehouses(res.data);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
// 작업지시 검색
|
|
const handleSearch = useCallback(async () => {
|
|
setWorkOrdersLoading(true);
|
|
try {
|
|
const res = await getWorkOrders({
|
|
dateFrom: searchDateFrom,
|
|
dateTo: searchDateTo,
|
|
itemCode: searchItemCode || undefined,
|
|
itemName: searchItemName || undefined,
|
|
});
|
|
if (res.success && res.data) {
|
|
setWorkOrders(res.data);
|
|
setCheckedWoIds([]);
|
|
setSelectedWoId(null);
|
|
setMaterials([]);
|
|
}
|
|
} finally {
|
|
setWorkOrdersLoading(false);
|
|
}
|
|
}, [searchDateFrom, searchDateTo, searchItemCode, searchItemName]);
|
|
|
|
// 초기 로드
|
|
useEffect(() => {
|
|
handleSearch();
|
|
}, []);
|
|
|
|
const isAllChecked =
|
|
workOrders.length > 0 && checkedWoIds.length === workOrders.length;
|
|
|
|
const handleCheckAll = useCallback(
|
|
(checked: boolean) => {
|
|
setCheckedWoIds(checked ? workOrders.map((wo) => wo.id) : []);
|
|
},
|
|
[workOrders]
|
|
);
|
|
|
|
const handleCheckWo = useCallback((id: string, checked: boolean) => {
|
|
setCheckedWoIds((prev) =>
|
|
checked ? [...prev, id] : prev.filter((i) => i !== id)
|
|
);
|
|
}, []);
|
|
|
|
const handleSelectWo = useCallback((id: string) => {
|
|
setSelectedWoId((prev) => (prev === id ? null : id));
|
|
}, []);
|
|
|
|
// 선택된 작업지시의 자재 조회
|
|
const handleLoadSelectedMaterials = useCallback(async () => {
|
|
if (checkedWoIds.length === 0) {
|
|
alert("자재를 조회할 작업지시를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
setMaterialsLoading(true);
|
|
try {
|
|
const res = await getMaterialStatus({
|
|
planIds: checkedWoIds,
|
|
warehouseCode: warehouse || undefined,
|
|
});
|
|
if (res.success && res.data) {
|
|
setMaterials(res.data);
|
|
}
|
|
} finally {
|
|
setMaterialsLoading(false);
|
|
}
|
|
}, [checkedWoIds, warehouse]);
|
|
|
|
const handleResetSearch = useCallback(() => {
|
|
const t = new Date();
|
|
const m = new Date(t);
|
|
m.setMonth(t.getMonth() - 1);
|
|
setSearchDateFrom(formatDate(m));
|
|
setSearchDateTo(formatDate(t));
|
|
setSearchItemCode("");
|
|
setSearchItemName("");
|
|
setMaterialSearch("");
|
|
setShowShortageOnly(false);
|
|
}, []);
|
|
|
|
const filteredMaterials = useMemo(() => {
|
|
return materials.filter((m) => {
|
|
const searchLower = materialSearch.toLowerCase();
|
|
const matchesSearch =
|
|
!materialSearch ||
|
|
m.code.toLowerCase().includes(searchLower) ||
|
|
m.name.toLowerCase().includes(searchLower);
|
|
const matchesShortage = !showShortageOnly || m.current < m.required;
|
|
return matchesSearch && matchesShortage;
|
|
});
|
|
}, [materials, materialSearch, showShortageOnly]);
|
|
|
|
return (
|
|
<div className="flex h-[calc(100vh-4rem)] flex-col gap-3 p-3">
|
|
{/* 검색 영역 */}
|
|
<div className="shrink-0 flex flex-wrap items-end gap-3 rounded-lg border bg-card p-3">
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">기간</span>
|
|
<div className="flex items-center gap-1.5">
|
|
<Input
|
|
type="date"
|
|
className="h-9 w-[140px]"
|
|
value={searchDateFrom}
|
|
onChange={(e) => setSearchDateFrom(e.target.value)}
|
|
/>
|
|
<span className="text-muted-foreground/50 text-xs">~</span>
|
|
<Input
|
|
type="date"
|
|
className="h-9 w-[140px]"
|
|
value={searchDateTo}
|
|
onChange={(e) => setSearchDateTo(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">품목코드</span>
|
|
<Input
|
|
placeholder="품목코드"
|
|
className="h-9 w-[140px]"
|
|
value={searchItemCode}
|
|
onChange={(e) => setSearchItemCode(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">품목명</span>
|
|
<Input
|
|
placeholder="품목명"
|
|
className="h-9 w-[140px]"
|
|
value={searchItemName}
|
|
onChange={(e) => setSearchItemName(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex-1" />
|
|
|
|
<div className="flex items-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9"
|
|
onClick={handleResetSearch}
|
|
>
|
|
<RotateCcw className="mr-1 h-4 w-4" />
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
className="h-9"
|
|
onClick={handleSearch}
|
|
disabled={workOrdersLoading}
|
|
>
|
|
{workOrdersLoading ? (
|
|
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Search className="mr-1 h-4 w-4" />
|
|
)}
|
|
검색
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 메인 콘텐츠 (좌우 분할) */}
|
|
<div className="flex-1 overflow-hidden rounded-lg border bg-card">
|
|
<ResizablePanelGroup direction="horizontal">
|
|
{/* 왼쪽: 작업지시 리스트 */}
|
|
<ResizablePanel defaultSize={35} minSize={25}>
|
|
<div className="flex h-full flex-col">
|
|
{/* 패널 헤더 */}
|
|
<div className="flex items-center justify-between border-b bg-muted/30 px-3 py-2.5 shrink-0">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
checked={isAllChecked}
|
|
onCheckedChange={handleCheckAll}
|
|
/>
|
|
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-semibold">작업지시 리스트</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="rounded-full border border-primary/15 bg-primary/8 px-2 py-0.5 font-mono text-[11px] font-bold text-primary">
|
|
{workOrders.length}건
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
className="h-8"
|
|
onClick={handleLoadSelectedMaterials}
|
|
disabled={materialsLoading}
|
|
>
|
|
{materialsLoading ? (
|
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<Search className="mr-1.5 h-3.5 w-3.5" />
|
|
)}
|
|
자재조회
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-8" onClick={() => ts.setOpen(true)} title="테이블 설정">
|
|
<Settings2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 작업지시 목록 */}
|
|
<div className="flex-1 space-y-2 overflow-auto p-3">
|
|
{workOrdersLoading ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<Loader2 className="mb-3 h-8 w-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">
|
|
작업지시를 조회하고 있어요...
|
|
</p>
|
|
</div>
|
|
) : workOrders.length === 0 ? (
|
|
<div className="m-3 flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border py-12 text-center">
|
|
<Inbox className="h-10 w-10 text-muted-foreground/30" />
|
|
<p className="text-sm text-muted-foreground">
|
|
조회된 작업지시가 없어요
|
|
</p>
|
|
</div>
|
|
) : (
|
|
ts.groupData(workOrders).map((wo) => {
|
|
if ((wo as any)._isGroupSummary || (wo as any)._isGroupHeader) return null;
|
|
return (
|
|
<div
|
|
key={wo.id}
|
|
className={cn(
|
|
"flex gap-3 rounded-lg border p-3 transition-all cursor-pointer",
|
|
"hover:border-primary/50 hover:shadow-sm",
|
|
selectedWoId === wo.id
|
|
? "border-primary bg-primary/5 shadow-sm"
|
|
: "border-border"
|
|
)}
|
|
onClick={() => handleSelectWo(wo.id)}
|
|
>
|
|
<div
|
|
className="flex items-start pt-0.5"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Checkbox
|
|
checked={checkedWoIds.includes(wo.id)}
|
|
onCheckedChange={(c) =>
|
|
handleCheckWo(wo.id, c as boolean)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
<div className="flex items-center gap-2">
|
|
{ts.isVisible("plan_no") && (
|
|
<span className="text-sm font-bold text-primary">
|
|
{wo.plan_no || wo.work_order_no || `WO-${wo.id}`}
|
|
</span>
|
|
)}
|
|
{ts.isVisible("status") && (
|
|
<span
|
|
className={cn(
|
|
"ml-auto rounded-md border px-2 py-0.5 text-[11px] font-semibold",
|
|
getStatusStyle(wo.status)
|
|
)}
|
|
>
|
|
{getStatusLabel(wo.status)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
{ts.isVisible("item_name") && (
|
|
<span className="text-sm font-semibold">
|
|
{wo.item_name}
|
|
</span>
|
|
)}
|
|
{ts.isVisible("item_code") && (
|
|
<span className="text-xs text-muted-foreground">
|
|
({wo.item_code})
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
{ts.isVisible("plan_qty") && (
|
|
<>
|
|
<span>수량:</span>
|
|
<span className="font-semibold text-foreground">
|
|
{Number(wo.plan_qty).toLocaleString()}개
|
|
</span>
|
|
</>
|
|
)}
|
|
{ts.isVisible("plan_qty") && ts.isVisible("plan_date") && (
|
|
<span className="mx-1">|</span>
|
|
)}
|
|
{ts.isVisible("plan_date") && (
|
|
<>
|
|
<span>일자:</span>
|
|
<span className="font-semibold text-foreground">
|
|
{wo.plan_date
|
|
? new Date(wo.plan_date)
|
|
.toISOString()
|
|
.slice(0, 10)
|
|
: "-"}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle withHandle />
|
|
|
|
{/* 오른쪽: 원자재 현황 */}
|
|
<ResizablePanel defaultSize={65} minSize={35}>
|
|
<div className="flex h-full flex-col">
|
|
{/* 패널 헤더 */}
|
|
<div className="flex items-center gap-2 border-b bg-muted/30 px-3 py-2.5 shrink-0">
|
|
<Factory className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-semibold">원자재 재고 현황</span>
|
|
</div>
|
|
|
|
{/* 필터 */}
|
|
<div className="flex flex-wrap items-center gap-3 border-b bg-muted/30 px-4 py-2.5 shrink-0">
|
|
<Input
|
|
placeholder="원자재 검색"
|
|
className="h-8 min-w-[150px] flex-1 text-xs"
|
|
value={materialSearch}
|
|
onChange={(e) => setMaterialSearch(e.target.value)}
|
|
/>
|
|
<Select value={warehouse} onValueChange={setWarehouse}>
|
|
<SelectTrigger className="h-8 w-[180px] text-xs">
|
|
<SelectValue placeholder="전체 창고" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__all__">전체 창고</SelectItem>
|
|
{warehouses.map((wh) => (
|
|
<SelectItem
|
|
key={wh.warehouse_code}
|
|
value={wh.warehouse_code}
|
|
>
|
|
{wh.warehouse_name}
|
|
{wh.warehouse_type
|
|
? ` (${wh.warehouse_type})`
|
|
: ""}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium">
|
|
<Checkbox
|
|
checked={showShortageOnly}
|
|
onCheckedChange={(c) => setShowShortageOnly(c as boolean)}
|
|
/>
|
|
<span>부족한 것만 보기</span>
|
|
</label>
|
|
<span className="ml-auto rounded-full border border-primary/15 bg-primary/8 px-2 py-0.5 font-mono text-[11px] font-bold text-primary">
|
|
{filteredMaterials.length}개 품목
|
|
</span>
|
|
</div>
|
|
|
|
{/* 원자재 목록 */}
|
|
<div className="flex-1 space-y-2 overflow-auto p-3">
|
|
{materialsLoading ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<Loader2 className="mb-3 h-8 w-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">
|
|
자재현황을 조회하고 있어요...
|
|
</p>
|
|
</div>
|
|
) : materials.length === 0 ? (
|
|
<div className="m-3 flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border py-12 text-center">
|
|
<Inbox className="h-10 w-10 text-muted-foreground/30" />
|
|
<p className="text-sm text-muted-foreground">
|
|
작업지시를 선택하고 자재조회 버튼을 클릭해주세요
|
|
</p>
|
|
</div>
|
|
) : filteredMaterials.length === 0 ? (
|
|
<div className="m-3 flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border py-12 text-center">
|
|
<Package className="h-10 w-10 text-muted-foreground/30" />
|
|
<p className="text-sm text-muted-foreground">
|
|
조회된 원자재가 없어요
|
|
</p>
|
|
</div>
|
|
) : (
|
|
filteredMaterials.map((material) => {
|
|
const shortage = material.required - material.current;
|
|
const isShortage = shortage > 0;
|
|
const percentage =
|
|
material.required > 0
|
|
? Math.min(
|
|
(material.current / material.required) * 100,
|
|
100
|
|
)
|
|
: 100;
|
|
|
|
return (
|
|
<div
|
|
key={material.code}
|
|
className={cn(
|
|
"rounded-lg border p-3 transition-all hover:shadow-sm",
|
|
isShortage
|
|
? "border-destructive/30 bg-destructive/5"
|
|
: "border-primary/15 bg-primary/5"
|
|
)}
|
|
>
|
|
{/* 메인 정보 라인 */}
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm font-bold">
|
|
{material.name}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
({material.code})
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
필요:
|
|
</span>
|
|
<span className="text-xs font-semibold text-primary">
|
|
{material.required.toLocaleString()}
|
|
{material.unit}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
현재:
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"text-xs font-semibold",
|
|
isShortage
|
|
? "text-destructive"
|
|
: "text-foreground"
|
|
)}
|
|
>
|
|
{material.current.toLocaleString()}
|
|
{material.unit}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{isShortage ? "부족:" : "여유:"}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
"text-xs font-semibold",
|
|
isShortage
|
|
? "text-destructive"
|
|
: "text-primary"
|
|
)}
|
|
>
|
|
{Math.abs(shortage).toLocaleString()}
|
|
{material.unit}
|
|
</span>
|
|
<span className="text-xs font-semibold text-muted-foreground">
|
|
({percentage.toFixed(0)}%)
|
|
</span>
|
|
|
|
{isShortage ? (
|
|
<span className="ml-auto inline-flex items-center gap-1 rounded-md border border-destructive bg-destructive/10 px-2 py-0.5 text-[11px] font-semibold text-destructive">
|
|
<AlertTriangle className="h-3 w-3" />
|
|
부족
|
|
</span>
|
|
) : (
|
|
<span className="ml-auto inline-flex items-center gap-1 rounded-md border border-primary bg-primary/10 px-2 py-0.5 text-[11px] font-semibold text-primary">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
충분
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 위치별 재고 */}
|
|
{material.locations.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
<MapPin className="h-3.5 w-3.5 text-muted-foreground" />
|
|
{material.locations.map((loc, idx) => (
|
|
<span
|
|
key={idx}
|
|
className="inline-flex items-center gap-1 rounded bg-muted/40 px-2 py-0.5 text-xs transition-colors hover:bg-muted/60"
|
|
>
|
|
<span className="font-semibold font-mono text-primary">
|
|
{loc.location || loc.warehouse}
|
|
</span>
|
|
<span className="font-semibold">
|
|
{loc.qty.toLocaleString()}
|
|
{material.unit}
|
|
</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
|
|
<TableSettingsModal
|
|
open={ts.open}
|
|
onOpenChange={ts.setOpen}
|
|
tableName={ts.tableName}
|
|
settingsId={ts.settingsId}
|
|
defaultVisibleKeys={ts.defaultVisibleKeys}
|
|
onSave={ts.applySettings}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|