'use client'; import { useState, useCallback, useMemo, useEffect } from 'react'; import { format } from 'date-fns'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { getPresetStyle } from '@/lib/utils/status-config'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { FileText, Eye } from 'lucide-react'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { LineItemsTable, useLineItems } from '@/components/organisms/LineItemsTable'; import { purchaseConfig } from './purchaseConfig'; import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail'; import type { ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types'; import { getApprovalById } from '@/components/approval/DocumentCreate/actions'; import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types'; import { PURCHASE_TYPE_LABELS } from './types'; import { getPurchaseById, createPurchase, updatePurchase, deletePurchase, } from './actions'; import { getClients } from '../VendorManagement/actions'; import { toast } from 'sonner'; import { formatNumber as formatAmount } from '@/lib/utils/amount'; interface PurchaseDetailProps { purchaseId: string; mode: 'view' | 'edit' | 'new'; } // ===== 거래처 타입 ===== interface ClientOption { id: string; name: string; } // ===== 초기 품목 데이터 ===== const createEmptyItem = (): PurchaseItem => ({ id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, itemName: '', quantity: 1, unitPrice: 0, supplyPrice: 0, vat: 0, note: '', }); export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { const isViewMode = mode === 'view'; const isNewMode = mode === 'new'; // ===== 로딩 상태 ===== const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); // ===== 거래처 목록 ===== const [clients, setClients] = useState([]); // ===== 폼 상태 ===== const [purchaseNo, setPurchaseNo] = useState(''); const [purchaseDate, setPurchaseDate] = useState(format(new Date(), 'yyyy-MM-dd')); const [vendorId, setVendorId] = useState(''); const [vendorName, setVendorName] = useState(''); const [purchaseType, setPurchaseType] = useState('unset'); const [items, setItems] = useState([createEmptyItem()]); const [taxInvoiceReceived, setTaxInvoiceReceived] = useState(false); // ===== 문서 관련 상태 (sourceDocument는 API에서 아직 미지원) ===== const [sourceDocument, setSourceDocument] = useState(undefined); const [withdrawalAccount, setWithdrawalAccount] = useState(undefined); const [createdAt, setCreatedAt] = useState(''); // ===== 문서 열람 모달 상태 ===== const [documentModalOpen, setDocumentModalOpen] = useState(false); const [modalData, setModalData] = useState(null); const [isModalLoading, setIsModalLoading] = useState(false); const [approvalId, setApprovalId] = useState(undefined); // ===== 품목 관리 (공통 훅) ===== const { handleItemChange, handleAddItem, handleRemoveItem, totals } = useLineItems({ items, setItems, createEmptyItem, supplyKey: 'supplyPrice', vatKey: 'vat', minItems: 1, }); // ===== 초기 데이터 로드 (거래처 + 매입 상세 병렬) ===== useEffect(() => { async function loadInitialData() { const isEditMode = purchaseId && mode !== 'new'; setIsLoading(true); const [clientsResult, purchaseResult] = await Promise.all([ getClients({ size: 1000, only_active: true }), isEditMode ? getPurchaseById(purchaseId) : Promise.resolve(null), ]); // 거래처 목록 if (clientsResult.success) { setClients(clientsResult.data.map(v => ({ id: v.id, name: v.vendorName, }))); } // 매입 상세 if (purchaseResult) { if (purchaseResult.success && purchaseResult.data) { const data = purchaseResult.data; setPurchaseNo(data.purchaseNo); setPurchaseDate(data.purchaseDate); setVendorId(data.vendorId); setVendorName(data.vendorName); setPurchaseType(data.purchaseType); setItems(data.items.length > 0 ? data.items : [createEmptyItem()]); setTaxInvoiceReceived(data.taxInvoiceReceived); setSourceDocument(data.sourceDocument); setWithdrawalAccount(data.withdrawalAccount); setCreatedAt(data.createdAt); setApprovalId(data.approvalId); } } else if (isNewMode) { setPurchaseNo('(자동생성)'); } setIsLoading(false); } loadInitialData(); }, [purchaseId, mode, isNewMode]); // ===== 문서 열람 핸들러 (API 연동) ===== const handleOpenDocument = useCallback(async () => { if (!approvalId) { toast.error('연결된 결재 문서가 없습니다.'); return; } setIsModalLoading(true); setDocumentModalOpen(true); try { const result = await getApprovalById(parseInt(approvalId)); if (result.success && result.data) { const formData = result.data; const docType = sourceDocument?.type === 'expense_report' ? 'expenseReport' : 'proposal'; // 기안자 정보 const drafter = { id: 'drafter-1', name: formData.basicInfo.drafter, position: formData.basicInfo.drafterPosition || '', department: formData.basicInfo.drafterDepartment || '', status: 'approved' as const, }; // 결재자 정보 const approvers = formData.approvalLine.map((person) => ({ id: person.id, name: person.name, position: person.position, department: person.department, status: 'approved' as const, })); if (docType === 'expenseReport') { setModalData({ documentNo: formData.basicInfo.documentNo, createdAt: formData.basicInfo.draftDate, requestDate: formData.expenseReportData?.requestDate || '', paymentDate: formData.expenseReportData?.paymentDate || '', items: formData.expenseReportData?.items.map((item, index) => ({ id: item.id, no: index + 1, description: item.description, amount: item.amount, note: item.note, })) || [], cardInfo: formData.expenseReportData?.cardId || '-', totalAmount: formData.expenseReportData?.totalAmount || 0, attachments: formData.expenseReportData?.uploadedFiles?.map(f => f.name) || [], approvers, drafter, } as ExpenseReportDocumentData); } else { const uploadedFileUrls = (formData.proposalData?.uploadedFiles || []).map(f => `/api/proxy/files/${f.id}/download` ); setModalData({ documentNo: formData.basicInfo.documentNo, createdAt: formData.basicInfo.draftDate, vendor: formData.proposalData?.vendor || '-', vendorPaymentDate: formData.proposalData?.vendorPaymentDate || '', title: formData.proposalData?.title || '', description: formData.proposalData?.description || '-', reason: formData.proposalData?.reason || '-', estimatedCost: formData.proposalData?.estimatedCost || 0, attachments: uploadedFileUrls, approvers, drafter, } as ProposalDocumentData); } } else { toast.error(result.error || '문서 조회에 실패했습니다.'); setDocumentModalOpen(false); } } catch { toast.error('문서 조회 중 오류가 발생했습니다.'); setDocumentModalOpen(false); } finally { setIsModalLoading(false); } }, [approvalId, sourceDocument]); // ===== 핸들러 ===== const handleVendorChange = useCallback((clientId: string) => { const client = clients.find(c => c.id === clientId); if (client) { setVendorId(client.id); setVendorName(client.name); } }, [clients]); // ===== 저장 (IntegratedDetailTemplate 호환) ===== const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { if (!vendorId) { toast.warning('거래처를 선택해주세요.'); return { success: false, error: '거래처를 선택해주세요.' }; } setIsSaving(true); const purchaseData: Partial = { purchaseDate, vendorId, supplyAmount: totals.supplyAmount, vat: totals.vat, totalAmount: totals.total, purchaseType, taxInvoiceReceived, }; try { let result; if (isNewMode) { result = await createPurchase(purchaseData); } else if (purchaseId) { result = await updatePurchase(purchaseId, purchaseData); } if (result?.success) { toast.success(isNewMode ? '매입이 등록되었습니다.' : '매입이 수정되었습니다.'); return { success: true }; } else { toast.error(result?.error || '저장에 실패했습니다.'); return { success: false, error: result?.error || '저장에 실패했습니다.' }; } } catch { toast.error('저장 중 오류가 발생했습니다.'); return { success: false, error: '저장 중 오류가 발생했습니다.' }; } finally { setIsSaving(false); } }, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId]); // ===== 삭제 (IntegratedDetailTemplate 호환) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { if (!purchaseId) return { success: false, error: 'ID가 없습니다.' }; try { const result = await deletePurchase(purchaseId); if (result.success) { toast.success('매입이 삭제되었습니다.'); return { success: true }; } else { toast.error(result.error || '삭제에 실패했습니다.'); return { success: false, error: result.error || '삭제에 실패했습니다.' }; } } catch { toast.error('삭제 중 오류가 발생했습니다.'); return { success: false, error: '삭제 중 오류가 발생했습니다.' }; } }, [purchaseId]); // ===== 폼 내용 렌더링 ===== const renderFormContent = () => ( <>
{/* ===== 기본 정보 섹션 ===== */} 기본 정보 {/* 품의서/지출결의서인 경우 전용 레이아웃 */} {sourceDocument ? ( <> {/* 문서 타입 및 열람 버튼 */}
{sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} 연결된 문서가 있습니다
{/* 품의서/지출결의서용 필드 */}
{/* 품의서/지출결의서 제목 */}
{/* 예상비용 */}
{/* 매입번호 */}
{/* 거래처명 */}
{/* 매입 유형 */}
) : ( /* 일반 매입 (품의서/지출결의서 없는 경우) */
{/* 매입번호 */}
{/* 매입일 */}
{/* 거래처명 */}
{/* 매입 유형 */}
)}
{/* ===== 품목 정보 섹션 ===== */} 품목 정보 items={items} getItemName={(item) => item.itemName} getQuantity={(item) => item.quantity} getUnitPrice={(item) => item.unitPrice} getSupplyAmount={(item) => item.supplyPrice} getVat={(item) => item.vat} getNote={(item) => item.note ?? ''} onItemChange={handleItemChange} onAddItem={handleAddItem} onRemoveItem={handleRemoveItem} totals={totals} isViewMode={isViewMode} /> {/* ===== 세금계산서 섹션 ===== */} 세금계산서
{taxInvoiceReceived ? ( 수취완료 ) : ( 미수취 )}
{/* ===== 품의서/지출결의서 문서 열람 모달 ===== */} {modalData && ( { setDocumentModalOpen(open); if (!open) setModalData(null); }} mode="reference" documentType={sourceDocument?.type === 'expense_report' ? 'expenseReport' : 'proposal'} data={modalData} /> )} ); // ===== 모드 변환 ===== const templateMode = isNewMode ? 'create' : mode; // ===== 동적 config ===== const dynamicConfig = { ...purchaseConfig, title: isNewMode ? '매입' : '매입 상세', actions: { ...purchaseConfig.actions, submitLabel: isNewMode ? '등록' : '저장', }, }; return ( renderFormContent()} renderForm={() => renderFormContent()} /> ); }