ERP-node/frontend/lib/registry/components/v2-bom-tree/BomVersionModal.tsx

254 lines
9.0 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, Plus, Trash2, Download, ShieldCheck } from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
interface BomVersion {
id: string;
version_name: string;
revision: number;
status: string;
created_by: string;
created_date: string;
}
interface BomVersionModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
bomId: string | null;
tableName?: string;
detailTable?: string;
onVersionLoaded?: () => void;
}
const STATUS_STYLE: Record<string, { label: string; className: string }> = {
developing: { label: "개발중", className: "bg-red-50 text-red-600 ring-red-200" },
active: { label: "사용", className: "bg-emerald-50 text-emerald-600 ring-emerald-200" },
inactive: { label: "사용중지", className: "bg-gray-100 text-gray-500 ring-gray-200" },
};
export function BomVersionModal({ open, onOpenChange, bomId, tableName = "bom_version", detailTable = "bom_detail", onVersionLoaded }: BomVersionModalProps) {
const [versions, setVersions] = useState<BomVersion[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [actionId, setActionId] = useState<string | null>(null);
useEffect(() => {
if (open && bomId) loadVersions();
}, [open, bomId]);
const loadVersions = async () => {
if (!bomId) return;
setLoading(true);
try {
const res = await apiClient.get(`/bom/${bomId}/versions`, { params: { tableName } });
if (res.data?.success) setVersions(res.data.data || []);
} catch (error) {
console.error("[BomVersion] 로드 실패:", error);
} finally {
setLoading(false);
}
};
const handleCreateVersion = async () => {
if (!bomId) return;
setCreating(true);
try {
const res = await apiClient.post(`/bom/${bomId}/versions`, { tableName, detailTable });
if (res.data?.success) loadVersions();
} catch (error) {
console.error("[BomVersion] 생성 실패:", error);
} finally {
setCreating(false);
}
};
const handleLoadVersion = async (versionId: string) => {
if (!bomId) return;
setActionId(versionId);
try {
const res = await apiClient.post(`/bom/${bomId}/versions/${versionId}/load`, { tableName, detailTable });
if (res.data?.success) {
onVersionLoaded?.();
loadVersions();
}
} catch (error) {
console.error("[BomVersion] 불러오기 실패:", error);
} finally {
setActionId(null);
}
};
const handleActivateVersion = async (versionId: string) => {
if (!bomId || !confirm("이 버전을 사용 확정하시겠습니까?\n기존 사용중 버전은 사용중지로 변경됩니다.")) return;
setActionId(versionId);
try {
const res = await apiClient.post(`/bom/${bomId}/versions/${versionId}/activate`, { tableName });
if (res.data?.success) {
loadVersions();
window.dispatchEvent(new CustomEvent("refreshTable"));
}
} catch (error) {
console.error("[BomVersion] 사용 확정 실패:", error);
} finally {
setActionId(null);
}
};
const handleDeleteVersion = async (versionId: string) => {
if (!bomId || !confirm("이 버전을 삭제하시겠습니까?")) return;
setActionId(versionId);
try {
const res = await apiClient.delete(`/bom/${bomId}/versions/${versionId}`, { params: { tableName } });
if (res.data?.success) loadVersions();
} catch (error) {
console.error("[BomVersion] 삭제 실패:", error);
} finally {
setActionId(null);
}
};
const formatDate = (dateStr: string) => {
if (!dateStr) return "-";
try {
return new Date(dateStr).toLocaleDateString("ko-KR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
} catch {
return dateStr;
}
};
const getStatus = (status: string) => STATUS_STYLE[status] || STATUS_STYLE.inactive;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[550px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg"> </DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
BOM . .
</DialogDescription>
</DialogHeader>
<div className="max-h-[400px] space-y-2 overflow-auto">
{loading ? (
<div className="flex h-32 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-primary" />
</div>
) : versions.length === 0 ? (
<div className="flex h-32 items-center justify-center">
<p className="text-sm text-gray-400"> </p>
</div>
) : (
versions.map((ver) => {
const st = getStatus(ver.status);
const isActing = actionId === ver.id;
return (
<div
key={ver.id}
className={cn(
"flex items-center gap-3 rounded-lg border p-3 transition-colors",
ver.status === "active" ? "border-emerald-200 bg-emerald-50/30" : "border-gray-200",
)}
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-900">
Version {ver.version_name}
</span>
<span className={cn(
"inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium ring-1 ring-inset",
st.className,
)}>
{st.label}
</span>
</div>
<div className="mt-0.5 flex gap-3 text-[11px] text-gray-400">
<span>: {ver.revision}</span>
<span>: {formatDate(ver.created_date)}</span>
{ver.created_by && <span>: {ver.created_by}</span>}
</div>
</div>
<div className="flex gap-1">
<Button
variant="outline"
size="sm"
onClick={() => handleLoadVersion(ver.id)}
disabled={isActing}
className="h-7 gap-1 px-2 text-[10px]"
>
{isActing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Download className="h-3 w-3" />}
</Button>
{ver.status === "active" ? (
<span className="flex h-7 items-center rounded-md bg-emerald-50 px-2 text-[10px] font-medium text-emerald-600 ring-1 ring-inset ring-emerald-200">
</span>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={() => handleActivateVersion(ver.id)}
disabled={isActing}
className="h-7 gap-1 px-2 text-[10px] border-emerald-300 text-emerald-600 hover:bg-emerald-50"
>
<ShieldCheck className="h-3 w-3" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDeleteVersion(ver.id)}
disabled={isActing}
className="h-7 gap-1 px-2 text-[10px]"
>
<Trash2 className="h-3 w-3" />
</Button>
</>
)}
</div>
</div>
);
})
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
onClick={handleCreateVersion}
disabled={creating}
className="h-8 flex-1 gap-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{creating ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : <Plus className="mr-1 h-4 w-4" />}
</Button>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}