'use client'; export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2'; /** * 악성채권 추심관리 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (검색, 거래처, 상태, 정렬) * - Stats 카드 (API 통계 또는 로컬 계산) * - tableHeaderActions: 3개 Select 필터 * - Switch 토글 (설정) * - 삭제 다이얼로그 (deleteConfirmMessage) */ import { useState, useMemo, useCallback, useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle } from 'lucide-react'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { MobileCard } from '@/components/organisms/MobileCard'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import type { BadDebtRecord, SortOption } from './types'; import { COLLECTION_STATUS_LABELS, STATUS_FILTER_OPTIONS, STATUS_BADGE_STYLES, SORT_OPTIONS, } from './types'; import { formatNumber } from '@/lib/utils/amount'; import { applyFilters, enumFilter } from '@/lib/utils/search'; import { deleteBadDebt, toggleBadDebt } from './actions'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, { key: 'vendorName', label: '거래처', className: 'w-[100px]' }, { 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]' }, ]; // ===== 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 Array.from(vendorMap.entries()).map(([id, name]) => ({ value: id, label: name, })); }; export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollectionProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [data, setData] = useState(initialData); const [vendorFilter, setVendorFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [sortOption, setSortOption] = useState('latest'); // 거래처 옵션 const vendorOptions = useMemo(() => getVendorOptions(data), [data]); // ===== 핸들러 ===== const handleRowClick = useCallback( (item: BadDebtRecord) => { router.push(`/ko/accounting/bad-debt-collection/${item.id}?mode=view`); }, [router] ); // 설정 토글 핸들러 (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); } }); }, [] ); // ===== 통계 계산 ===== const statsData = useMemo(() => { if (initialSummary) { return { totalAmount: initialSummary.total_amount, collectingAmount: initialSummary.collecting_amount, legalActionAmount: initialSummary.legal_action_amount, recoveredAmount: initialSummary.recovered_amount, }; } // 로컬 데이터로 계산 (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 { totalAmount, collectingAmount, legalActionAmount, recoveredAmount }; }, [data, initialSummary]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '악성채권 추심관리', description: '연체 및 악성채권 현황을 추적하고 관리합니다', icon: AlertTriangle, basePath: '/accounting/bad-debt-collection', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { return { success: true, data: data, totalCount: data.length, }; }, deleteItem: async (id: string) => { const result = await deleteBadDebt(id); if (result.success) { setData((prev) => prev.filter((item) => item.id !== id)); } return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 검색 필터 searchPlaceholder: '거래처명, 거래처코드, 사업자번호 검색...', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.vendorName.toLowerCase().includes(search) || item.vendorCode.toLowerCase().includes(search) || item.businessNumber.toLowerCase().includes(search) ); }, // 필터 설정 (모바일용) filterConfig: [ { key: 'vendor', label: '거래처', type: 'single', options: vendorOptions, }, { key: 'status', label: '상태', type: 'single', options: STATUS_FILTER_OPTIONS.filter((o) => o.value !== 'all').map((o) => ({ value: o.value, label: o.label, })), }, { key: 'sortBy', label: '정렬', type: 'single', options: SORT_OPTIONS.map((o) => ({ value: o.value, label: o.label, })), }, ], initialFilters: { vendor: 'all', status: 'all', sortBy: 'latest', }, filterTitle: '악성채권 필터', // 커스텀 필터 함수 customFilterFn: (items) => { if (!items || items.length === 0) return items; return applyFilters([...items], [ enumFilter('vendorId', vendorFilter), enumFilter('status', statusFilter), ]); }, // 커스텀 정렬 함수 customSortFn: (items) => { const sorted = [...items]; switch (sortOption) { case 'oldest': sorted.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); break; default: // latest sorted.sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); break; } return sorted; }, // 테이블 헤더 액션 (3개 필터) tableHeaderActions: () => (
{/* 거래처 필터 */} {/* 상태 필터 */} {/* 정렬 */}
), // Stats 카드 computeStats: (): StatCard[] => [ { label: '총 악성채권', value: `${formatNumber(statsData.totalAmount)}원`, icon: AlertTriangle, iconColor: 'text-red-500', }, { label: '추심중', value: `${formatNumber(statsData.collectingAmount)}원`, icon: AlertTriangle, iconColor: 'text-orange-500', }, { label: '법적조치', value: `${formatNumber(statsData.legalActionAmount)}원`, icon: AlertTriangle, iconColor: 'text-red-600', }, { label: '회수완료', value: `${formatNumber(statsData.recoveredAmount)}원`, icon: AlertTriangle, iconColor: 'text-green-500', }, ], // 삭제 확인 메시지 deleteConfirmMessage: { title: '악성채권 삭제', description: '이 악성채권 기록을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.', }, // 테이블 행 렌더링 renderTableRow: ( item: BadDebtRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} > e.stopPropagation()}> {/* No. */} {globalIndex} {/* 거래처 */} {item.vendorName} {/* 채권금액 */} {formatNumber(item.debtAmount)}원 {/* 발생일 */} {item.occurrenceDate} {/* 연체일수 */} {item.overdueDays}일 {/* 담당자 */} {item.assignedManager?.name || '-'} {/* 상태 */} {COLLECTION_STATUS_LABELS[item.status]} {/* 설정 */} e.stopPropagation()}> handleSettingToggle(item.id, checked)} disabled={isPending} /> ), // 모바일 카드 렌더링 renderMobileCard: ( item: BadDebtRecord, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '연체일수', value: `${item.overdueDays}일` }, { label: '발생일', value: item.occurrenceDate }, { label: '담당자', value: item.assignedManager?.name || '-' }, ]} /> ), }), [ data, vendorOptions, vendorFilter, statusFilter, sortOption, statsData, handleRowClick, handleSettingToggle, isPending, ] ); return ; }