ERP-node/frontend/components/v2/config-panels/V2ApprovalStepConfigPanel.tsx

380 lines
15 KiB
TypeScript

"use client";
/**
* V2 결재 단계 설정 패널
* 토스식 단계별 UX: 데이터 소스(Combobox) -> 표시 모드 -> 표시 옵션(Collapsible)
*/
import React, { useState, useEffect } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown, Database, ChevronDown, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import { tableTypeApi } from "@/lib/api/screen";
import { tableManagementApi } from "@/lib/api/tableManagement";
import type { ApprovalStepConfig } from "@/lib/registry/components/v2-approval-step/types";
interface V2ApprovalStepConfigPanelProps {
config: ApprovalStepConfig;
onChange: (config: Partial<ApprovalStepConfig>) => void;
screenTableName?: string;
}
export const V2ApprovalStepConfigPanel: React.FC<V2ApprovalStepConfigPanelProps> = ({
config,
onChange,
screenTableName,
}) => {
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableOpen, setTableOpen] = useState(false);
const [availableColumns, setAvailableColumns] = useState<Array<{ columnName: string; label: string }>>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [columnOpen, setColumnOpen] = useState(false);
const [displayOpen, setDisplayOpen] = useState(false);
const targetTableName = config.targetTable || screenTableName;
const handleChange = (key: keyof ApprovalStepConfig, value: any) => {
onChange({ [key]: value });
};
useEffect(() => {
const fetchTables = async () => {
setLoadingTables(true);
try {
const response = await tableTypeApi.getTables();
setAvailableTables(
response.map((table: any) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName,
}))
);
} catch {
/* ignore */
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
useEffect(() => {
if (!targetTableName) {
setAvailableColumns([]);
return;
}
const fetchColumns = async () => {
setLoadingColumns(true);
try {
const result = await tableManagementApi.getColumnList(targetTableName);
if (result.success && result.data) {
const columns = Array.isArray(result.data) ? result.data : result.data.columns;
if (columns && Array.isArray(columns)) {
setAvailableColumns(
columns.map((col: any) => ({
columnName: col.columnName || col.column_name || col.name,
label:
col.displayName ||
col.columnLabel ||
col.column_label ||
col.columnName ||
col.column_name ||
col.name,
}))
);
}
}
} catch {
setAvailableColumns([]);
} finally {
setLoadingColumns(false);
}
};
fetchColumns();
}, [targetTableName]);
const handleTableChange = (newTableName: string) => {
if (newTableName === targetTableName) return;
handleChange("targetTable", newTableName);
handleChange("targetRecordIdField", "");
setTableOpen(false);
};
return (
<div className="space-y-4">
{/* ─── 1단계: 데이터 소스 ─── */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium"> </p>
</div>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
{/* 대상 테이블 */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> </span>
<Popover open={tableOpen} onOpenChange={setTableOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableOpen}
className="h-7 w-full justify-between text-xs"
disabled={loadingTables}
>
{loadingTables
? "로딩 중..."
: targetTableName
? availableTables.find((t) => t.tableName === targetTableName)?.displayName ||
targetTableName
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
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">
{availableTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.displayName}`}
onSelect={() => handleTableChange(table.tableName)}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
targetTableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.displayName}</span>
{table.displayName !== table.tableName && (
<span className="text-[10px] text-muted-foreground">
{table.tableName}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{screenTableName && targetTableName !== screenTableName && (
<div className="mt-1 flex items-center justify-between rounded-md bg-amber-50 px-2 py-1">
<span className="text-[10px] text-amber-700">
({screenTableName})
</span>
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-[10px] text-amber-700 hover:text-amber-900"
onClick={() => handleTableChange(screenTableName)}
>
</Button>
</div>
)}
</div>
{/* 레코드 ID 필드 */}
<div className="space-y-1">
<span className="text-xs text-muted-foreground"> ID </span>
{targetTableName ? (
<Popover open={columnOpen} onOpenChange={setColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={columnOpen}
className="h-7 w-full justify-between text-xs"
disabled={loadingColumns}
>
{loadingColumns
? "컬럼 로딩 중..."
: config.targetRecordIdField
? availableColumns.find((c) => c.columnName === config.targetRecordIdField)
?.label || config.targetRecordIdField
: "PK 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
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">
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={`${col.columnName} ${col.label}`}
onSelect={() => {
handleChange("targetRecordIdField", col.columnName);
setColumnOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.targetRecordIdField === col.columnName
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.label}</span>
{col.label !== col.columnName && (
<span className="text-[10px] text-muted-foreground">
{col.columnName}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<p className="text-[10px] text-muted-foreground"> </p>
)}
<p className="text-[10px] text-muted-foreground mt-0.5">
PK
</p>
</div>
</div>
{/* ─── 2단계: 표시 모드 ─── */}
<div className="space-y-2">
<p className="text-sm font-medium"> </p>
<p className="text-[11px] text-muted-foreground"> </p>
</div>
<div className="rounded-lg border bg-muted/30 p-4">
<Select
value={config.displayMode || "horizontal"}
onValueChange={(v) => handleChange("displayMode", v)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="horizontal"> </SelectItem>
<SelectItem value="vertical"> </SelectItem>
</SelectContent>
</Select>
</div>
{/* ─── 3단계: 표시 옵션 (Collapsible) ─── */}
<Collapsible open={displayOpen} onOpenChange={setDisplayOpen}>
<CollapsibleTrigger asChild>
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium"> </span>
</div>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-200",
displayOpen && "rotate-180"
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="rounded-b-lg border border-t-0 p-4 space-y-3">
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm">/ </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<Switch
checked={config.showDept !== false}
onCheckedChange={(checked) => handleChange("showDept", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<Switch
checked={config.showComment !== false}
onCheckedChange={(checked) => handleChange("showComment", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<Switch
checked={config.showTimestamp !== false}
onCheckedChange={(checked) => handleChange("showTimestamp", checked)}
/>
</div>
<div className="flex items-center justify-between py-1">
<div>
<p className="text-sm"> </p>
<p className="text-[11px] text-muted-foreground">
</p>
</div>
<Switch
checked={config.compact || false}
onCheckedChange={(checked) => handleChange("compact", checked)}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};
export default V2ApprovalStepConfigPanel;