ERP-node/frontend/lib/registry/components/v2-item-routing/ItemRoutingConfigPanel.tsx

781 lines
24 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Plus, Trash2, Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { ItemRoutingConfig, ProcessColumnDef } from "./types";
import { defaultConfig } from "./config";
interface TableInfo {
tableName: string;
displayName?: string;
}
interface ColumnInfo {
columnName: string;
displayName?: string;
dataType?: string;
}
interface ScreenInfo {
screenId: number;
screenName: string;
screenCode: string;
}
// 테이블 셀렉터 Combobox
function TableSelector({
value,
onChange,
tables,
loading,
}: {
value: string;
onChange: (v: string) => void;
tables: TableInfo[];
loading: boolean;
}) {
const [open, setOpen] = useState(false);
const selected = tables.find((t) => t.tableName === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs"
disabled={loading}
>
{loading
? "로딩 중..."
: selected
? selected.displayName || selected.tableName
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs">
.
</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{tables.map((t) => (
<CommandItem
key={t.tableName}
value={`${t.displayName || ""} ${t.tableName}`}
onSelect={() => {
onChange(t.tableName);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
value === t.tableName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">
{t.displayName || t.tableName}
</span>
{t.displayName && (
<span className="text-[10px] text-muted-foreground">
{t.tableName}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// 컬럼 셀렉터 Combobox
function ColumnSelector({
value,
onChange,
tableName,
label,
}: {
value: string;
onChange: (v: string) => void;
tableName: string;
label?: string;
}) {
const [open, setOpen] = useState(false);
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!tableName) {
setColumns([]);
return;
}
const load = async () => {
setLoading(true);
try {
const { tableManagementApi } = await import(
"@/lib/api/tableManagement"
);
const res = await tableManagementApi.getColumnList(tableName);
if (res.success && res.data?.columns) {
setColumns(res.data.columns);
}
} catch {
/* ignore */
} finally {
setLoading(false);
}
};
load();
}, [tableName]);
const selected = columns.find((c) => c.columnName === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs"
disabled={loading || !tableName}
>
{loading
? "로딩..."
: !tableName
? "테이블 먼저 선택"
: selected
? selected.displayName || selected.columnName
: label || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs">
.
</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{columns.map((c) => (
<CommandItem
key={c.columnName}
value={`${c.displayName || ""} ${c.columnName}`}
onSelect={() => {
onChange(c.columnName);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
value === c.columnName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">
{c.displayName || c.columnName}
</span>
{c.displayName && (
<span className="text-[10px] text-muted-foreground">
{c.columnName}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// 화면 셀렉터 Combobox
function ScreenSelector({
value,
onChange,
}: {
value?: number;
onChange: (v?: number) => void;
}) {
const [open, setOpen] = useState(false);
const [screens, setScreens] = useState<ScreenInfo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const { screenApi } = await import("@/lib/api/screen");
const res = await screenApi.getScreens({ page: 1, size: 1000 });
setScreens(
res.data.map((s: any) => ({
screenId: s.screenId,
screenName: s.screenName,
screenCode: s.screenCode,
}))
);
} catch {
/* ignore */
} finally {
setLoading(false);
}
};
load();
}, []);
const selected = screens.find((s) => s.screenId === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs"
disabled={loading}
>
{loading
? "로딩 중..."
: selected
? `${selected.screenName} (${selected.screenId})`
: "화면 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0" align="start">
<Command>
<CommandInput placeholder="화면 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-4 text-center text-xs">
.
</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-auto">
{screens.map((s) => (
<CommandItem
key={s.screenId}
value={`${s.screenName.toLowerCase()} ${s.screenCode.toLowerCase()} ${s.screenId}`}
onSelect={() => {
onChange(s.screenId === value ? undefined : s.screenId);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
value === s.screenId ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{s.screenName}</span>
<span className="text-[10px] text-muted-foreground">
{s.screenCode} (ID: {s.screenId})
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// 공정 테이블 컬럼 셀렉터 (routingDetailTable의 컬럼 목록에서 선택)
function ProcessColumnSelector({
value,
onChange,
tableName,
processTable,
}: {
value: string;
onChange: (v: string) => void;
tableName: string;
processTable: string;
}) {
const [open, setOpen] = useState(false);
const [columns, setColumns] = useState<ColumnInfo[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadAll = async () => {
if (!tableName) return;
setLoading(true);
try {
const { tableManagementApi } = await import(
"@/lib/api/tableManagement"
);
const res = await tableManagementApi.getColumnList(tableName);
const cols: ColumnInfo[] = [];
if (res.success && res.data?.columns) {
cols.push(...res.data.columns);
}
if (processTable && processTable !== tableName) {
const res2 = await tableManagementApi.getColumnList(processTable);
if (res2.success && res2.data?.columns) {
cols.push(
...res2.data.columns.map((c: any) => ({
...c,
columnName: c.columnName,
displayName: `[${processTable}] ${c.displayName || c.columnName}`,
}))
);
}
}
setColumns(cols);
} catch {
/* ignore */
} finally {
setLoading(false);
}
};
loadAll();
}, [tableName, processTable]);
const selected = columns.find((c) => c.columnName === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 w-24 justify-between text-[10px]"
disabled={loading}
>
{selected ? selected.displayName || selected.columnName : value || "선택"}
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-3 text-center text-xs">
</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{columns.map((c) => (
<CommandItem
key={c.columnName}
value={`${c.displayName || ""} ${c.columnName}`}
onSelect={() => {
onChange(c.columnName);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-1 h-3 w-3",
value === c.columnName ? "opacity-100" : "opacity-0"
)}
/>
{c.displayName || c.columnName}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
interface ConfigPanelProps {
config: Partial<ItemRoutingConfig>;
onChange: (config: Partial<ItemRoutingConfig>) => void;
}
export function ItemRoutingConfigPanel({
config: configProp,
onChange,
}: ConfigPanelProps) {
const config: ItemRoutingConfig = {
...defaultConfig,
...configProp,
dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource },
modals: { ...defaultConfig.modals, ...configProp?.modals },
processColumns: configProp?.processColumns?.length
? configProp.processColumns
: defaultConfig.processColumns,
};
const [allTables, setAllTables] = useState<TableInfo[]>([]);
const [tablesLoading, setTablesLoading] = useState(false);
useEffect(() => {
const load = async () => {
setTablesLoading(true);
try {
const { tableManagementApi } = await import(
"@/lib/api/tableManagement"
);
const res = await tableManagementApi.getTableList();
if (res.success && res.data) {
setAllTables(res.data);
}
} catch {
/* ignore */
} finally {
setTablesLoading(false);
}
};
load();
}, []);
const update = (partial: Partial<ItemRoutingConfig>) => {
onChange({ ...configProp, ...partial });
};
const updateDataSource = (field: string, value: string) => {
update({ dataSource: { ...config.dataSource, [field]: value } });
};
const updateModals = (field: string, value: number | undefined) => {
update({ modals: { ...config.modals, [field]: value } });
};
// 컬럼 관리
const addColumn = () => {
update({
processColumns: [
...config.processColumns,
{ name: "", label: "새 컬럼", width: 100 },
],
});
};
const removeColumn = (idx: number) => {
update({
processColumns: config.processColumns.filter((_, i) => i !== idx),
});
};
const updateColumn = (
idx: number,
field: keyof ProcessColumnDef,
value: any
) => {
const next = [...config.processColumns];
next[idx] = { ...next[idx], [field]: value };
update({ processColumns: next });
};
return (
<div className="space-y-5 p-4">
<h3 className="text-sm font-semibold"> </h3>
{/* 데이터 소스 설정 */}
<section className="space-y-3">
<p className="text-xs font-medium text-muted-foreground">
</p>
<div>
<Label className="text-xs"> </Label>
<TableSelector
value={config.dataSource.itemTable}
onChange={(v) => updateDataSource("itemTable", v)}
tables={allTables}
loading={tablesLoading}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"> </Label>
<ColumnSelector
value={config.dataSource.itemNameColumn}
onChange={(v) => updateDataSource("itemNameColumn", v)}
tableName={config.dataSource.itemTable}
label="품목명"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<ColumnSelector
value={config.dataSource.itemCodeColumn}
onChange={(v) => updateDataSource("itemCodeColumn", v)}
tableName={config.dataSource.itemTable}
label="품목코드"
/>
</div>
</div>
<div>
<Label className="text-xs"> </Label>
<TableSelector
value={config.dataSource.routingVersionTable}
onChange={(v) => updateDataSource("routingVersionTable", v)}
tables={allTables}
loading={tablesLoading}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"> FK </Label>
<ColumnSelector
value={config.dataSource.routingVersionFkColumn}
onChange={(v) => updateDataSource("routingVersionFkColumn", v)}
tableName={config.dataSource.routingVersionTable}
label="FK 컬럼"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<ColumnSelector
value={config.dataSource.routingVersionNameColumn}
onChange={(v) =>
updateDataSource("routingVersionNameColumn", v)
}
tableName={config.dataSource.routingVersionTable}
label="버전명"
/>
</div>
</div>
<div>
<Label className="text-xs"> </Label>
<TableSelector
value={config.dataSource.routingDetailTable}
onChange={(v) => updateDataSource("routingDetailTable", v)}
tables={allTables}
loading={tablesLoading}
/>
</div>
<div>
<Label className="text-xs"> FK </Label>
<ColumnSelector
value={config.dataSource.routingDetailFkColumn}
onChange={(v) => updateDataSource("routingDetailFkColumn", v)}
tableName={config.dataSource.routingDetailTable}
label="FK 컬럼"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<TableSelector
value={config.dataSource.processTable}
onChange={(v) => updateDataSource("processTable", v)}
tables={allTables}
loading={tablesLoading}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"> </Label>
<ColumnSelector
value={config.dataSource.processNameColumn}
onChange={(v) => updateDataSource("processNameColumn", v)}
tableName={config.dataSource.processTable}
label="공정명"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<ColumnSelector
value={config.dataSource.processCodeColumn}
onChange={(v) => updateDataSource("processCodeColumn", v)}
tableName={config.dataSource.processTable}
label="공정코드"
/>
</div>
</div>
</section>
{/* 모달 설정 */}
<section className="space-y-3">
<p className="text-xs font-medium text-muted-foreground"> </p>
<div>
<Label className="text-xs"> </Label>
<ScreenSelector
value={config.modals.versionAddScreenId}
onChange={(v) => updateModals("versionAddScreenId", v)}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<ScreenSelector
value={config.modals.processAddScreenId}
onChange={(v) => updateModals("processAddScreenId", v)}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<ScreenSelector
value={config.modals.processEditScreenId}
onChange={(v) => updateModals("processEditScreenId", v)}
/>
</div>
</section>
{/* 공정 테이블 컬럼 설정 */}
<section className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">
</p>
<Button
variant="outline"
size="sm"
className="h-6 gap-1 text-[10px]"
onClick={addColumn}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="space-y-1.5">
{config.processColumns.map((col, idx) => (
<div
key={idx}
className="flex items-center gap-1.5 rounded border bg-muted/30 p-1.5"
>
<ProcessColumnSelector
value={col.name}
onChange={(v) => updateColumn(idx, "name", v)}
tableName={config.dataSource.routingDetailTable}
processTable={config.dataSource.processTable}
/>
<Input
value={col.label}
onChange={(e) => updateColumn(idx, "label", e.target.value)}
className="h-7 flex-1 text-[10px]"
placeholder="표시명"
/>
<Input
type="number"
value={col.width || ""}
onChange={(e) =>
updateColumn(
idx,
"width",
e.target.value ? Number(e.target.value) : undefined
)
}
className="h-7 w-14 text-[10px]"
placeholder="너비"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 text-destructive hover:text-destructive"
onClick={() => removeColumn(idx)}
disabled={config.processColumns.length <= 1}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</div>
</section>
{/* UI 설정 */}
<section className="space-y-3">
<p className="text-xs font-medium text-muted-foreground">UI </p>
<div>
<Label className="text-xs"> (%)</Label>
<Input
type="number"
value={config.splitRatio || 40}
onChange={(e) => update({ splitRatio: Number(e.target.value) })}
min={20}
max={60}
className="mt-1 h-8 w-20 text-xs"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={config.leftPanelTitle || ""}
onChange={(e) => update({ leftPanelTitle: e.target.value })}
className="mt-1 h-8 text-xs"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={config.rightPanelTitle || ""}
onChange={(e) => update({ rightPanelTitle: e.target.value })}
className="mt-1 h-8 text-xs"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={config.versionAddButtonText || ""}
onChange={(e) => update({ versionAddButtonText: e.target.value })}
className="mt-1 h-8 text-xs"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Input
value={config.processAddButtonText || ""}
onChange={(e) => update({ processAddButtonText: e.target.value })}
className="mt-1 h-8 text-xs"
/>
</div>
<div className="flex items-center gap-2">
<Switch
checked={config.autoSelectFirstVersion ?? true}
onCheckedChange={(v) => update({ autoSelectFirstVersion: v })}
/>
<Label className="text-xs"> </Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={config.readonly || false}
onCheckedChange={(v) => update({ readonly: v })}
/>
<Label className="text-xs"> </Label>
</div>
</section>
</div>
);
}