314 lines
14 KiB
TypeScript
314 lines
14 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* V2 결재 단계 설정 패널
|
|
* 토스식 단계별 UX: 데이터 소스(Combobox) -> 레코드 식별(Combobox) -> 표시 설정(Switch+설명, 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>
|
|
);
|
|
};
|
|
|
|
V2ApprovalStepConfigPanel.displayName = "V2ApprovalStepConfigPanel";
|
|
|
|
export default V2ApprovalStepConfigPanel;
|