- 배차차량관리 목업→API 연동, 배차정보 다중 행 - ShipmentManagement 출고관리 API 매핑 - BillManagement 리팩토링 (섹션 분리, hooks, constants) - 상품권 actions/types 확장 - 출하관리 캘린더 기본 뷰 week-time
261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { toast } from 'sonner';
|
|
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
|
import { billConfig } from './billConfig';
|
|
import { apiDataToFormData, transformFormDataToApi } from './types';
|
|
import type { BillApiData } from './types';
|
|
import { getBillRaw, createBillRaw, updateBillRaw, deleteBill, getClients } from './actions';
|
|
import { useBillForm } from './hooks/useBillForm';
|
|
import { useBillConditions } from './hooks/useBillConditions';
|
|
import {
|
|
BasicInfoSection,
|
|
ElectronicBillSection,
|
|
ExchangeBillSection,
|
|
DiscountInfoSection,
|
|
EndorsementSection,
|
|
CollectionSection,
|
|
HistorySection,
|
|
RenewalSection,
|
|
RecourseSection,
|
|
BuybackSection,
|
|
DishonoredSection,
|
|
} from './sections';
|
|
import { useDetailData } from '@/hooks';
|
|
|
|
interface BillDetailProps {
|
|
billId: string;
|
|
mode: 'view' | 'edit' | 'new';
|
|
}
|
|
|
|
interface ClientOption {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export function BillDetail({ billId, mode }: BillDetailProps) {
|
|
const router = useRouter();
|
|
const isViewMode = mode === 'view';
|
|
const isNewMode = mode === 'new';
|
|
|
|
// 거래처 목록
|
|
const [clients, setClients] = useState<ClientOption[]>([]);
|
|
|
|
// V8 폼 훅
|
|
const {
|
|
formData,
|
|
updateField,
|
|
handleInstrumentTypeChange,
|
|
handleDirectionChange,
|
|
addInstallment,
|
|
removeInstallment,
|
|
updateInstallment,
|
|
setFormDataFull,
|
|
} = useBillForm();
|
|
|
|
// 조건부 표시 플래그
|
|
const conditions = useBillConditions(formData);
|
|
|
|
// 거래처 목록 로드
|
|
useEffect(() => {
|
|
async function loadClients() {
|
|
const result = await getClients();
|
|
if (result.success && result.data) {
|
|
setClients(result.data.map(c => ({ id: String(c.id), name: c.name })));
|
|
}
|
|
}
|
|
loadClients();
|
|
}, []);
|
|
|
|
// API 데이터 로딩 (BillApiData 그대로)
|
|
const fetchBillWrapper = useCallback(
|
|
(id: string | number) => getBillRaw(String(id)),
|
|
[]
|
|
);
|
|
|
|
const {
|
|
data: billApiData,
|
|
isLoading,
|
|
error: loadError,
|
|
} = useDetailData<BillApiData>(
|
|
billId !== 'new' ? billId : null,
|
|
fetchBillWrapper,
|
|
{ skip: isNewMode }
|
|
);
|
|
|
|
// API 데이터 → V8 폼 데이터로 변환
|
|
useEffect(() => {
|
|
if (billApiData) {
|
|
setFormDataFull(apiDataToFormData(billApiData));
|
|
}
|
|
}, [billApiData, setFormDataFull]);
|
|
|
|
// 로드 에러
|
|
useEffect(() => {
|
|
if (loadError) {
|
|
toast.error(loadError);
|
|
router.push('/ko/accounting/bills');
|
|
}
|
|
}, [loadError, router]);
|
|
|
|
// 유효성 검사
|
|
const validateForm = useCallback((): { valid: boolean; error?: string } => {
|
|
if (!formData.billNumber.trim()) return { valid: false, error: '어음번호를 입력해주세요.' };
|
|
const vendorId = conditions.isReceived ? formData.vendor : formData.payee;
|
|
if (!vendorId) return { valid: false, error: '거래처를 선택해주세요.' };
|
|
if (formData.amount <= 0) return { valid: false, error: '금액을 입력해주세요.' };
|
|
if (!formData.issueDate) return { valid: false, error: '발행일을 입력해주세요.' };
|
|
if (conditions.isBill && !formData.maturityDate) return { valid: false, error: '만기일을 입력해주세요.' };
|
|
return { valid: true };
|
|
}, [formData, conditions.isReceived, conditions.isBill]);
|
|
|
|
// 제출
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
|
const validation = validateForm();
|
|
if (!validation.valid) {
|
|
toast.error(validation.error!);
|
|
return { success: false, error: validation.error };
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const vendorName = clients.find(c => c.id === (conditions.isReceived ? formData.vendor : formData.payee))?.name || '';
|
|
const apiPayload = transformFormDataToApi(formData, vendorName);
|
|
|
|
if (isNewMode) {
|
|
const result = await createBillRaw(apiPayload);
|
|
if (result.success) {
|
|
toast.success('등록되었습니다.');
|
|
router.push('/ko/accounting/bills');
|
|
return { success: false, error: '' };
|
|
}
|
|
return result;
|
|
} else {
|
|
const result = await updateBillRaw(String(billId), apiPayload);
|
|
return result;
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [formData, clients, conditions.isReceived, isNewMode, billId, validateForm, router]);
|
|
|
|
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
|
setIsDeleting(true);
|
|
try {
|
|
return await deleteBill(String(billId));
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
}, [billId]);
|
|
|
|
// 폼 콘텐츠 렌더링
|
|
const renderFormContent = () => (
|
|
<>
|
|
{/* 1. 기본 정보 */}
|
|
<BasicInfoSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isViewMode={isViewMode}
|
|
clients={clients}
|
|
conditions={conditions}
|
|
onInstrumentTypeChange={handleInstrumentTypeChange}
|
|
onDirectionChange={handleDirectionChange}
|
|
/>
|
|
|
|
{/* 2. 전자어음 정보 */}
|
|
{conditions.showElectronic && (
|
|
<ElectronicBillSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 3. 환어음 정보 */}
|
|
{conditions.showExchangeBill && (
|
|
<ExchangeBillSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isViewMode={isViewMode}
|
|
showAcceptanceRefusal={conditions.showAcceptanceRefusal}
|
|
/>
|
|
)}
|
|
|
|
{/* 4. 할인 정보 */}
|
|
{conditions.showDiscount && (
|
|
<DiscountInfoSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 5. 배서양도 정보 */}
|
|
{conditions.showEndorsement && (
|
|
<EndorsementSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 6. 추심 정보 */}
|
|
{conditions.showCollection && (
|
|
<CollectionSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 7. 이력 관리 (받을어음만) */}
|
|
{conditions.isReceived && (
|
|
<HistorySection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isViewMode={isViewMode}
|
|
isElectronic={conditions.isElectronic}
|
|
maxSplitCount={conditions.maxSplitCount}
|
|
onAddInstallment={addInstallment}
|
|
onRemoveInstallment={removeInstallment}
|
|
onUpdateInstallment={updateInstallment}
|
|
/>
|
|
)}
|
|
|
|
{/* 8. 개서 정보 */}
|
|
{conditions.showRenewal && (
|
|
<RenewalSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 9. 소구 정보 */}
|
|
{conditions.showRecourse && (
|
|
<RecourseSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 10. 환매 정보 */}
|
|
{conditions.showBuyback && (
|
|
<BuybackSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
|
|
{/* 11. 부도 정보 */}
|
|
{conditions.showDishonored && (
|
|
<DishonoredSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
|
|
)}
|
|
</>
|
|
);
|
|
|
|
// 템플릿 설정
|
|
const templateMode = isNewMode ? 'create' : mode;
|
|
const dynamicConfig = {
|
|
...billConfig,
|
|
title: isViewMode ? '어음/수표 상세' : '어음/수표',
|
|
actions: {
|
|
...billConfig.actions,
|
|
submitLabel: isNewMode ? '등록' : '저장',
|
|
},
|
|
};
|
|
|
|
return (
|
|
<IntegratedDetailTemplate
|
|
config={dynamicConfig}
|
|
mode={templateMode}
|
|
initialData={{}}
|
|
itemId={billId}
|
|
isLoading={isLoading || isSubmitting || isDeleting}
|
|
onSubmit={handleSubmit}
|
|
onDelete={billId && billId !== 'new' ? handleDelete : undefined}
|
|
renderView={() => renderFormContent()}
|
|
renderForm={() => renderFormContent()}
|
|
/>
|
|
);
|
|
}
|