ERP-node/frontend/components/flow/FlowStepPanel.tsx

440 lines
17 KiB
TypeScript
Raw Normal View History

2025-10-20 10:55:33 +09:00
/**
*
*
*/
import { useState, useEffect } from "react";
import { X, Trash2, Save, Check, ChevronsUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2025-10-20 15:53:00 +09:00
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
2025-10-20 10:55:33 +09:00
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useToast } from "@/hooks/use-toast";
import { updateFlowStep, deleteFlowStep } from "@/lib/api/flow";
import { FlowStep } from "@/types/flow";
import { FlowConditionBuilder } from "./FlowConditionBuilder";
2025-10-20 15:53:00 +09:00
import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement";
2025-10-20 10:55:33 +09:00
import { cn } from "@/lib/utils";
interface FlowStepPanelProps {
step: FlowStep;
flowId: number;
onClose: () => void;
onUpdate: () => void;
}
export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanelProps) {
const { toast } = useToast();
const [formData, setFormData] = useState({
stepName: step.stepName,
tableName: step.tableName || "",
conditionJson: step.conditionJson,
2025-10-20 15:53:00 +09:00
// 하이브리드 모드 필드
moveType: step.moveType || "status",
statusColumn: step.statusColumn || "",
statusValue: step.statusValue || "",
targetTable: step.targetTable || "",
fieldMappings: step.fieldMappings || {},
2025-10-20 10:55:33 +09:00
});
const [tableList, setTableList] = useState<any[]>([]);
const [loadingTables, setLoadingTables] = useState(true);
const [openTableCombobox, setOpenTableCombobox] = useState(false);
2025-10-20 15:53:00 +09:00
// 컬럼 목록 (상태 컬럼 선택용)
const [columns, setColumns] = useState<any[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [openStatusColumnCombobox, setOpenStatusColumnCombobox] = useState(false);
2025-10-20 10:55:33 +09:00
// 테이블 목록 조회
useEffect(() => {
const loadTables = async () => {
try {
setLoadingTables(true);
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setTableList(response.data);
}
} catch (error) {
console.error("Failed to load tables:", error);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
useEffect(() => {
setFormData({
stepName: step.stepName,
tableName: step.tableName || "",
conditionJson: step.conditionJson,
2025-10-20 15:53:00 +09:00
// 하이브리드 모드 필드
moveType: step.moveType || "status",
statusColumn: step.statusColumn || "",
statusValue: step.statusValue || "",
targetTable: step.targetTable || "",
fieldMappings: step.fieldMappings || {},
2025-10-20 10:55:33 +09:00
});
}, [step]);
2025-10-20 15:53:00 +09:00
// 테이블 선택 시 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!formData.tableName) {
setColumns([]);
return;
}
try {
setLoadingColumns(true);
console.log("🔍 Loading columns for status column selector:", formData.tableName);
const response = await getTableColumns(formData.tableName);
console.log("📦 Columns response:", response);
if (response.success && response.data && response.data.columns) {
console.log("✅ Setting columns:", response.data.columns);
setColumns(response.data.columns);
} else {
console.log("❌ No columns in response");
setColumns([]);
}
} catch (error) {
console.error("Failed to load columns:", error);
setColumns([]);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
}, [formData.tableName]);
2025-10-20 10:55:33 +09:00
// 저장
const handleSave = async () => {
try {
const response = await updateFlowStep(step.id, formData);
if (response.success) {
toast({
title: "저장 완료",
description: "단계가 수정되었습니다.",
});
onUpdate();
onClose();
} else {
toast({
title: "저장 실패",
description: response.error,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
};
// 삭제
const handleDelete = async () => {
if (!confirm(`"${step.stepName}" 단계를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
return;
}
try {
const response = await deleteFlowStep(step.id);
if (response.success) {
toast({
title: "삭제 완료",
description: "단계가 삭제되었습니다.",
});
onUpdate();
onClose();
} else {
toast({
title: "삭제 실패",
description: response.error,
variant: "destructive",
});
}
} catch (error: any) {
toast({
title: "오류 발생",
description: error.message,
variant: "destructive",
});
}
};
return (
<div className="fixed top-0 right-0 z-50 h-full w-96 overflow-y-auto border-l bg-white shadow-xl">
<div className="space-y-6 p-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold"> </h2>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
{/* 기본 정보 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label> </Label>
<Input
value={formData.stepName}
onChange={(e) => setFormData({ ...formData, stepName: e.target.value })}
placeholder="단계 이름 입력"
/>
</div>
<div>
<Label> </Label>
<Input value={step.stepOrder} disabled />
</div>
<div>
<Label> </Label>
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openTableCombobox}
className="w-full justify-between"
disabled={loadingTables}
>
{formData.tableName
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
formData.tableName
: loadingTables
? "로딩 중..."
: "테이블 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput placeholder="테이블 검색..." />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
{tableList.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
setFormData({ ...formData, tableName: currentValue });
setOpenTableCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.displayName || table.tableName}</span>
{table.description && <span className="text-xs text-gray-500">{table.description}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</CardContent>
</Card>
{/* 조건 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
{!formData.tableName ? (
<div className="py-8 text-center text-gray-500"> </div>
) : (
<FlowConditionBuilder
flowId={flowId}
tableName={formData.tableName}
condition={formData.conditionJson}
onChange={(condition) => setFormData({ ...formData, conditionJson: condition })}
/>
)}
</CardContent>
</Card>
2025-10-20 15:53:00 +09:00
{/* 데이터 이동 설정 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 이동 방식 선택 */}
<div>
<Label> </Label>
<Select
value={formData.moveType}
onValueChange={(value: "status" | "table" | "both") => setFormData({ ...formData, moveType: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="status">
<div>
<div className="font-medium"> </div>
<div className="text-xs text-gray-500"> </div>
</div>
</SelectItem>
<SelectItem value="table">
<div>
<div className="font-medium"> </div>
<div className="text-xs text-gray-500"> </div>
</div>
</SelectItem>
<SelectItem value="both">
<div>
<div className="font-medium"></div>
<div className="text-xs text-gray-500"> + </div>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 상태 변경 설정 (status 또는 both일 때) */}
{(formData.moveType === "status" || formData.moveType === "both") && (
<>
<div>
<Label> </Label>
<Popover open={openStatusColumnCombobox} onOpenChange={setOpenStatusColumnCombobox}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openStatusColumnCombobox}
className="w-full justify-between"
disabled={!formData.tableName || loadingColumns}
>
{loadingColumns
? "컬럼 로딩 중..."
: formData.statusColumn
? columns.find((col) => col.columnName === formData.statusColumn)?.columnName ||
formData.statusColumn
: "상태 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandList>
<CommandEmpty> .</CommandEmpty>
<CommandGroup>
{columns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={() => {
setFormData({ ...formData, statusColumn: column.columnName });
setOpenStatusColumnCombobox(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
formData.statusColumn === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
<div>
<div>{column.columnName}</div>
<div className="text-xs text-gray-500">({column.dataType})</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
<div>
<Label> </Label>
<Input
value={formData.statusValue}
onChange={(e) => setFormData({ ...formData, statusValue: e.target.value })}
placeholder="예: approved"
/>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</>
)}
{/* 테이블 이동 설정 (table 또는 both일 때) */}
{(formData.moveType === "table" || formData.moveType === "both") && (
<>
<div>
<Label> </Label>
<Select
value={formData.targetTable}
onValueChange={(value) => setFormData({ ...formData, targetTable: value })}
>
<SelectTrigger>
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tableList.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.displayName || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
<div className="rounded-md bg-blue-50 p-3">
<p className="text-sm text-blue-900">
💡 . .
</p>
</div>
</>
)}
</CardContent>
</Card>
2025-10-20 10:55:33 +09:00
{/* 액션 버튼 */}
<div className="flex gap-2">
<Button className="flex-1" onClick={handleSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
<Button variant="destructive" onClick={handleDelete}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}