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

222 lines
7.4 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogDescription } from "@/components/ui/resizable-dialog";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Loader2, AlertCircle, ArrowRight } from "lucide-react";
import { getStepDataList, moveDataToNextStep } from "@/lib/api/flow";
import { toast } from "sonner";
interface FlowDataListModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
flowId: number;
stepId: number;
stepName: string;
allowDataMove?: boolean;
onDataMoved?: () => void; // 데이터 이동 후 리프레시
}
export function FlowDataListModal({
open,
onOpenChange,
flowId,
stepId,
stepName,
allowDataMove = false,
onDataMoved,
}: FlowDataListModalProps) {
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<string[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [movingData, setMovingData] = useState(false);
// 데이터 조회
useEffect(() => {
if (!open) return;
const loadData = async () => {
try {
setLoading(true);
setError(null);
setSelectedRows(new Set());
const response = await getStepDataList(flowId, stepId, 1, 100);
if (!response.success) {
throw new Error(response.message || "데이터를 불러올 수 없습니다");
}
const rows = response.data?.records || [];
setData(rows);
// 컬럼 추출 (첫 번째 행에서)
if (rows.length > 0) {
setColumns(Object.keys(rows[0]));
} else {
setColumns([]);
}
} catch (err: any) {
console.error("Failed to load flow data:", err);
setError(err.message || "데이터를 불러오는데 실패했습니다");
} finally {
setLoading(false);
}
};
loadData();
}, [open, flowId, stepId]);
// 전체 선택/해제
const toggleAllSelection = () => {
if (selectedRows.size === data.length) {
setSelectedRows(new Set());
} else {
setSelectedRows(new Set(data.map((_, index) => index)));
}
};
// 개별 행 선택/해제
const toggleRowSelection = (index: number) => {
const newSelected = new Set(selectedRows);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
setSelectedRows(newSelected);
};
// 선택된 데이터 이동
const handleMoveData = async () => {
if (selectedRows.size === 0) {
toast.error("이동할 데이터를 선택해주세요");
return;
}
try {
setMovingData(true);
// 선택된 행의 ID 추출 (가정: 각 행에 'id' 필드가 있음)
const selectedDataIds = Array.from(selectedRows).map((index) => data[index].id);
// 데이터 이동 API 호출
for (const dataId of selectedDataIds) {
const response = await moveDataToNextStep(flowId, stepId, dataId);
if (!response.success) {
throw new Error(`데이터 이동 실패: ${response.message}`);
}
}
toast.success(`${selectedRows.size}건의 데이터를 다음 단계로 이동했습니다`);
// 모달 닫고 리프레시
onOpenChange(false);
onDataMoved?.();
} catch (err: any) {
console.error("Failed to move data:", err);
toast.error(err.message || "데이터 이동에 실패했습니다");
} finally {
setMovingData(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[80vh] max-w-4xl flex-col overflow-hidden">
<DialogHeader>
<ResizableDialogTitle className="flex items-center gap-2">
{stepName}
<Badge variant="secondary">{data.length}</Badge>
</ResizableDialogTitle>
<DialogDescription> </ResizableDialogDescription>
</DialogHeader>
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
<span className="text-muted-foreground ml-2 text-sm"> ...</span>
</div>
) : error ? (
<div className="border-destructive/50 bg-destructive/10 flex items-center gap-2 rounded-lg border p-4">
<AlertCircle className="text-destructive h-5 w-5" />
<span className="text-destructive text-sm">{error}</span>
</div>
) : data.length === 0 ? (
<div className="text-muted-foreground flex items-center justify-center py-12 text-sm">
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
{allowDataMove && (
<TableHead className="w-12">
<Checkbox
checked={selectedRows.size === data.length && data.length > 0}
onCheckedChange={toggleAllSelection}
/>
</TableHead>
)}
{columns.map((col) => (
<TableHead key={col}>{col}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
{allowDataMove && (
<TableCell>
<Checkbox
checked={selectedRows.has(index)}
onCheckedChange={() => toggleRowSelection(index)}
/>
</TableCell>
)}
{columns.map((col) => (
<TableCell key={col}>
{row[col] !== null && row[col] !== undefined ? String(row[col]) : "-"}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
<div className="flex justify-between border-t pt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
{allowDataMove && data.length > 0 && (
<Button onClick={handleMoveData} disabled={selectedRows.size === 0 || movingData}>
{movingData ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<ArrowRight className="mr-2 h-4 w-4" />
({selectedRows.size})
</>
)}
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}