"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "sonner"; import { Plus, Edit, Trash2, Search, Users, FileText, Loader2 } from "lucide-react"; import { ScrollToTop } from "@/components/common/ScrollToTop"; import { type ApprovalDefinition, type ApprovalLineTemplate, type ApprovalLineTemplateStep, getApprovalDefinitions, createApprovalDefinition, updateApprovalDefinition, deleteApprovalDefinition, getApprovalTemplates, getApprovalTemplate, createApprovalTemplate, updateApprovalTemplate, deleteApprovalTemplate, } from "@/lib/api/approval"; // ============================================================ // 결재 유형 관리 탭 // ============================================================ function DefinitionsTab() { const [definitions, setDefinitions] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [editOpen, setEditOpen] = useState(false); const [editingDef, setEditingDef] = useState(null); const [formData, setFormData] = useState({ definition_name: "", definition_name_eng: "", description: "", max_steps: 3, allow_self_approval: false, allow_cancel: true, is_active: "Y", }); const [deleteTarget, setDeleteTarget] = useState(null); const fetchDefinitions = useCallback(async () => { setLoading(true); const res = await getApprovalDefinitions({ search: searchTerm || undefined }); if (res.success && res.data) { setDefinitions(res.data); } setLoading(false); }, [searchTerm]); useEffect(() => { fetchDefinitions(); }, [fetchDefinitions]); const openCreate = () => { setEditingDef(null); setFormData({ definition_name: "", definition_name_eng: "", description: "", max_steps: 3, allow_self_approval: false, allow_cancel: true, is_active: "Y", }); setEditOpen(true); }; const openEdit = (def: ApprovalDefinition) => { setEditingDef(def); setFormData({ definition_name: def.definition_name, definition_name_eng: def.definition_name_eng || "", description: def.description || "", max_steps: def.max_steps, allow_self_approval: def.allow_self_approval, allow_cancel: def.allow_cancel, is_active: def.is_active, }); setEditOpen(true); }; const handleSave = async () => { if (!formData.definition_name.trim()) { toast.warning("결재 유형명을 입력해주세요."); return; } let res; if (editingDef) { res = await updateApprovalDefinition(editingDef.definition_id, formData); } else { res = await createApprovalDefinition(formData); } if (res.success) { toast.success(editingDef ? "수정되었습니다." : "등록되었습니다."); setEditOpen(false); fetchDefinitions(); } else { toast.error(res.error || "저장 실패"); } }; const handleDelete = async () => { if (!deleteTarget) return; const res = await deleteApprovalDefinition(deleteTarget.definition_id); if (res.success) { toast.success("삭제되었습니다."); setDeleteTarget(null); fetchDefinitions(); } else { toast.error(res.error || "삭제 실패"); } }; const filtered = definitions.filter( (d) => d.definition_name.toLowerCase().includes(searchTerm.toLowerCase()) || (d.description || "").toLowerCase().includes(searchTerm.toLowerCase()), ); return (
{/* 검색 + 등록 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
{filtered.length}
{/* 테이블 */} {loading ? (
) : filtered.length === 0 ? (

등록된 결재 유형이 없습니다.

) : (
유형명 설명 최대 단계 자가결재 회수가능 상태 관리 {filtered.map((def) => ( {def.definition_name} {def.description || "-"} {def.max_steps} {def.allow_self_approval ? "허용" : "불가"} {def.allow_cancel ? "허용" : "불가"} {def.is_active === "Y" ? "활성" : "비활성"}
))}
)} {/* 등록/수정 모달 */} {editingDef ? "결재 유형 수정" : "결재 유형 등록"} 결재 유형의 기본 정보를 설정합니다.
setFormData((p) => ({ ...p, definition_name: e.target.value }))} placeholder="예: 일반 결재, 긴급 결재" className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, definition_name_eng: e.target.value }))} placeholder="예: General Approval" className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, description: e.target.value }))} placeholder="유형에 대한 설명" className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, max_steps: Number(e.target.value) }))} className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, allow_self_approval: v }))} />
setFormData((p) => ({ ...p, allow_cancel: v }))} />
setFormData((p) => ({ ...p, is_active: v ? "Y" : "N" }))} />
{/* 삭제 확인 */} setDeleteTarget(null)}> 결재 유형 삭제 "{deleteTarget?.definition_name}"을(를) 삭제하시겠습니까?
이 유형에 연결된 결재 요청이 있으면 삭제할 수 없습니다.
취소 삭제
); } // ============================================================ // 결재선 템플릿 관리 탭 // ============================================================ function TemplatesTab() { const [templates, setTemplates] = useState([]); const [definitions, setDefinitions] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [editOpen, setEditOpen] = useState(false); const [editingTpl, setEditingTpl] = useState(null); const [formData, setFormData] = useState({ template_name: "", description: "", definition_id: null as number | null, is_active: "Y", steps: [] as Omit[], }); const [deleteTarget, setDeleteTarget] = useState(null); const fetchData = useCallback(async () => { setLoading(true); const [tplRes, defRes] = await Promise.all([ getApprovalTemplates(), getApprovalDefinitions({ is_active: "Y" }), ]); if (tplRes.success && tplRes.data) setTemplates(tplRes.data); if (defRes.success && defRes.data) setDefinitions(defRes.data); setLoading(false); }, []); useEffect(() => { fetchData(); }, [fetchData]); const openCreate = () => { setEditingTpl(null); setFormData({ template_name: "", description: "", definition_id: null, is_active: "Y", steps: [{ step_order: 1, approver_type: "user", approver_user_id: "", approver_label: "1차 결재자" }], }); setEditOpen(true); }; const openEdit = async (tpl: ApprovalLineTemplate) => { const res = await getApprovalTemplate(tpl.template_id); if (!res.success || !res.data) { toast.error("템플릿 정보를 불러올 수 없습니다."); return; } const detail = res.data; setEditingTpl(detail); setFormData({ template_name: detail.template_name, description: detail.description || "", definition_id: detail.definition_id || null, is_active: detail.is_active, steps: (detail.steps || []).map((s) => ({ step_order: s.step_order, approver_type: s.approver_type, approver_user_id: s.approver_user_id, approver_position: s.approver_position, approver_dept_code: s.approver_dept_code, approver_label: s.approver_label, })), }); setEditOpen(true); }; const addStep = () => { setFormData((p) => ({ ...p, steps: [ ...p.steps, { step_order: p.steps.length + 1, approver_type: "user" as const, approver_user_id: "", approver_label: `${p.steps.length + 1}차 결재자`, }, ], })); }; const removeStep = (idx: number) => { setFormData((p) => ({ ...p, steps: p.steps.filter((_, i) => i !== idx).map((s, i) => ({ ...s, step_order: i + 1 })), })); }; const updateStep = (idx: number, field: string, value: string) => { setFormData((p) => ({ ...p, steps: p.steps.map((s, i) => (i === idx ? { ...s, [field]: value } : s)), })); }; const handleSave = async () => { if (!formData.template_name.trim()) { toast.warning("템플릿명을 입력해주세요."); return; } if (formData.steps.length === 0) { toast.warning("결재 단계를 최소 1개 추가해주세요."); return; } const payload = { template_name: formData.template_name, description: formData.description || undefined, definition_id: formData.definition_id || undefined, is_active: formData.is_active, steps: formData.steps, }; let res; if (editingTpl) { res = await updateApprovalTemplate(editingTpl.template_id, payload); } else { res = await createApprovalTemplate(payload); } if (res.success) { toast.success(editingTpl ? "수정되었습니다." : "등록되었습니다."); setEditOpen(false); fetchData(); } else { toast.error(res.error || "저장 실패"); } }; const handleDelete = async () => { if (!deleteTarget) return; const res = await deleteApprovalTemplate(deleteTarget.template_id); if (res.success) { toast.success("삭제되었습니다."); setDeleteTarget(null); fetchData(); } else { toast.error(res.error || "삭제 실패"); } }; const filtered = templates.filter( (t) => t.template_name.toLowerCase().includes(searchTerm.toLowerCase()) || (t.description || "").toLowerCase().includes(searchTerm.toLowerCase()), ); return (
{/* 검색 + 등록 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
{filtered.length}
{/* 테이블 */} {loading ? (
) : filtered.length === 0 ? (

등록된 결재선 템플릿이 없습니다.

) : (
템플릿명 설명 연결된 유형 단계 수 상태 관리 {filtered.map((tpl) => ( {tpl.template_name} {tpl.description || "-"} {tpl.definition_name || "-"} {tpl.steps?.length || 0}단계 {tpl.is_active === "Y" ? "활성" : "비활성"}
))}
)} {/* 등록/수정 모달 */} {editingTpl ? "결재선 템플릿 수정" : "결재선 템플릿 등록"} 결재선의 기본 정보와 결재 단계를 설정합니다.
setFormData((p) => ({ ...p, template_name: e.target.value }))} placeholder="예: 일반 3단계 결재선" className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, description: e.target.value }))} placeholder="템플릿에 대한 설명" className="h-8 text-xs sm:h-10 sm:text-sm" />
setFormData((p) => ({ ...p, is_active: v ? "Y" : "N" }))} />
{/* 결재 단계 설정 */}
{formData.steps.length === 0 && (

결재 단계를 추가해주세요.

)} {formData.steps.map((step, idx) => (
{step.step_order}단계
updateStep(idx, "approver_label", e.target.value)} placeholder="예: 팀장" className="h-7 text-xs" />
{step.approver_type === "user" && (
updateStep(idx, "approver_user_id", e.target.value)} placeholder="고정 결재자 ID (비워두면 요청 시 지정)" className="h-7 text-xs" />
)} {step.approver_type === "position" && (
updateStep(idx, "approver_position", e.target.value)} placeholder="예: 부장, 이사" className="h-7 text-xs" />
)} {step.approver_type === "dept" && (
updateStep(idx, "approver_dept_code", e.target.value)} placeholder="예: DEPT001" className="h-7 text-xs" />
)}
))}
{/* 삭제 확인 */} setDeleteTarget(null)}> 결재선 템플릿 삭제 "{deleteTarget?.template_name}"을(를) 삭제하시겠습니까? 취소 삭제
); } // ============================================================ // 메인 페이지 // ============================================================ export default function ApprovalManagementPage() { return (
{/* 페이지 헤더 */}

결재 관리

결재 유형과 결재선 템플릿을 관리합니다.

{/* 탭 */} 결재 유형 결재선 템플릿
); }