forked from baron/baron-sso
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import type { AxiosError } from "axios";
|
|
import { Download, FileText, Loader2, Upload } from "lucide-react";
|
|
import * as React from "react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "../../../components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "../../../components/ui/dialog";
|
|
import { importOrgChart } from "../../../lib/adminApi";
|
|
import { t } from "../../../lib/i18n";
|
|
|
|
interface OrgChartUploadModalProps {
|
|
tenantId: string;
|
|
onSuccess?: () => void;
|
|
}
|
|
|
|
export function OrgChartUploadModal({
|
|
tenantId,
|
|
onSuccess,
|
|
}: OrgChartUploadModalProps) {
|
|
const [open, setOpen] = React.useState(false);
|
|
const [file, setFile] = React.useState<File | null>(null);
|
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (file: File) => importOrgChart(tenantId, file),
|
|
onSuccess: () => {
|
|
toast.success(
|
|
t(
|
|
"msg.admin.org.import_success",
|
|
"조직도가 성공적으로 업로드되었습니다.",
|
|
),
|
|
);
|
|
setOpen(false);
|
|
onSuccess?.();
|
|
},
|
|
onError: (error: AxiosError<{ error?: string }>) => {
|
|
toast.error(t("msg.admin.org.import_error", "조직도 업로드 실패"), {
|
|
description: error.response?.data?.error || error.message,
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFile = e.target.files?.[0];
|
|
if (selectedFile) {
|
|
setFile(selectedFile);
|
|
}
|
|
};
|
|
|
|
const handleUpload = () => {
|
|
if (file) {
|
|
mutation.mutate(file);
|
|
}
|
|
};
|
|
|
|
const downloadTemplate = () => {
|
|
const headers = "email,name,organization,position,jobtitle,is_owner";
|
|
const example = `ceo@example.com,홍길동,경영진,대표이사,경영총괄,true
|
|
cto@example.com,이몽룡,기술부문,이사,기술총괄,true
|
|
user1@example.com,성춘향,기술부문/개발팀,팀원,프론트엔드 개발,false`;
|
|
const blob = new Blob(
|
|
[
|
|
`${headers}
|
|
${example}`,
|
|
],
|
|
{ type: "text/csv" },
|
|
);
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "org_chart_template.csv";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm" className="gap-2">
|
|
<Upload size={14} />
|
|
{t("ui.admin.org.import_btn", "조직도 임포트 (CSV)")}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{t("ui.admin.org.import_title", "조직도 일괄 등록")}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t(
|
|
"msg.admin.org.import_description",
|
|
"CSV 파일을 업로드하여 조직 계층과 멤버를 한 번에 구성합니다.",
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<div className="flex justify-between items-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={downloadTemplate}
|
|
className="gap-2"
|
|
>
|
|
<Download size={14} />
|
|
{t("ui.admin.org.download_template", "템플릿 다운로드")}
|
|
</Button>
|
|
<input
|
|
type="file"
|
|
accept=".csv"
|
|
className="hidden"
|
|
ref={fileInputRef}
|
|
onChange={handleFileChange}
|
|
/>
|
|
<Button
|
|
onClick={() => fileInputRef.current?.click()}
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
{file
|
|
? t("ui.common.change_file", "파일 변경")
|
|
: t("ui.common.select_file", "파일 선택")}
|
|
</Button>
|
|
</div>
|
|
|
|
{file && (
|
|
<div className="rounded-lg border p-4 bg-muted/20 flex items-center gap-3">
|
|
<FileText className="text-primary" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium truncate">{file.name}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{(file.size / 1024).toFixed(1)} KB
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
onClick={handleUpload}
|
|
disabled={!file || mutation.isPending}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
{mutation.isPending && (
|
|
<Loader2 size={16} className="mr-2 animate-spin" />
|
|
)}
|
|
{t("ui.admin.org.start_import", "임포트 시작")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|