ERP-node/frontend/components/admin/MenuCopyDialog.tsx

345 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { menuApi, MenuCopyResult } from "@/lib/api/menu";
import { apiClient } from "@/lib/api/client";
interface MenuCopyDialogProps {
menuObjid: number | null;
menuName: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onCopyComplete?: () => void;
}
interface Company {
company_code: string;
company_name: string;
}
export function MenuCopyDialog({
menuObjid,
menuName,
open,
onOpenChange,
onCopyComplete,
}: MenuCopyDialogProps) {
const [targetCompanyCode, setTargetCompanyCode] = useState("");
const [companies, setCompanies] = useState<Company[]>([]);
const [copying, setCopying] = useState(false);
const [result, setResult] = useState<MenuCopyResult | null>(null);
const [loadingCompanies, setLoadingCompanies] = useState(false);
// 화면명 일괄 변경 설정
const [useBulkRename, setUseBulkRename] = useState(false);
const [removeText, setRemoveText] = useState("");
const [addPrefix, setAddPrefix] = useState("");
// 회사 목록 로드
useEffect(() => {
if (open) {
loadCompanies();
// 다이얼로그가 열릴 때마다 초기화
setTargetCompanyCode("");
setResult(null);
setUseBulkRename(false);
setRemoveText("");
setAddPrefix("");
}
}, [open]);
const loadCompanies = async () => {
try {
setLoadingCompanies(true);
const response = await apiClient.get("/admin/companies/db");
if (response.data.success && response.data.data) {
// 최고 관리자(*) 회사 제외
const filteredCompanies = response.data.data.filter(
(company: Company) => company.company_code !== "*"
);
setCompanies(filteredCompanies);
}
} catch (error) {
console.error("회사 목록 조회 실패:", error);
toast.error("회사 목록을 불러올 수 없습니다");
} finally {
setLoadingCompanies(false);
}
};
const handleCopy = async () => {
if (!menuObjid) {
toast.error("메뉴를 선택해주세요");
return;
}
if (!targetCompanyCode) {
toast.error("대상 회사를 선택해주세요");
return;
}
setCopying(true);
setResult(null);
try {
// 화면명 변환 설정 (사용 중일 때만 전달)
const screenNameConfig =
useBulkRename && (removeText.trim() || addPrefix.trim())
? {
removeText: removeText.trim() || undefined,
addPrefix: addPrefix.trim() || undefined,
}
: undefined;
const response = await menuApi.copyMenu(
menuObjid,
targetCompanyCode,
screenNameConfig
);
if (response.success && response.data) {
setResult(response.data);
toast.success("메뉴 복사 완료!");
// 경고 메시지 표시
if (response.data.warnings && response.data.warnings.length > 0) {
response.data.warnings.forEach((warning) => {
toast.warning(warning);
});
}
// 복사 완료 콜백
if (onCopyComplete) {
onCopyComplete();
}
} else {
toast.error(response.message || "메뉴 복사 실패");
}
} catch (error: any) {
console.error("메뉴 복사 오류:", error);
toast.error(error.message || "메뉴 복사 중 오류가 발생했습니다");
} finally {
setCopying(false);
}
};
const handleClose = () => {
if (!copying) {
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
"{menuName}" .
</DialogDescription>
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 회사 선택 */}
{!result && (
<div>
<Label htmlFor="company" className="text-xs sm:text-sm">
*
</Label>
<Select
value={targetCompanyCode}
onValueChange={setTargetCompanyCode}
disabled={copying || loadingCompanies}
>
<SelectTrigger
id="company"
className="h-8 text-xs sm:h-10 sm:text-sm"
>
<SelectValue placeholder="회사 선택" />
</SelectTrigger>
<SelectContent>
{loadingCompanies ? (
<div className="flex items-center justify-center p-2 text-xs text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</div>
) : companies.length === 0 ? (
<div className="p-2 text-xs text-muted-foreground text-center">
</div>
) : (
companies.map((company) => (
<SelectItem
key={company.company_code}
value={company.company_code}
className="text-xs sm:text-sm"
>
{company.company_name} ({company.company_code})
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
)}
{/* 화면명 일괄 변경 설정 */}
{!result && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Checkbox
id="useBulkRename"
checked={useBulkRename}
onCheckedChange={(checked) => setUseBulkRename(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="useBulkRename"
className="text-xs sm:text-sm font-medium cursor-pointer"
>
</Label>
</div>
{useBulkRename && (
<div className="space-y-3 pl-6 border-l-2">
<div>
<Label htmlFor="removeText" className="text-xs sm:text-sm">
</Label>
<Input
id="removeText"
value={removeText}
onChange={(e) => setRemoveText(e.target.value)}
placeholder="예: 탑씰"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "탑씰 회사정보" "회사정보")
</p>
</div>
<div>
<Label htmlFor="addPrefix" className="text-xs sm:text-sm">
</Label>
<Input
id="addPrefix"
value={addPrefix}
onChange={(e) => setAddPrefix(e.target.value)}
placeholder="예: 한신"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "회사정보" "한신 회사정보")
</p>
</div>
</div>
)}
</div>
)}
{/* 복사 항목 안내 */}
{!result && (
<div className="rounded-md border p-3 text-xs">
<p className="font-medium mb-2"> :</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li> ( )</li>
<li> + (, )</li>
<li> (, )</li>
<li> + </li>
<li> + </li>
</ul>
<p className="mt-2 text-warning">
.
</p>
</div>
)}
{/* 복사 결과 */}
{result && (
<div className="rounded-md border border-success bg-success/10 p-3 text-xs space-y-2">
<p className="font-medium text-success"> !</p>
<div className="grid grid-cols-2 gap-2">
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedMenus}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedScreens}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedFlows}</span>
</div>
<div>
<span className="text-muted-foreground"> :</span>{" "}
<span className="font-medium">{result.copiedCategories}</span>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">:</span>{" "}
<span className="font-medium">{result.copiedCodes}</span>
</div>
</div>
</div>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={handleClose}
disabled={copying}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{result ? "닫기" : "취소"}
</Button>
{!result && (
<Button
onClick={handleCopy}
disabled={copying || !targetCompanyCode || loadingCompanies}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
{copying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
"복사 시작"
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}