'use client'; import { useState, useMemo, useCallback, useEffect, useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { FileCheck, Check, X, Clock, FileX, Files, Edit, } from 'lucide-react'; import { toast } from 'sonner'; import { getInbox, getInboxSummary, approveDocument, rejectDocument, approveDocumentsBulk, rejectDocumentsBulk, } from './actions'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { TableRow, TableCell } from '@/components/ui/table'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { UniversalListPage, type UniversalListConfig, type TabOption, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; // import { DocumentDetailModal } from '@/components/approval/DocumentDetail'; import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail'; import type { DocumentType, ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData, } from '@/components/approval/DocumentDetail/types'; import type { ApprovalTabType, ApprovalRecord, ApprovalType, SortOption, FilterOption, } from './types'; import { APPROVAL_TAB_LABELS, SORT_OPTIONS, FILTER_OPTIONS, APPROVAL_TYPE_LABELS, APPROVAL_STATUS_LABELS, APPROVAL_STATUS_COLORS, } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; // ===== 통계 타입 ===== interface InboxSummary { total: number; pending: number; approved: number; rejected: number; } export function ApprovalBox() { const router = useRouter(); const [isPending, startTransition] = useTransition(); // ===== 상태 관리 ===== const [activeTab, setActiveTab] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [filterOption, setFilterOption] = useState('all'); const [sortOption, setSortOption] = useState('latest'); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 날짜 범위 상태 const [startDate, setStartDate] = useState('2025-09-01'); const [endDate, setEndDate] = useState('2025-09-03'); // 다이얼로그 상태 const [approveDialogOpen, setApproveDialogOpen] = useState(false); const [rejectDialogOpen, setRejectDialogOpen] = useState(false); const [rejectComment, setRejectComment] = useState(''); const [pendingSelectedItems, setPendingSelectedItems] = useState>(new Set()); const [pendingClearSelection, setPendingClearSelection] = useState<(() => void) | null>(null); // ===== 문서 상세 모달 상태 ===== const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); // API 데이터 const [data, setData] = useState([]); const [totalCount, setTotalCount] = useState(0); const [totalPages, setTotalPages] = useState(1); const [isLoading, setIsLoading] = useState(true); // 통계 데이터 const [fixedStats, setFixedStats] = useState({ all: 0, pending: 0, approved: 0, rejected: 0 }); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { const sortConfig: { sort_by: string; sort_dir: 'asc' | 'desc' } = (() => { switch (sortOption) { case 'latest': return { sort_by: 'created_at', sort_dir: 'desc' }; case 'oldest': return { sort_by: 'created_at', sort_dir: 'asc' }; case 'draftDateAsc': return { sort_by: 'created_at', sort_dir: 'asc' }; case 'draftDateDesc': return { sort_by: 'created_at', sort_dir: 'desc' }; default: return { sort_by: 'created_at', sort_dir: 'desc' }; } })(); const result = await getInbox({ page: currentPage, per_page: itemsPerPage, search: searchQuery || undefined, status: activeTab !== 'all' ? activeTab : undefined, approval_type: filterOption !== 'all' ? filterOption : undefined, ...sortConfig, }); setData(result.data); setTotalCount(result.total); setTotalPages(result.lastPage); } catch (error) { if (isNextRedirectError(error)) throw error; console.error('Failed to load inbox:', error); toast.error('결재함 목록을 불러오는데 실패했습니다.'); } finally { setIsLoading(false); } }, [currentPage, itemsPerPage, searchQuery, filterOption, sortOption, activeTab]); // ===== 초기 로드 ===== useEffect(() => { loadData(); }, [loadData]); // ===== 검색어/필터/탭 변경 시 페이지 초기화 ===== useEffect(() => { setCurrentPage(1); }, [searchQuery, filterOption, sortOption, activeTab]); // ===== 탭 변경 핸들러 ===== const handleTabChange = useCallback((value: string) => { setActiveTab(value as ApprovalTabType); setSearchQuery(''); }, []); // ===== 전체 탭일 때만 통계 업데이트 ===== useEffect(() => { if (activeTab === 'all' && data.length > 0) { const pending = data.filter((item) => item.status === 'pending').length; const approved = data.filter((item) => item.status === 'approved').length; const rejected = data.filter((item) => item.status === 'rejected').length; setFixedStats({ all: totalCount, pending, approved, rejected, }); } }, [data, totalCount, activeTab]); // ===== 승인/반려 핸들러 ===== const handleApproveClick = useCallback( (selectedItems: Set, onClearSelection: () => void) => { if (selectedItems.size === 0) return; setPendingSelectedItems(selectedItems); setPendingClearSelection(() => onClearSelection); setApproveDialogOpen(true); }, [] ); const handleApproveConfirm = useCallback(async () => { const ids = Array.from(pendingSelectedItems); startTransition(async () => { try { const result = await approveDocumentsBulk(ids); if (result.success) { toast.success('승인 완료', { description: '결재 승인이 완료되었습니다.', }); pendingClearSelection?.(); loadData(); } else { toast.error(result.error || '승인 처리에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('Approve error:', error); toast.error('승인 처리 중 오류가 발생했습니다.'); } }); setApproveDialogOpen(false); setPendingSelectedItems(new Set()); setPendingClearSelection(null); }, [pendingSelectedItems, pendingClearSelection, loadData]); const handleRejectClick = useCallback( (selectedItems: Set, onClearSelection: () => void) => { if (selectedItems.size === 0) return; setPendingSelectedItems(selectedItems); setPendingClearSelection(() => onClearSelection); setRejectComment(''); setRejectDialogOpen(true); }, [] ); const handleRejectConfirm = useCallback(async () => { if (!rejectComment.trim()) { toast.error('반려 사유를 입력해주세요.'); return; } const ids = Array.from(pendingSelectedItems); startTransition(async () => { try { const result = await rejectDocumentsBulk(ids, rejectComment); if (result.success) { toast.success('반려 완료', { description: '결재 반려가 완료되었습니다.', }); pendingClearSelection?.(); setRejectComment(''); loadData(); } else { toast.error(result.error || '반려 처리에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('Reject error:', error); toast.error('반려 처리 중 오류가 발생했습니다.'); } }); setRejectDialogOpen(false); setPendingSelectedItems(new Set()); setPendingClearSelection(null); }, [pendingSelectedItems, rejectComment, pendingClearSelection, loadData]); // ===== 문서 클릭 핸들러 ===== const handleDocumentClick = useCallback((item: ApprovalRecord) => { setSelectedDocument(item); setIsModalOpen(true); }, []); const handleModalEdit = useCallback(() => { if (selectedDocument) { router.push(`/ko/approval/draft/new?id=${selectedDocument.id}&mode=edit`); setIsModalOpen(false); } }, [selectedDocument, router]); const handleEditClick = useCallback( (item: ApprovalRecord) => { router.push(`/ko/approval/draft/new?id=${item.id}&mode=edit`); }, [router] ); const handleModalCopy = useCallback(() => { toast.info('문서 복제 기능은 준비 중입니다.'); setIsModalOpen(false); }, []); const handleModalApprove = useCallback(async () => { if (!selectedDocument?.id) return; const result = await approveDocument(selectedDocument.id); if (result.success) { toast.success('문서가 승인되었습니다.'); loadData(); } else { toast.error(result.error || '승인에 실패했습니다.'); } setIsModalOpen(false); }, [selectedDocument, loadData]); const handleModalReject = useCallback(async () => { if (!selectedDocument?.id) return; const result = await rejectDocument(selectedDocument.id, '반려'); if (result.success) { toast.success('문서가 반려되었습니다.'); loadData(); } else { toast.error(result.error || '반려에 실패했습니다.'); } setIsModalOpen(false); }, [selectedDocument, loadData]); // ===== 문서 타입 변환 ===== const getDocumentType = (approvalType: ApprovalType): DocumentType => { switch (approvalType) { case 'expense_estimate': return 'expenseEstimate'; case 'expense_report': return 'expenseReport'; default: return 'proposal'; } }; // ===== 모달용 데이터 변환 ===== const convertToModalData = ( item: ApprovalRecord ): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => { const docType = getDocumentType(item.approvalType); const drafter = { id: 'drafter-1', name: item.drafter, position: item.drafterPosition, department: item.drafterDepartment, status: 'approved' as const, }; const approvers = [ { id: 'approver-1', name: item.approver || '미지정', position: '부장', department: '경영지원팀', status: item.status === 'approved' ? ('approved' as const) : item.status === 'rejected' ? ('rejected' as const) : ('pending' as const), }, ]; switch (docType) { case 'expenseEstimate': return { documentNo: item.documentNo, createdAt: item.draftDate, items: [], totalExpense: 0, accountBalance: 0, finalDifference: 0, approvers, drafter, }; case 'expenseReport': return { documentNo: item.documentNo, createdAt: item.draftDate, requestDate: item.draftDate, paymentDate: item.draftDate, items: [], cardInfo: '', totalAmount: 0, attachments: [], approvers, drafter, }; default: return { documentNo: item.documentNo, createdAt: item.draftDate, vendor: '거래처', vendorPaymentDate: item.draftDate, title: item.title, description: item.title, reason: '업무상 필요', estimatedCost: 0, attachments: [], approvers, drafter, }; } }; // ===== 탭 옵션 ===== const tabs: TabOption[] = useMemo( () => [ { value: 'all', label: APPROVAL_TAB_LABELS.all, count: fixedStats.all, color: 'blue', }, { value: 'pending', label: APPROVAL_TAB_LABELS.pending, count: fixedStats.pending, color: 'yellow', }, { value: 'approved', label: APPROVAL_TAB_LABELS.approved, count: fixedStats.approved, color: 'green', }, { value: 'rejected', label: APPROVAL_TAB_LABELS.rejected, count: fixedStats.rejected, color: 'red', }, ], [fixedStats] ); // ===== UniversalListPage 설정 ===== const approvalBoxConfig: UniversalListConfig = useMemo( () => ({ title: '결재함', description: '결재 문서를 관리합니다', icon: FileCheck, basePath: '/approval/inbox', idField: 'id', actions: { getList: async () => ({ success: true, data: data, totalCount: totalCount, totalPages: totalPages, }), }, columns: [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, { key: 'documentNo', label: '문서번호' }, { key: 'approvalType', label: '문서유형' }, { key: 'title', label: '제목' }, { key: 'drafter', label: '기안자' }, { key: 'approver', label: '결재자' }, { key: 'draftDate', label: '기안일시' }, { key: 'status', label: '상태', className: 'text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ], tabs: tabs, defaultTab: activeTab, dateRangeSelector: { enabled: true, showPresets: false, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, searchPlaceholder: '제목, 기안자, 부서 검색...', itemsPerPage: itemsPerPage, // 모바일 필터 설정 filterConfig: [ { key: 'approvalType', label: '문서유형', type: 'single', options: FILTER_OPTIONS.filter((o) => o.value !== 'all'), }, { key: 'sort', label: '정렬', type: 'single', options: SORT_OPTIONS, }, ], initialFilters: { approvalType: filterOption, sort: sortOption, }, filterTitle: '결재함 필터', computeStats: () => [ { label: '전체결재', value: `${fixedStats.all}건`, icon: Files, iconColor: 'text-blue-500', }, { label: '미결재', value: `${fixedStats.pending}건`, icon: Clock, iconColor: 'text-yellow-500', }, { label: '결재완료', value: `${fixedStats.approved}건`, icon: FileCheck, iconColor: 'text-green-500', }, { label: '결재반려', value: `${fixedStats.rejected}건`, icon: FileX, iconColor: 'text-red-500', }, ], headerActions: ({ selectedItems, onClearSelection }) => ( <> {selectedItems.size > 0 && (
)} ), tableHeaderActions: (
), renderTableRow: (item, index, globalIndex, handlers) => { const { isSelected, onToggle } = handlers; return ( handleDocumentClick(item)} > e.stopPropagation()}> {globalIndex} {item.documentNo} {APPROVAL_TYPE_LABELS[item.approvalType]} {item.title} {item.drafter} {item.approver || '-'} {item.draftDate} {APPROVAL_STATUS_LABELS[item.status]} e.stopPropagation()}> {isSelected && ( )} ); }, renderMobileCard: (item, index, globalIndex, handlers) => { const { isSelected, onToggle } = handlers; return ( {APPROVAL_TYPE_LABELS[item.approvalType]} {APPROVAL_STATUS_LABELS[item.status]} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} actions={ item.status === 'pending' && isSelected ? (
) : undefined } onClick={() => handleDocumentClick(item)} /> ); }, renderDialogs: () => ( <> {/* 승인 확인 다이얼로그 */} {/* 반려 확인 다이얼로그 */} 결재 반려 {pendingSelectedItems.size}건의 결재를 반려합니다. 반려 사유를 입력해주세요.