'use client'; import { useState, useMemo, useCallback, useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle, Pencil, Trash2, Eye, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { IntegratedListTemplateV2, type TableColumn, type StatCard, } from '@/components/templates/IntegratedListTemplateV2'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import type { BadDebtRecord, SortOption, } from './types'; import { COLLECTION_STATUS_LABELS, STATUS_FILTER_OPTIONS, STATUS_BADGE_STYLES, SORT_OPTIONS, } from './types'; import { deleteBadDebt, toggleBadDebt } from './actions'; // ===== Props 타입 정의 ===== interface BadDebtCollectionProps { initialData: BadDebtRecord[]; initialSummary?: { total_amount: number; collecting_amount: number; legal_action_amount: number; recovered_amount: number; bad_debt_amount: number; } | null; } // 거래처 목록 추출 (필터용) const getVendorOptions = (data: BadDebtRecord[]) => { const vendorMap = new Map(); data.forEach(item => { vendorMap.set(item.vendorId, item.vendorName); }); return [ { value: 'all', label: '전체' }, ...Array.from(vendorMap.entries()).map(([id, name]) => ({ value: id, label: name, })), ]; }; export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollectionProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); // ===== 상태 관리 ===== const [searchQuery, setSearchQuery] = useState(''); const [sortOption, setSortOption] = useState('latest'); const [vendorFilter, setVendorFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 삭제 다이얼로그 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteTargetId, setDeleteTargetId] = useState(null); // 데이터 (서버에서 받은 초기 데이터 사용) const [data, setData] = useState(initialData); // 거래처 옵션 const vendorOptions = useMemo(() => getVendorOptions(data), [data]); // ===== 체크박스 핸들러 ===== const toggleSelection = useCallback((id: string) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); return newSet; }); }, []); // ===== 필터링된 데이터 ===== const filteredData = useMemo(() => { let result = data.filter(item => item.vendorName.includes(searchQuery) || item.vendorCode.includes(searchQuery) || item.businessNumber.includes(searchQuery) ); // 거래처 필터 if (vendorFilter !== 'all') { result = result.filter(item => item.vendorId === vendorFilter); } // 상태 필터 if (statusFilter !== 'all') { result = result.filter(item => item.status === statusFilter); } // 정렬 switch (sortOption) { case 'latest': result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case 'oldest': result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); break; } return result; }, [data, searchQuery, vendorFilter, statusFilter, sortOption]); const paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredData.slice(startIndex, startIndex + itemsPerPage); }, [filteredData, currentPage, itemsPerPage]); const totalPages = Math.ceil(filteredData.length / itemsPerPage); // ===== 전체 선택 핸들러 ===== const toggleSelectAll = useCallback(() => { if (selectedItems.size === filteredData.length && filteredData.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(filteredData.map(item => item.id))); } }, [selectedItems.size, filteredData]); // ===== 액션 핸들러 ===== const handleRowClick = useCallback((item: BadDebtRecord) => { router.push(`/ko/accounting/bad-debt-collection/${item.id}`); }, [router]); const handleEdit = useCallback((item: BadDebtRecord) => { router.push(`/ko/accounting/bad-debt-collection/${item.id}/edit`); }, [router]); const handleDeleteClick = useCallback((id: string) => { setDeleteTargetId(id); setShowDeleteDialog(true); }, []); const handleConfirmDelete = useCallback(() => { if (deleteTargetId) { startTransition(async () => { const result = await deleteBadDebt(deleteTargetId); if (result.success) { setData(prev => prev.filter(item => item.id !== deleteTargetId)); setSelectedItems(prev => { const newSet = new Set(prev); newSet.delete(deleteTargetId); return newSet; }); } else { console.error('[BadDebtCollection] Delete failed:', result.error); } }); } setShowDeleteDialog(false); setDeleteTargetId(null); }, [deleteTargetId]); // 설정 토글 핸들러 (API 호출) const handleSettingToggle = useCallback((id: string, checked: boolean) => { // Optimistic update setData(prev => prev.map(item => item.id === id ? { ...item, settingToggle: checked } : item )); startTransition(async () => { const result = await toggleBadDebt(id); if (!result.success) { // Rollback on error setData(prev => prev.map(item => item.id === id ? { ...item, settingToggle: !checked } : item )); console.error('[BadDebtCollection] Toggle failed:', result.error); } }); }, []); // ===== 통계 카드 (API 통계 또는 로컬 계산) ===== const statCards: StatCard[] = useMemo(() => { if (initialSummary) { // API 통계 데이터 사용 return [ { label: '총 악성채권', value: `${initialSummary.total_amount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-red-500' }, { label: '추심중', value: `${initialSummary.collecting_amount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-orange-500' }, { label: '법적조치', value: `${initialSummary.legal_action_amount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-red-600' }, { label: '회수완료', value: `${initialSummary.recovered_amount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-green-500' }, ]; } // 로컬 데이터로 계산 (fallback) const totalAmount = data.reduce((sum, d) => sum + d.debtAmount, 0); const collectingAmount = data.filter(d => d.status === 'collecting').reduce((sum, d) => sum + d.debtAmount, 0); const legalActionAmount = data.filter(d => d.status === 'legalAction').reduce((sum, d) => sum + d.debtAmount, 0); const recoveredAmount = data.filter(d => d.status === 'recovered').reduce((sum, d) => sum + d.debtAmount, 0); return [ { label: '총 악성채권', value: `${totalAmount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-red-500' }, { label: '추심중', value: `${collectingAmount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-orange-500' }, { label: '법적조치', value: `${legalActionAmount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-red-600' }, { label: '회수완료', value: `${recoveredAmount.toLocaleString()}원`, icon: AlertTriangle, iconColor: 'text-green-500' }, ]; }, [data, initialSummary]); // ===== 테이블 컬럼 ===== const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, { key: 'vendorName', label: '거래처' }, { key: 'debtAmount', label: '채권금액', className: 'text-right w-[140px]' }, { key: 'occurrenceDate', label: '발생일', className: 'text-center w-[110px]' }, { key: 'overdueDays', label: '연체일수', className: 'text-center w-[100px]' }, { key: 'managerName', label: '담당자', className: 'w-[100px]' }, { key: 'status', label: '상태', className: 'text-center w-[100px]' }, { key: 'setting', label: '설정', className: 'text-center w-[80px]' }, { key: 'actions', label: '작업', className: 'text-center w-[120px]' }, ], []); // ===== 테이블 행 렌더링 ===== const renderTableRow = useCallback((item: BadDebtRecord, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); return ( handleRowClick(item)} > e.stopPropagation()}> toggleSelection(item.id)} /> {/* No. */} {globalIndex} {/* 거래처 */} {item.vendorName} {/* 채권금액 */} {item.debtAmount.toLocaleString()}원 {/* 발생일 */} {item.occurrenceDate} {/* 연체일수 */} {item.overdueDays}일 {/* 담당자 */} {item.assignedManager?.name || '-'} {/* 상태 */} {COLLECTION_STATUS_LABELS[item.status]} {/* 설정 */} e.stopPropagation()}> handleSettingToggle(item.id, checked)} disabled={isPending} /> {/* 작업 */} e.stopPropagation()}> {isSelected && (
)}
); }, [selectedItems, toggleSelection, handleRowClick, handleEdit, handleDeleteClick, handleSettingToggle, isPending]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: BadDebtRecord, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void ) => { return ( {COLLECTION_STATUS_LABELS[item.status]} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} actions={ isSelected ? (
) : undefined } onClick={() => handleRowClick(item)} /> ); }, [handleRowClick, handleEdit, handleDeleteClick]); // ===== 테이블 헤더 액션 (3개 필터) ===== const tableHeaderActions = (
{/* 거래처 필터 */} {/* 상태 필터 */} {/* 정렬 */}
); return ( <> item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} pagination={{ currentPage, totalPages, totalItems: filteredData.length, itemsPerPage, onPageChange: setCurrentPage, }} /> {/* 삭제 확인 다이얼로그 */} 악성채권 삭제 이 악성채권 기록을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다. 취소 {isPending ? '삭제 중...' : '삭제'} ); }