diff --git a/src/components/approval/ApprovalBox/actions.ts b/src/components/approval/ApprovalBox/actions.ts index fa8c6779..b85a09ca 100644 --- a/src/components/approval/ApprovalBox/actions.ts +++ b/src/components/approval/ApprovalBox/actions.ts @@ -139,8 +139,8 @@ function mapDocumentStatus(status: string): string { * API 데이터 → 프론트엔드 데이터 변환 */ function transformApiToFrontend(data: InboxApiData): ApprovalRecord { - // 현재 사용자의 결재 단계 정보 추출 - const currentStep = data.steps?.find(s => s.step_type === 'approval'); + // 현재 사용자의 결재 단계 정보 추출 ('approval' 또는 'agreement' 타입) + const currentStep = data.steps?.find(s => s.step_type === 'approval' || s.step_type === 'agreement'); const approver = currentStep?.approver; const stepStatus = currentStep?.status || 'pending'; diff --git a/src/components/approval/ApprovalBox/index.tsx b/src/components/approval/ApprovalBox/index.tsx index 1d939eac..7c1be24e 100644 --- a/src/components/approval/ApprovalBox/index.tsx +++ b/src/components/approval/ApprovalBox/index.tsx @@ -20,6 +20,7 @@ import { approveDocumentsBulk, rejectDocumentsBulk, } from './actions'; +import { getApprovalById } from '@/components/approval/DocumentCreate/actions'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; @@ -109,6 +110,8 @@ export function ApprovalBox() { // ===== 문서 상세 모달 상태 ===== const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); + const [modalData, setModalData] = useState(null); + const [isModalLoading, setIsModalLoading] = useState(false); // API 데이터 const [data, setData] = useState([]); @@ -274,9 +277,118 @@ export function ApprovalBox() { }, [pendingSelectedItems, rejectComment, pendingClearSelection, loadData]); // ===== 문서 클릭 핸들러 ===== - const handleDocumentClick = useCallback((item: ApprovalRecord) => { + const handleDocumentClick = useCallback(async (item: ApprovalRecord) => { setSelectedDocument(item); + setIsModalLoading(true); setIsModalOpen(true); + + try { + const result = await getApprovalById(parseInt(item.id)); + if (result.success && result.data) { + const formData = result.data; + const docType = getDocumentType(item.approvalType); + + // 기안자 정보 + 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, index) => ({ + id: person.id, + name: person.name, + position: person.position, + department: person.department, + status: + item.status === 'approved' + ? ('approved' as const) + : item.status === 'rejected' + ? ('rejected' as const) + : index === 0 + ? ('pending' as const) + : ('none' as const), + })); + + let convertedData: ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData; + + switch (docType) { + case 'expenseEstimate': + convertedData = { + documentNo: formData.basicInfo.documentNo, + createdAt: formData.basicInfo.draftDate, + items: formData.expenseEstimateData?.items.map(item => ({ + id: item.id, + expectedPaymentDate: item.expectedPaymentDate, + category: item.category, + amount: item.amount, + vendor: item.vendor, + account: item.memo || '', + })) || [], + totalExpense: formData.expenseEstimateData?.totalExpense || 0, + accountBalance: formData.expenseEstimateData?.accountBalance || 0, + finalDifference: formData.expenseEstimateData?.finalDifference || 0, + approvers, + drafter, + }; + break; + case 'expenseReport': + convertedData = { + 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, + }; + break; + default: + // 품의서 + const uploadedFileUrls = (formData.proposalData?.uploadedFiles || []).map(f => + `/api/proxy/files/${f.id}/download` + ); + convertedData = { + documentNo: formData.basicInfo.documentNo, + createdAt: formData.basicInfo.draftDate, + vendor: formData.proposalData?.vendor || '-', + vendorPaymentDate: formData.proposalData?.vendorPaymentDate || '', + title: formData.proposalData?.title || item.title, + description: formData.proposalData?.description || '-', + reason: formData.proposalData?.reason || '-', + estimatedCost: formData.proposalData?.estimatedCost || 0, + attachments: uploadedFileUrls, + approvers, + drafter, + }; + break; + } + + setModalData(convertedData); + } else { + toast.error(result.error || '문서 조회에 실패했습니다.'); + setIsModalOpen(false); + } + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('Failed to load document:', error); + toast.error('문서를 불러오는데 실패했습니다.'); + setIsModalOpen(false); + } finally { + setIsModalLoading(false); + } }, []); const handleModalEdit = useCallback(() => { @@ -333,77 +445,7 @@ export function ApprovalBox() { 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( () => [ { @@ -739,12 +781,17 @@ export function ApprovalBox() { {/* 문서 상세 모달 */} - {selectedDocument && ( + {selectedDocument && modalData && ( { + setIsModalOpen(open); + if (!open) { + setModalData(null); + } + }} documentType={getDocumentType(selectedDocument.approvalType)} - data={convertToModalData(selectedDocument)} + data={modalData} mode="inbox" onEdit={handleModalEdit} onCopy={handleModalCopy} @@ -778,6 +825,7 @@ export function ApprovalBox() { handleRejectConfirm, selectedDocument, isModalOpen, + modalData, handleModalEdit, handleModalCopy, handleModalApprove, diff --git a/src/components/approval/DocumentCreate/ProposalForm.tsx b/src/components/approval/DocumentCreate/ProposalForm.tsx index 454e2aec..19ee8c78 100644 --- a/src/components/approval/DocumentCreate/ProposalForm.tsx +++ b/src/components/approval/DocumentCreate/ProposalForm.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect, useState } from 'react'; import { Mic } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -8,14 +9,61 @@ import { CurrencyInput } from '@/components/ui/currency-input'; import { Textarea } from '@/components/ui/textarea'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { getClients } from '@/components/accounting/VendorManagement/actions'; import type { ProposalData, UploadedFile } from './types'; +// 거래처 옵션 타입 +interface ClientOption { + id: string; + name: string; +} + interface ProposalFormProps { data: ProposalData; onChange: (data: ProposalData) => void; } export function ProposalForm({ data, onChange }: ProposalFormProps) { + // 거래처 목록 상태 + const [clients, setClients] = useState([]); + const [isLoadingClients, setIsLoadingClients] = useState(true); + + // 거래처 목록 로드 (매입 거래처만) + useEffect(() => { + async function loadClients() { + setIsLoadingClients(true); + const result = await getClients({ size: 1000, only_active: true }); + if (result.success) { + // 매입 거래처(purchase, both)만 필터링 + const purchaseClients = result.data + .filter((v) => v.category === 'purchase' || v.category === 'both') + .map((v) => ({ + id: v.id, + name: v.vendorName, + })); + setClients(purchaseClients); + } + setIsLoadingClients(false); + } + loadClients(); + }, []); + + // 거래처 선택 핸들러 + const handleVendorChange = (vendorId: string) => { + const selected = clients.find((c) => c.id === vendorId); + onChange({ + ...data, + vendorId, + vendor: selected?.name || '', + }); + }; const handleFilesSelect = (files: File[]) => { onChange({ ...data, attachments: [...data.attachments, ...files] }); }; @@ -41,12 +89,22 @@ export function ProposalForm({ data, onChange }: ProposalFormProps) {
- onChange({ ...data, vendor: e.target.value })} - /> +
diff --git a/src/components/approval/DocumentCreate/actions.ts b/src/components/approval/DocumentCreate/actions.ts index fc2e18dc..f05bdc24 100644 --- a/src/components/approval/DocumentCreate/actions.ts +++ b/src/components/approval/DocumentCreate/actions.ts @@ -760,7 +760,8 @@ function transformApiToFormData(apiData: { department, }; - if (step.step_type === 'approval') { + // 'approval'과 'agreement' 모두 결재선에 포함 + if (step.step_type === 'approval' || step.step_type === 'agreement') { approvalLine.push(person); } else if (step.step_type === 'reference') { references.push(person); @@ -808,6 +809,7 @@ function transformApiToFormData(apiData: { if (documentType === 'proposal') { proposalData = { + vendorId: (content.vendorId as string) || '', vendor: (content.vendor as string) || '', vendorPaymentDate: (content.vendorPaymentDate as string) || '', title: (content.title as string) || '', @@ -894,6 +896,7 @@ function getDocumentContent( switch (formData.basicInfo.documentType) { case 'proposal': return { + vendorId: formData.proposalData?.vendorId, vendor: formData.proposalData?.vendor, vendorPaymentDate: formData.proposalData?.vendorPaymentDate, title: formData.proposalData?.title, diff --git a/src/components/approval/DocumentCreate/index.tsx b/src/components/approval/DocumentCreate/index.tsx index d9b18256..9aa641e6 100644 --- a/src/components/approval/DocumentCreate/index.tsx +++ b/src/components/approval/DocumentCreate/index.tsx @@ -45,6 +45,7 @@ import type { } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { useDevFill, generatePurchaseApprovalData } from '@/components/dev'; +import { getClients } from '@/components/accounting/VendorManagement/actions'; // 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정 const getInitialBasicInfo = (): BasicInfo => ({ @@ -55,6 +56,7 @@ const getInitialBasicInfo = (): BasicInfo => ({ }); const getInitialProposalData = (): ProposalData => ({ + vendorId: '', vendor: '', vendorPaymentDate: '', // 클라이언트에서 설정 title: '', @@ -131,6 +133,19 @@ export function DocumentCreate() { // 직원 목록 가져오기 const employees = await getEmployees(); + // 거래처 목록 가져오기 (매입 거래처만) + const clientsResult = await getClients({ size: 1000, only_active: true }); + const purchaseClients = clientsResult.success + ? clientsResult.data + .filter((v) => v.category === 'purchase' || v.category === 'both') + .map((v) => ({ id: v.id, name: v.vendorName })) + : []; + + // 랜덤 거래처 선택 + const randomClient = purchaseClients.length > 0 + ? purchaseClients[Math.floor(Math.random() * purchaseClients.length)] + : null; + // localStorage에서 실제 로그인 사용자 이름 가져오기 (우측 상단 표시와 동일한 소스) const userDataStr = localStorage.getItem("user"); const currentUserName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name; @@ -171,6 +186,9 @@ export function DocumentCreate() { setProposalData(prev => ({ ...prev, ...mockData.proposalData, + // 실제 API 거래처로 덮어쓰기 + vendorId: randomClient?.id || '', + vendor: randomClient?.name || '', })); toast.success('지출결의서 데이터가 자동 입력되었습니다.'); } diff --git a/src/components/approval/DocumentCreate/types.ts b/src/components/approval/DocumentCreate/types.ts index abe45f18..f296228a 100644 --- a/src/components/approval/DocumentCreate/types.ts +++ b/src/components/approval/DocumentCreate/types.ts @@ -38,7 +38,8 @@ export interface BasicInfo { // 품의서 데이터 export interface ProposalData { - vendor: string; + vendorId: string; // 거래처 ID (API 연동) + vendor: string; // 거래처명 (표시용) vendorPaymentDate: string; title: string; description: string;