Files
sam-react-prod/src/components/outbound/VehicleDispatchManagement/VehicleDispatchEdit.tsx
유병철 00a6209347 feat: 레이아웃/출하/생산/회계/대시보드 전반 개선
- HeaderFavoritesBar 대폭 개선
- Sidebar/AuthenticatedLayout 소폭 수정
- ShipmentCreate, VehicleDispatch 출하 관련 개선
- WorkOrderCreate/Edit, WorkerScreen 생산 관련 개선
- InspectionCreate 자재 입고검사 개선
- DailyReport, VendorDetail 회계 수정
- CEO 대시보드: CardManagement/DailyProduction/DailyAttendance 섹션 개선
- useCEODashboard, expense transformer 정비
- DocumentViewer, PDF generate route 소폭 수정
- bill-prototype 개발 페이지 추가
- mockData 불필요 데이터 제거
2026-03-05 13:35:48 +09:00

380 lines
13 KiB
TypeScript

'use client';
/**
* 배차차량 수정 페이지
* 3개 섹션: 기본정보(운임비용만 편집), 배차정보(모두 편집), 배송비정보(공급가액 편집→자동계산)
*/
import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Input } from '@/components/ui/input';
import { DateTimePicker } from '@/components/ui/date-time-picker';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { vehicleDispatchEditConfig } from './vehicleDispatchConfig';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { getVehicleDispatchById, updateVehicleDispatch } from './actions';
import {
VEHICLE_DISPATCH_STATUS_LABELS,
VEHICLE_DISPATCH_STATUS_STYLES,
FREIGHT_COST_LABELS,
} from './types';
import type {
VehicleDispatchDetail,
VehicleDispatchEditFormData,
FreightCostType,
} from './types';
import { formatNumber as formatAmount } from '@/lib/utils/amount';
// 운임비용 옵션
const freightCostOptions: { value: FreightCostType; label: string }[] = Object.entries(
FREIGHT_COST_LABELS
).map(([value, label]) => ({ value: value as FreightCostType, label }));
interface VehicleDispatchEditProps {
id: string;
}
export function VehicleDispatchEdit({ id }: VehicleDispatchEditProps) {
const router = useRouter();
// 상세 데이터
const [detail, setDetail] = useState<VehicleDispatchDetail | null>(null);
// 폼 상태
const [formData, setFormData] = useState<VehicleDispatchEditFormData>({
freightCostType: 'prepaid',
logisticsCompany: '',
arrivalDateTime: '',
tonnage: '',
vehicleNo: '',
driverContact: '',
remarks: '',
supplyAmount: 0,
vat: 0,
totalAmount: 0,
});
// 로딩/에러
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await getVehicleDispatchById(id);
if (result.success && result.data) {
const d = result.data;
setDetail(d);
setFormData({
freightCostType: d.freightCostType,
logisticsCompany: d.logisticsCompany,
arrivalDateTime: d.arrivalDateTime,
tonnage: d.tonnage,
vehicleNo: d.vehicleNo,
driverContact: d.driverContact,
remarks: d.remarks,
supplyAmount: d.supplyAmount,
vat: d.vat,
totalAmount: d.totalAmount,
});
} else {
setError(result.error || '배차차량 정보를 불러오는 데 실패했습니다.');
}
} catch (err) {
console.error('[VehicleDispatchEdit] loadData error:', err);
setError('데이터를 불러오는 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}, [id]);
useEffect(() => {
loadData();
}, [loadData]);
// 공급가액 변경 → 부가세/합계 자동 계산
const handleSupplyAmountChange = useCallback((value: string) => {
const amount = parseInt(value.replace(/[^0-9]/g, ''), 10) || 0;
const vat = Math.round(amount * 0.1);
const total = amount + vat;
setFormData(prev => ({
...prev,
supplyAmount: amount,
vat,
totalAmount: total,
}));
if (Object.keys(validationErrors).length > 0) setValidationErrors({});
}, [validationErrors]);
// 폼 입력 핸들러
const handleInputChange = (field: keyof VehicleDispatchEditFormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
if (Object.keys(validationErrors).length > 0) setValidationErrors({});
};
const handleCancel = useCallback(() => {
router.push(`/ko/outbound/vehicle-dispatches/${id}?mode=view`);
}, [router, id]);
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
setIsSubmitting(true);
try {
const result = await updateVehicleDispatch(id, formData);
if (!result.success) {
return { success: false, error: result.error || '배차차량 수정에 실패했습니다.' };
}
return { success: true };
} catch (err) {
if (isNextRedirectError(err)) throw err;
console.error('[VehicleDispatchEdit] handleSubmit error:', err);
const errorMessage = err instanceof Error ? err.message : '저장 중 오류가 발생했습니다.';
return { success: false, error: errorMessage };
} finally {
setIsSubmitting(false);
}
}, [id, formData]);
// 동적 config
const dynamicConfig = {
...vehicleDispatchEditConfig,
title: detail?.dispatchNo ? `배차차량 수정 (${detail.dispatchNo})` : '배차차량 수정',
};
// 폼 컨텐츠 렌더링
const renderFormContent = useCallback(
(_props: {
formData: Record<string, unknown>;
onChange: (key: string, value: unknown) => void;
mode: string;
errors: Record<string, string>;
}) => {
if (!detail) return null;
return (
<div className="space-y-6">
{/* 상태 배지 */}
<div className="flex items-center gap-2">
<Badge className={`text-xs ${VEHICLE_DISPATCH_STATUS_STYLES[detail.status]}`}>
{VEHICLE_DISPATCH_STATUS_LABELS[detail.status]}
</Badge>
</div>
{/* 카드 1: 기본 정보 (운임비용만 편집 가능) */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.dispatchNo}</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.lotNo || detail.shipmentNo}</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.siteName}</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.orderCustomer}</div>
</div>
<div className="space-y-2">
<Label></Label>
<Select
key={`freight-${formData.freightCostType}`}
value={formData.freightCostType}
onValueChange={(value) => handleInputChange('freightCostType', value)}
disabled={isSubmitting}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{freightCostOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div>
<Badge className={VEHICLE_DISPATCH_STATUS_STYLES[detail.status]}>
{VEHICLE_DISPATCH_STATUS_LABELS[detail.status]}
</Badge>
</div>
</div>
<div className="space-y-1">
<Label className="text-muted-foreground"></Label>
<div className="font-medium">{detail.writer}</div>
</div>
</div>
</CardContent>
</Card>
{/* 카드 2: 배차 정보 (모두 편집 가능) */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
value={formData.logisticsCompany}
onChange={(e) => handleInputChange('logisticsCompany', e.target.value)}
placeholder="물류업체명"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label></Label>
<DateTimePicker
value={formData.arrivalDateTime}
onChange={(val) => handleInputChange('arrivalDateTime', val)}
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.tonnage}
onChange={(e) => handleInputChange('tonnage', e.target.value)}
placeholder="예: 3.5 톤"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.vehicleNo}
onChange={(e) => handleInputChange('vehicleNo', e.target.value)}
placeholder="예: 경기12차1234"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.driverContact}
onChange={(e) => handleInputChange('driverContact', e.target.value)}
placeholder="010-0000-0000"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value={formData.remarks}
onChange={(e) => handleInputChange('remarks', e.target.value)}
placeholder="비고"
disabled={isSubmitting}
/>
</div>
</div>
</CardContent>
</Card>
{/* 카드 3: 배송비 정보 (공급가액 편집 → 자동계산) */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label></Label>
<Input
type="text"
value={formatAmount(formData.supplyAmount)}
onChange={(e) => handleSupplyAmountChange(e.target.value)}
placeholder="0"
disabled={isSubmitting}
/>
</div>
<div className="space-y-2">
<Label> (10% )</Label>
<Input
type="text"
value={formatAmount(formData.vat)}
readOnly
className="bg-muted"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
type="text"
value={formatAmount(formData.totalAmount)}
readOnly
className="bg-muted font-bold"
/>
</div>
</div>
</CardContent>
</Card>
</div>
);
},
[detail, formData, validationErrors, isSubmitting, handleSupplyAmountChange]
);
if (error && !isLoading) {
return (
<IntegratedDetailTemplate
config={dynamicConfig}
mode="edit"
isLoading={false}
onCancel={handleCancel}
renderForm={(_props: {
formData: Record<string, unknown>;
onChange: (key: string, value: unknown) => void;
mode: string;
errors: Record<string, string>;
}) => (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-900">
{error || '배차차량 정보를 찾을 수 없습니다.'}
</div>
)}
/>
);
}
return (
<IntegratedDetailTemplate
config={dynamicConfig}
mode="edit"
isLoading={isLoading}
onCancel={handleCancel}
onSubmit={async () => {
return await handleSubmit();
}}
renderForm={renderFormContent}
/>
);
}