"use client"; import React, { useState, useEffect, useCallback, useRef } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Plus, X, Loader2, Search, GripVertical, Users, ArrowDown, Layers } from "lucide-react"; import { toast } from "sonner"; import { createApprovalRequest } from "@/lib/api/approval"; import { getUserList } from "@/lib/api/user"; // 결재 방식 type ApprovalMode = "sequential" | "parallel"; interface ApproverRow { id: string; user_id: string; user_name: string; position_name: string; dept_name: string; } export interface ApprovalModalEventDetail { targetTable: string; targetRecordId: string; targetRecordData?: Record; definitionId?: number; screenId?: number; buttonComponentId?: string; } interface ApprovalRequestModalProps { open: boolean; onOpenChange: (open: boolean) => void; eventDetail?: ApprovalModalEventDetail | null; } interface UserSearchResult { user_id: string; user_name: string; position_name?: string; dept_name?: string; dept_code?: string; email?: string; } function genId(): string { return `a_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; } export const ApprovalRequestModal: React.FC = ({ open, onOpenChange, eventDetail, }) => { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [approvalMode, setApprovalMode] = useState("sequential"); const [approvers, setApprovers] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); // 사용자 검색 상태 const [searchOpen, setSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const searchInputRef = useRef(null); const searchTimerRef = useRef(null); // 모달 닫힐 때 초기화 useEffect(() => { if (!open) { setTitle(""); setDescription(""); setApprovalMode("sequential"); setApprovers([]); setError(null); setSearchOpen(false); setSearchQuery(""); setSearchResults([]); } }, [open]); // 사용자 검색 (디바운스) const searchUsers = useCallback(async (query: string) => { if (!query.trim() || query.trim().length < 1) { setSearchResults([]); return; } setIsSearching(true); try { const res = await getUserList({ search: query.trim(), limit: 20 }); const data = res?.data || res || []; const users: UserSearchResult[] = Array.isArray(data) ? data : []; // 이미 추가된 결재자 제외 const existingIds = new Set(approvers.map((a) => a.user_id)); setSearchResults(users.filter((u) => !existingIds.has(u.user_id))); } catch { setSearchResults([]); } finally { setIsSearching(false); } }, [approvers]); useEffect(() => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); if (!searchQuery.trim()) { setSearchResults([]); return; } searchTimerRef.current = setTimeout(() => { searchUsers(searchQuery); }, 300); return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); }; }, [searchQuery, searchUsers]); const addApprover = (user: UserSearchResult) => { setApprovers((prev) => [ ...prev, { id: genId(), user_id: user.user_id, user_name: user.user_name, position_name: user.position_name || "", dept_name: user.dept_name || "", }, ]); setSearchQuery(""); setSearchResults([]); setSearchOpen(false); }; const removeApprover = (id: string) => { setApprovers((prev) => prev.filter((a) => a.id !== id)); }; const moveApprover = (idx: number, direction: "up" | "down") => { setApprovers((prev) => { const next = [...prev]; const targetIdx = direction === "up" ? idx - 1 : idx + 1; if (targetIdx < 0 || targetIdx >= next.length) return prev; [next[idx], next[targetIdx]] = [next[targetIdx], next[idx]]; return next; }); }; const handleSubmit = async () => { if (!title.trim()) { setError("결재 제목을 입력해주세요."); return; } if (approvers.length === 0) { setError("결재자를 1명 이상 추가해주세요."); return; } if (!eventDetail?.targetTable || !eventDetail?.targetRecordId) { setError("결재 대상 정보가 없습니다. 레코드를 선택 후 다시 시도해주세요."); return; } setIsSubmitting(true); setError(null); const res = await createApprovalRequest({ title: title.trim(), description: description.trim() || undefined, target_table: eventDetail.targetTable, target_record_id: eventDetail.targetRecordId, target_record_data: { ...eventDetail.targetRecordData, approval_mode: approvalMode, }, screen_id: eventDetail.screenId, button_component_id: eventDetail.buttonComponentId, approvers: approvers.map((a, idx) => ({ approver_id: a.user_id, approver_name: a.user_name, approver_position: a.position_name || undefined, approver_dept: a.dept_name || undefined, approver_label: approvalMode === "sequential" ? `${idx + 1}차 결재` : "동시 결재", })), }); setIsSubmitting(false); if (res.success) { toast.success("결재 요청이 완료되었습니다."); onOpenChange(false); } else { setError(res.error || res.message || "결재 요청에 실패했습니다."); } }; return ( 결재 상신 결재 방식을 선택하고 결재자를 검색하여 추가합니다.
{/* 결재 제목 */}
setTitle(e.target.value)} placeholder="결재 제목을 입력하세요" className="h-8 text-xs sm:h-10 sm:text-sm" />
{/* 결재 사유 */}