'use client'; /** * 견적관리 리스트 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (검색, 필터, 정렬) * - Stats 카드 클릭 필터링 (activeStatTab) * - DateRangeSelector (headerActions → dateRangeSelector config) * - filterConfig (multi: 거래처, 견적자 / single: 상태, 정렬) */ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { FileText, FileTextIcon, Clock, FileCheck, Pencil } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { MobileCard } from '@/components/organisms/MobileCard'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import type { Estimate, EstimateStats } from './types'; import { ESTIMATE_STATUS_OPTIONS, ESTIMATE_SORT_OPTIONS, STATUS_STYLES, STATUS_LABELS, } from './types'; import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions'; import type { ClientOption, UserOption } from './actions'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, { key: 'estimateCode', label: '견적번호', className: 'w-[100px]' }, { key: 'partnerName', label: '거래처', className: 'w-[120px]' }, { key: 'projectName', label: '현장명', className: 'min-w-[150px]' }, { key: 'estimatorName', label: '견적자', className: 'w-[80px] text-center' }, { key: 'itemCount', label: '총 개소', className: 'w-[80px] text-center' }, { key: 'estimateAmount', label: '견적금액', className: 'w-[120px] text-right' }, { key: 'completedDate', label: '견적완료일', className: 'w-[110px] text-center' }, { key: 'bidDate', label: '입찰일', className: 'w-[110px] text-center' }, { key: 'status', label: '상태', className: 'w-[100px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; // 금액 포맷팅 function formatAmount(amount: number): string { return new Intl.NumberFormat('ko-KR').format(amount); } interface EstimateListClientProps { initialData?: Estimate[]; initialStats?: EstimateStats; } export default function EstimateListClient({ initialData = [], initialStats }: EstimateListClientProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== // Stats 카드 클릭 필터용 const [activeStatTab, setActiveStatTab] = useState<'all' | 'pending' | 'completed'>('all'); // 날짜 범위 const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); // Stats 데이터 const [stats, setStats] = useState(initialStats || null); // 필터 옵션 데이터 const [partnerOptions, setPartnerOptions] = useState([]); const [estimatorOptions, setEstimatorOptions] = useState([]); // Stats 로드 useEffect(() => { if (!initialStats) { getEstimateStats().then((result) => { if (result.success && result.data) { setStats(result.data); } }); } }, [initialStats]); // 거래처/견적자 옵션 로드 useEffect(() => { // 거래처 옵션 로드 getClientOptions().then((result) => { if (result.success && result.data) { setPartnerOptions(result.data); } }); // 견적자(사용자) 옵션 로드 getUserOptions().then((result) => { if (result.success && result.data) { setEstimatorOptions(result.data); } }); }, []); // ===== 핸들러 ===== const handleRowClick = useCallback( (item: Estimate) => { router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=view`); }, [router] ); const handleEdit = useCallback( (item: Estimate) => { router.push(`/ko/construction/project/bidding/estimates/${item.id}?mode=edit`); }, [router] ); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '견적관리', description: '견적을 관리합니다', icon: FileText, basePath: '/construction/project/bidding/estimates', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { const result = await getEstimateList({ size: 100, startDate: startDate || undefined, endDate: endDate || undefined, }); if (result.success && result.data) { return { success: true, data: result.data.items, totalCount: result.data.total, }; } return { success: false, error: result.error }; }, deleteItem: async (id: string) => { const result = await deleteEstimate(id); return { success: result.success, error: result.error }; }, deleteBulk: async (ids: string[]) => { const result = await deleteEstimates(ids); return { success: result.success, error: result.error }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 검색 필터 searchPlaceholder: '견적번호, 거래처, 현장명 검색', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.projectName.toLowerCase().includes(search) || item.estimateCode.toLowerCase().includes(search) || item.partnerName.toLowerCase().includes(search) ); }, // 필터 설정 (PC: 인라인, 모바일: 바텀시트) filterConfig: [ { key: 'partner', label: '거래처', type: 'multi', options: partnerOptions, }, { key: 'estimator', label: '견적자', type: 'multi', options: estimatorOptions, }, { key: 'status', label: '상태', type: 'single', options: ESTIMATE_STATUS_OPTIONS.filter((o) => o.value !== 'all'), }, { key: 'sortBy', label: '정렬', type: 'single', options: ESTIMATE_SORT_OPTIONS, }, ], initialFilters: { partner: [], estimator: [], status: 'all', sortBy: 'latest', }, filterTitle: '견적 필터', // 커스텀 필터 함수 (activeStatTab + filterValues 기반) customFilterFn: (items, filterValues) => { return items.filter((item) => { // Stats 탭 필터 if (activeStatTab === 'pending' && item.status !== 'pending') return false; if (activeStatTab === 'completed' && item.status !== 'completed') return false; // 거래처 필터 (다중선택) const partnerFilters = filterValues.partner as string[]; if (partnerFilters?.length > 0 && !partnerFilters.includes(item.partnerId)) return false; // 견적자 필터 (다중선택) const estimatorFilters = filterValues.estimator as string[]; if (estimatorFilters?.length > 0 && !estimatorFilters.includes(item.estimatorId)) return false; // 상태 필터 const statusFilter = filterValues.status as string; if (statusFilter && statusFilter !== 'all' && item.status !== statusFilter) return false; return true; }); }, // 커스텀 정렬 함수 customSortFn: (items, filterValues) => { const sorted = [...items]; const sortBy = (filterValues.sortBy as string) || 'latest'; switch (sortBy) { case 'latest': sorted.sort((a, b) => { const dateA = a.completedDate ? new Date(a.completedDate).getTime() : new Date(a.createdAt).getTime(); const dateB = b.completedDate ? new Date(b.completedDate).getTime() : new Date(b.createdAt).getTime(); return dateB - dateA; }); break; case 'oldest': sorted.sort((a, b) => { const dateA = a.completedDate ? new Date(a.completedDate).getTime() : new Date(a.createdAt).getTime(); const dateB = b.completedDate ? new Date(b.completedDate).getTime() : new Date(b.createdAt).getTime(); return dateA - dateB; }); break; case 'bidDateDesc': sorted.sort((a, b) => { if (!a.bidDate) return 1; if (!b.bidDate) return -1; return new Date(b.bidDate).getTime() - new Date(a.bidDate).getTime(); }); break; case 'partnerNameAsc': sorted.sort((a, b) => a.partnerName.localeCompare(b.partnerName, 'ko')); break; case 'partnerNameDesc': sorted.sort((a, b) => b.partnerName.localeCompare(a.partnerName, 'ko')); break; case 'projectNameAsc': sorted.sort((a, b) => a.projectName.localeCompare(b.projectName, 'ko')); break; case 'projectNameDesc': sorted.sort((a, b) => b.projectName.localeCompare(a.projectName, 'ko')); break; } return sorted; }, // 공통 헤더 옵션: 날짜 선택기 dateRangeSelector: { enabled: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // Stats 카드 (동적 계산 with onClick) computeStats: (): StatCard[] => [ { label: '전체 견적', value: stats?.total ?? 0, icon: FileTextIcon, iconColor: 'text-blue-600', onClick: () => setActiveStatTab('all'), isActive: activeStatTab === 'all', }, { label: '견적대기', value: stats?.pending ?? 0, icon: Clock, iconColor: 'text-orange-500', onClick: () => setActiveStatTab('pending'), isActive: activeStatTab === 'pending', }, { label: '견적완료', value: stats?.completed ?? 0, icon: FileCheck, iconColor: 'text-green-600', onClick: () => setActiveStatTab('completed'), isActive: activeStatTab === 'completed', }, ], // 삭제 확인 메시지 deleteConfirmMessage: { title: '견적 삭제', description: '선택한 견적을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.', }, // 테이블 행 렌더링 renderTableRow: ( item: Estimate, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.estimateCode} {item.partnerName} {item.projectName} {item.estimatorName} {item.itemCount} {formatAmount(item.estimateAmount)} {item.completedDate || '-'} {item.bidDate || '-'} {STATUS_LABELS[item.status]} {handlers.isSelected && (
)}
), // 모바일 카드 렌더링 renderMobileCard: ( item: Estimate, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '거래처', value: item.partnerName }, { label: '견적자', value: item.estimatorName }, { label: '견적금액', value: `${formatAmount(item.estimateAmount)}원` }, { label: '입찰일', value: item.bidDate || '-' }, ]} /> ), }), [startDate, endDate, activeStatTab, stats, partnerOptions, estimatorOptions, handleRowClick, handleEdit] ); return ; }