feat: 생산/품질/자재/출고/주문 관리 페이지 구현
- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
314
src/components/outbound/ShipmentManagement/ShipmentCreate.tsx
Normal file
314
src/components/outbound/ShipmentManagement/ShipmentCreate.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하 등록 페이지
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Truck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import {
|
||||
mockLotOptions,
|
||||
mockLogisticsOptions,
|
||||
mockVehicleTonnageOptions,
|
||||
priorityOptions,
|
||||
deliveryMethodOptions,
|
||||
} from './mockData';
|
||||
import type { ShipmentCreateFormData, ShipmentPriority, DeliveryMethod } from './types';
|
||||
|
||||
export function ShipmentCreate() {
|
||||
const router = useRouter();
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState<ShipmentCreateFormData>({
|
||||
lotNo: '',
|
||||
scheduledDate: new Date().toISOString().split('T')[0],
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
logisticsCompany: '',
|
||||
vehicleTonnage: '',
|
||||
loadingTime: '',
|
||||
loadingManager: '',
|
||||
remarks: '',
|
||||
});
|
||||
|
||||
// validation 에러 상태
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// 폼 입력 핸들러
|
||||
const handleInputChange = (field: keyof ShipmentCreateFormData, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 입력 시 에러 클리어
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationErrors([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소
|
||||
const handleCancel = () => {
|
||||
router.push('/ko/outbound/shipments');
|
||||
};
|
||||
|
||||
// validation 체크
|
||||
const validateForm = (): boolean => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 필수 필드 체크
|
||||
if (!formData.lotNo) {
|
||||
errors.push('로트번호는 필수 선택 항목입니다.');
|
||||
}
|
||||
if (!formData.scheduledDate) {
|
||||
errors.push('출고예정일은 필수 입력 항목입니다.');
|
||||
}
|
||||
if (!formData.priority) {
|
||||
errors.push('출고 우선순위는 필수 선택 항목입니다.');
|
||||
}
|
||||
if (!formData.deliveryMethod) {
|
||||
errors.push('배송방식은 필수 선택 항목입니다.');
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSubmit = () => {
|
||||
// validation 체크
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: API 호출
|
||||
console.log('Submit:', formData);
|
||||
router.push('/ko/outbound/shipments');
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
<Truck className="w-6 h-6" />
|
||||
<h1 className="text-xl font-semibold">출하 등록</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation 에러 표시 */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({validationErrors.length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 수주 선택 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">수주 선택</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label>로트번호 *</Label>
|
||||
<Select
|
||||
value={formData.lotNo}
|
||||
onValueChange={(value) => handleInputChange('lotNo', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="로트 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockLotOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label} ({option.customerName} - {option.siteName})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 출고 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">출고 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>출고예정일 *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduledDate}
|
||||
onChange={(e) => handleInputChange('scheduledDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>출고 우선순위 *</Label>
|
||||
<Select
|
||||
value={formData.priority}
|
||||
onValueChange={(value) => handleInputChange('priority', value as ShipmentPriority)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="우선순위 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{priorityOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>배송방식 *</Label>
|
||||
<Select
|
||||
value={formData.deliveryMethod}
|
||||
onValueChange={(value) => handleInputChange('deliveryMethod', value as DeliveryMethod)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="배송방식 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deliveryMethodOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 상차 (물류업체) - 배송방식이 상차 또는 물류사일 때 표시 */}
|
||||
{(formData.deliveryMethod === 'pickup' || formData.deliveryMethod === 'logistics') && (
|
||||
<Card className="bg-muted/30">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">상차 (물류업체)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>물류사</Label>
|
||||
<Select
|
||||
value={formData.logisticsCompany || ''}
|
||||
onValueChange={(value) => handleInputChange('logisticsCompany', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockLogisticsOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>차량 톤수 (물량)</Label>
|
||||
<Select
|
||||
value={formData.vehicleTonnage || ''}
|
||||
onValueChange={(value) => handleInputChange('vehicleTonnage', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockVehicleTonnageOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>상차시간 (입차예정)</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formData.loadingTime || ''}
|
||||
onChange={(e) => handleInputChange('loadingTime', e.target.value)}
|
||||
placeholder="연도. 월. 일. -- --:--"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
물류업체와 입차시간 확정 후 입력하세요.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>상차담당자</Label>
|
||||
<Input
|
||||
value={formData.loadingManager || ''}
|
||||
onChange={(e) => handleInputChange('loadingManager', e.target.value)}
|
||||
placeholder="상차 작업 담당자명"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>비고</Label>
|
||||
<Textarea
|
||||
value={formData.remarks || ''}
|
||||
onChange={(e) => handleInputChange('remarks', e.target.value)}
|
||||
placeholder="특이사항 입력"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
351
src/components/outbound/ShipmentManagement/ShipmentDetail.tsx
Normal file
351
src/components/outbound/ShipmentManagement/ShipmentDetail.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하 상세 페이지
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Truck, FileText, Receipt, ClipboardList, Check, Printer, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { mockShipmentDetail } from './mockData';
|
||||
import {
|
||||
SHIPMENT_STATUS_LABELS,
|
||||
SHIPMENT_STATUS_STYLES,
|
||||
PRIORITY_LABELS,
|
||||
PRIORITY_STYLES,
|
||||
DELIVERY_METHOD_LABELS,
|
||||
} from './types';
|
||||
import { ShippingSlip } from './documents/ShippingSlip';
|
||||
import { TransactionStatement } from './documents/TransactionStatement';
|
||||
import { DeliveryConfirmation } from './documents/DeliveryConfirmation';
|
||||
|
||||
interface ShipmentDetailProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function ShipmentDetail({ id }: ShipmentDetailProps) {
|
||||
const router = useRouter();
|
||||
const [previewDocument, setPreviewDocument] = useState<'shipping' | 'transaction' | 'delivery' | null>(null);
|
||||
|
||||
// Mock 데이터 사용 (실제로는 API에서 id로 조회)
|
||||
const data = mockShipmentDetail;
|
||||
|
||||
// 목록으로 이동
|
||||
const handleGoBack = () => {
|
||||
router.push('/ko/outbound/shipments');
|
||||
};
|
||||
|
||||
// 수정 페이지로 이동
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/outbound/shipments/${id}/edit`);
|
||||
};
|
||||
|
||||
// 인쇄
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
// 정보 영역 렌더링
|
||||
const renderInfoField = (label: string, value: React.ReactNode, className?: string) => (
|
||||
<div className={className}>
|
||||
<div className="text-sm text-muted-foreground mb-1">{label}</div>
|
||||
<div className="font-medium">{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Truck className="w-6 h-6" />
|
||||
<h1 className="text-xl font-semibold">출하 상세</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 문서 미리보기 버튼 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPreviewDocument('shipping')}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-1" />
|
||||
출고증
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPreviewDocument('transaction')}
|
||||
>
|
||||
<Receipt className="w-4 h-4 mr-1" />
|
||||
거래명세서
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPreviewDocument('delivery')}
|
||||
>
|
||||
<ClipboardList className="w-4 h-4 mr-1" />
|
||||
납품확인서
|
||||
</Button>
|
||||
<div className="w-px h-6 bg-border mx-2" />
|
||||
<Button variant="outline" onClick={handleGoBack}>
|
||||
목록
|
||||
</Button>
|
||||
<Button onClick={handleEdit}>
|
||||
수정
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
<div className="space-y-6">
|
||||
{/* 출고 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">출고 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{renderInfoField('출고번호', data.shipmentNo)}
|
||||
{renderInfoField('출고예정일', data.scheduledDate)}
|
||||
{renderInfoField('로트번호', data.lotNo)}
|
||||
{renderInfoField(
|
||||
'출고상태',
|
||||
<Badge className={SHIPMENT_STATUS_STYLES[data.status]}>
|
||||
{SHIPMENT_STATUS_LABELS[data.status]}
|
||||
</Badge>
|
||||
)}
|
||||
{renderInfoField(
|
||||
'출고 우선순위',
|
||||
<Badge className={PRIORITY_STYLES[data.priority]}>
|
||||
{PRIORITY_LABELS[data.priority]}
|
||||
</Badge>
|
||||
)}
|
||||
{renderInfoField(
|
||||
'배송방식',
|
||||
<Badge variant="outline">
|
||||
{DELIVERY_METHOD_LABELS[data.deliveryMethod]}
|
||||
</Badge>
|
||||
)}
|
||||
{renderInfoField(
|
||||
'입금확인',
|
||||
data.depositConfirmed ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<Check className="w-4 h-4" />
|
||||
확인됨
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">미확인</span>
|
||||
)
|
||||
)}
|
||||
{renderInfoField(
|
||||
'세금계산서',
|
||||
data.invoiceIssued ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<Check className="w-4 h-4" />
|
||||
발행됨
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">미발행</span>
|
||||
)
|
||||
)}
|
||||
{renderInfoField('거래처 등급', data.customerGrade)}
|
||||
{renderInfoField(
|
||||
'출하가능',
|
||||
data.canShip ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<Check className="w-4 h-4" />
|
||||
가능
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-red-600">
|
||||
<X className="w-4 h-4" />
|
||||
불가
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{renderInfoField('상차담당자', data.loadingManager || '-')}
|
||||
{renderInfoField(
|
||||
'상차완료',
|
||||
data.loadingCompleted ? (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<Check className="w-4 h-4" />
|
||||
완료 ({data.loadingCompleted})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)
|
||||
)}
|
||||
{renderInfoField('등록자', data.registrant || '-')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 발주처/배송 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">발주처/배송 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{renderInfoField('발주처', data.customerName)}
|
||||
{renderInfoField('현장명', data.siteName)}
|
||||
{renderInfoField('배송주소', data.deliveryAddress, 'md:col-span-2')}
|
||||
{renderInfoField('인수자', data.receiver || '-')}
|
||||
{renderInfoField('연락처', data.receiverContact || '-')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 출고 품목 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">출고 품목</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12 text-center">No</TableHead>
|
||||
<TableHead className="w-24">품목코드</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead className="w-24">층/M호</TableHead>
|
||||
<TableHead className="w-28">규격</TableHead>
|
||||
<TableHead className="w-16 text-center">수량</TableHead>
|
||||
<TableHead className="w-36">LOT번호</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.products.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell className="text-center">{product.no}</TableCell>
|
||||
<TableCell>{product.itemCode}</TableCell>
|
||||
<TableCell>{product.itemName}</TableCell>
|
||||
<TableCell>{product.floorUnit}</TableCell>
|
||||
<TableCell>{product.specification}</TableCell>
|
||||
<TableCell className="text-center">{product.quantity}</TableCell>
|
||||
<TableCell>{product.lotNo}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 배차 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">배차 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{renderInfoField('배송방식', DELIVERY_METHOD_LABELS[data.deliveryMethod])}
|
||||
{renderInfoField('물류사', data.logisticsCompany || '-')}
|
||||
{renderInfoField('차량 톤수', data.vehicleTonnage || '-')}
|
||||
{renderInfoField('운송비', data.shippingCost !== undefined ? `${data.shippingCost.toLocaleString()}원` : '-')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 차량/운전자 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">차량/운전자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{renderInfoField('차량번호', data.vehicleNo || '-')}
|
||||
{renderInfoField('운전자', data.driverName || '-')}
|
||||
{renderInfoField('운전자 연락처', data.driverContact || '-')}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 비고 */}
|
||||
{data.remarks && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">비고</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm whitespace-pre-wrap">{data.remarks}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* 문서 미리보기 다이얼로그 - 작업일지 모달 패턴 적용 */}
|
||||
<Dialog open={previewDocument !== null} onOpenChange={() => setPreviewDocument(null)}>
|
||||
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto p-0 gap-0 bg-gray-100">
|
||||
{/* 접근성을 위한 숨겨진 타이틀 */}
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>
|
||||
{previewDocument === 'shipping' && '출고증'}
|
||||
{previewDocument === 'transaction' && '거래명세서'}
|
||||
{previewDocument === 'delivery' && '납품확인서'}
|
||||
</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
|
||||
{/* 모달 헤더 - 작업일지 스타일 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b bg-white sticky top-0 z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-semibold text-lg">
|
||||
{previewDocument === 'shipping' && '출고증 미리보기'}
|
||||
{previewDocument === 'transaction' && '거래명세서 미리보기'}
|
||||
{previewDocument === 'delivery' && '납품확인서'}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{data.customerName}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({data.shipmentNo})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="w-4 h-4 mr-1.5" />
|
||||
인쇄
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setPreviewDocument(null)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 문서 본문 - 흰색 카드 형태 */}
|
||||
<div className="m-6 p-6 bg-white rounded-lg shadow-sm">
|
||||
{previewDocument === 'shipping' && <ShippingSlip data={data} />}
|
||||
{previewDocument === 'transaction' && <TransactionStatement data={data} />}
|
||||
{previewDocument === 'delivery' && <DeliveryConfirmation data={data} />}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
393
src/components/outbound/ShipmentManagement/ShipmentEdit.tsx
Normal file
393
src/components/outbound/ShipmentManagement/ShipmentEdit.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하 수정 페이지
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Truck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import {
|
||||
mockShipmentDetail,
|
||||
mockLogisticsOptions,
|
||||
mockVehicleTonnageOptions,
|
||||
priorityOptions,
|
||||
deliveryMethodOptions,
|
||||
} from './mockData';
|
||||
import type { ShipmentEditFormData, ShipmentPriority, DeliveryMethod } from './types';
|
||||
|
||||
interface ShipmentEditProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function ShipmentEdit({ id }: ShipmentEditProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Mock 데이터에서 초기값 설정 (실제로는 API에서 조회)
|
||||
const detail = mockShipmentDetail;
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState<ShipmentEditFormData>({
|
||||
scheduledDate: detail.scheduledDate,
|
||||
priority: detail.priority,
|
||||
deliveryMethod: detail.deliveryMethod,
|
||||
loadingManager: detail.loadingManager || '',
|
||||
logisticsCompany: detail.logisticsCompany || '',
|
||||
vehicleTonnage: detail.vehicleTonnage || '',
|
||||
vehicleNo: detail.vehicleNo || '',
|
||||
shippingCost: detail.shippingCost,
|
||||
driverName: detail.driverName || '',
|
||||
driverContact: detail.driverContact || '',
|
||||
expectedArrival: '',
|
||||
confirmedArrival: '',
|
||||
changeReason: '',
|
||||
remarks: detail.remarks || '',
|
||||
});
|
||||
|
||||
// validation 에러 상태
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// 폼 입력 핸들러
|
||||
const handleInputChange = (field: keyof ShipmentEditFormData, value: string | number | undefined) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 입력 시 에러 클리어
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationErrors([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소
|
||||
const handleCancel = () => {
|
||||
router.push(`/ko/outbound/shipments/${id}`);
|
||||
};
|
||||
|
||||
// validation 체크
|
||||
const validateForm = (): boolean => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 필수 필드 체크
|
||||
if (!formData.scheduledDate) {
|
||||
errors.push('출고예정일은 필수 입력 항목입니다.');
|
||||
}
|
||||
if (!formData.priority) {
|
||||
errors.push('출고 우선순위는 필수 선택 항목입니다.');
|
||||
}
|
||||
if (!formData.deliveryMethod) {
|
||||
errors.push('배송방식은 필수 선택 항목입니다.');
|
||||
}
|
||||
if (!formData.changeReason.trim()) {
|
||||
errors.push('변경 사유는 필수 입력 항목입니다.');
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSubmit = () => {
|
||||
// validation 체크
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: API 호출
|
||||
console.log('Submit:', formData);
|
||||
router.push(`/ko/outbound/shipments/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
<Truck className="w-6 h-6" />
|
||||
<h1 className="text-xl font-semibold">출고 수정</h1>
|
||||
<span className="text-sm text-muted-foreground">{detail.lotNo}</span>
|
||||
<Badge variant="outline" className="bg-yellow-100 text-yellow-800">
|
||||
출하보류
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation 에러 표시 */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({validationErrors.length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 기본 정보 (읽기 전용) */}
|
||||
<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.shipmentNo}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground">로트번호</Label>
|
||||
<div className="font-medium">{detail.lotNo}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground">발주처</Label>
|
||||
<div className="font-medium">{detail.customerName}</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 md:col-span-4">
|
||||
<Label className="text-muted-foreground">배송주소</Label>
|
||||
<div className="font-medium">{detail.deliveryAddress}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 출고 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">출고 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>출고예정일 *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduledDate}
|
||||
onChange={(e) => handleInputChange('scheduledDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>출고 우선순위 *</Label>
|
||||
<Select
|
||||
value={formData.priority}
|
||||
onValueChange={(value) => handleInputChange('priority', value as ShipmentPriority)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="우선순위 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{priorityOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>배송방식 *</Label>
|
||||
<Select
|
||||
value={formData.deliveryMethod}
|
||||
onValueChange={(value) => handleInputChange('deliveryMethod', value as DeliveryMethod)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="배송방식 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{deliveryMethodOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>상차담당자</Label>
|
||||
<Input
|
||||
value={formData.loadingManager || ''}
|
||||
onChange={(e) => handleInputChange('loadingManager', e.target.value)}
|
||||
placeholder="상차담당자명"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>물류사</Label>
|
||||
<Select
|
||||
value={formData.logisticsCompany || ''}
|
||||
onValueChange={(value) => handleInputChange('logisticsCompany', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockLogisticsOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>차량 톤수</Label>
|
||||
<Select
|
||||
value={formData.vehicleTonnage || ''}
|
||||
onValueChange={(value) => handleInputChange('vehicleTonnage', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockVehicleTonnageOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 배차 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">배차 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>차량번호</Label>
|
||||
<Input
|
||||
value={formData.vehicleNo || ''}
|
||||
onChange={(e) => handleInputChange('vehicleNo', e.target.value)}
|
||||
placeholder="예: 12가 3456"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>운송비</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.shippingCost || ''}
|
||||
onChange={(e) => handleInputChange('shippingCost', parseInt(e.target.value) || undefined)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>운전자명</Label>
|
||||
<Input
|
||||
value={formData.driverName || ''}
|
||||
onChange={(e) => handleInputChange('driverName', e.target.value)}
|
||||
placeholder="운전자명"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>운전자 연락처</Label>
|
||||
<Input
|
||||
value={formData.driverContact || ''}
|
||||
onChange={(e) => handleInputChange('driverContact', e.target.value)}
|
||||
placeholder="010-0000-0000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>입차예정시간</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formData.expectedArrival || ''}
|
||||
onChange={(e) => handleInputChange('expectedArrival', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>입차확정시간</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={formData.confirmedArrival || ''}
|
||||
onChange={(e) => handleInputChange('confirmedArrival', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 변경 사유 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">변경 사유</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>변경 사유 *</Label>
|
||||
<Input
|
||||
value={formData.changeReason}
|
||||
onChange={(e) => handleInputChange('changeReason', e.target.value)}
|
||||
placeholder="변경 사유를 입력하세요 (예: 고객 요청, 물류사 일정 조율 등)"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>비고</Label>
|
||||
<Textarea
|
||||
value={formData.remarks || ''}
|
||||
onChange={(e) => handleInputChange('remarks', e.target.value)}
|
||||
placeholder="특이사항 입력"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
339
src/components/outbound/ShipmentManagement/ShipmentList.tsx
Normal file
339
src/components/outbound/ShipmentManagement/ShipmentList.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출하 목록 페이지
|
||||
* IntegratedListTemplateV2 패턴 적용
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Truck,
|
||||
Package,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
Eye,
|
||||
Plus,
|
||||
Check,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
IntegratedListTemplateV2,
|
||||
TabOption,
|
||||
TableColumn,
|
||||
StatCard,
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { mockShipmentItems, mockStats, mockFilterTabs } from './mockData';
|
||||
import {
|
||||
SHIPMENT_STATUS_LABELS,
|
||||
SHIPMENT_STATUS_STYLES,
|
||||
PRIORITY_LABELS,
|
||||
PRIORITY_STYLES,
|
||||
DELIVERY_METHOD_LABELS,
|
||||
} from './types';
|
||||
import type { ShipmentItem, ShipmentStatus } from './types';
|
||||
|
||||
// 페이지당 항목 수
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
export function ShipmentList() {
|
||||
const router = useRouter();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [activeFilter, setActiveFilter] = useState<string>('all');
|
||||
|
||||
// 출고 불가 건수 계산
|
||||
const cannotShipCount = useMemo(() => {
|
||||
return mockShipmentItems.filter(item => !item.canShip).length;
|
||||
}, []);
|
||||
|
||||
// 통계 카드
|
||||
const stats: StatCard[] = [
|
||||
{
|
||||
label: '당일 출하',
|
||||
value: `${mockStats.todayShipmentCount}건`,
|
||||
icon: Package,
|
||||
iconColor: 'text-green-600',
|
||||
},
|
||||
{
|
||||
label: '출고 대기',
|
||||
value: `${mockStats.scheduledCount}건`,
|
||||
icon: Clock,
|
||||
iconColor: 'text-yellow-600',
|
||||
},
|
||||
{
|
||||
label: '배송중',
|
||||
value: `${mockStats.shippingCount}건`,
|
||||
icon: Truck,
|
||||
iconColor: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
label: '긴급 출하',
|
||||
value: `${mockStats.urgentCount}건`,
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-red-600',
|
||||
},
|
||||
];
|
||||
|
||||
// 테이블 컬럼
|
||||
const tableColumns: TableColumn[] = [
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'shipmentNo', label: '출고번호', className: 'min-w-[130px]' },
|
||||
{ key: 'lotNo', label: '로트번호', className: 'min-w-[150px]' },
|
||||
{ key: 'scheduledDate', label: '출고예정일', className: 'w-[120px] text-center' },
|
||||
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
|
||||
{ key: 'canShip', label: '출하가능', className: 'w-[80px] text-center' },
|
||||
{ key: 'deliveryMethod', label: '배송', className: 'w-[100px] text-center' },
|
||||
{ key: 'customerName', label: '발주처', className: 'min-w-[120px]' },
|
||||
{ key: 'siteName', label: '현장명', className: 'min-w-[120px]' },
|
||||
{ key: 'manager', label: '담당', className: 'w-[80px] text-center' },
|
||||
{ key: 'deliveryTime', label: '납기(수배시간)', className: 'w-[120px] text-center' },
|
||||
];
|
||||
|
||||
// 필터링된 데이터
|
||||
const filteredResults = useMemo(() => {
|
||||
let result = [...mockShipmentItems];
|
||||
|
||||
// 탭 필터
|
||||
if (activeFilter !== 'all' && activeFilter !== 'calendar') {
|
||||
result = result.filter((item) => item.status === activeFilter);
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
result = result.filter(
|
||||
(item) =>
|
||||
item.shipmentNo.toLowerCase().includes(term) ||
|
||||
item.lotNo.toLowerCase().includes(term) ||
|
||||
item.customerName.toLowerCase().includes(term) ||
|
||||
item.siteName.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [searchTerm, activeFilter]);
|
||||
|
||||
// 페이지네이션 데이터
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
return filteredResults.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
||||
}, [filteredResults, currentPage]);
|
||||
|
||||
// 페이지네이션 설정
|
||||
const pagination = {
|
||||
currentPage,
|
||||
totalPages: Math.ceil(filteredResults.length / ITEMS_PER_PAGE),
|
||||
totalItems: filteredResults.length,
|
||||
itemsPerPage: ITEMS_PER_PAGE,
|
||||
onPageChange: setCurrentPage,
|
||||
};
|
||||
|
||||
// 선택 핸들러
|
||||
const handleToggleSelection = useCallback((id: string) => {
|
||||
setSelectedItems((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleToggleSelectAll = useCallback(() => {
|
||||
if (selectedItems.size === paginatedData.length) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
setSelectedItems(new Set(paginatedData.map((item) => item.id)));
|
||||
}
|
||||
}, [paginatedData, selectedItems.size]);
|
||||
|
||||
// 검색 변경 시 페이지 리셋
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchTerm(value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// 탭 변경 시 페이지 리셋
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveFilter(value);
|
||||
setCurrentPage(1);
|
||||
setSelectedItems(new Set());
|
||||
};
|
||||
|
||||
// 상세 보기
|
||||
const handleView = useCallback(
|
||||
(id: string) => {
|
||||
router.push(`/ko/outbound/shipments/${id}`);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
// 등록 페이지로 이동
|
||||
const handleCreate = () => {
|
||||
router.push('/ko/outbound/shipments/new');
|
||||
};
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = (item: ShipmentItem, index: number, globalIndex: number) => {
|
||||
const isSelected = selectedItems.has(item.id);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${isSelected ? 'bg-blue-50' : ''}`}
|
||||
onClick={() => handleView(item.id)}
|
||||
>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => handleToggleSelection(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
||||
<TableCell className="font-medium">{item.shipmentNo}</TableCell>
|
||||
<TableCell>{item.lotNo}</TableCell>
|
||||
<TableCell className="text-center">{item.scheduledDate}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge className={`text-xs ${SHIPMENT_STATUS_STYLES[item.status]}`}>
|
||||
{SHIPMENT_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{item.canShip ? (
|
||||
<Check className="w-4 h-4 mx-auto text-green-600" />
|
||||
) : (
|
||||
<X className="w-4 h-4 mx-auto text-red-600" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{DELIVERY_METHOD_LABELS[item.deliveryMethod]}</TableCell>
|
||||
<TableCell>{item.customerName}</TableCell>
|
||||
<TableCell className="max-w-[120px] truncate">{item.siteName}</TableCell>
|
||||
<TableCell className="text-center">{item.manager || '-'}</TableCell>
|
||||
<TableCell className="text-center">{item.deliveryTime || '-'}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
// 모바일 카드 렌더링
|
||||
const renderMobileCard = (
|
||||
item: ShipmentItem,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
isSelected: boolean,
|
||||
onToggle: () => void
|
||||
) => {
|
||||
return (
|
||||
<ListMobileCard
|
||||
id={item.id}
|
||||
isSelected={isSelected}
|
||||
onToggleSelection={onToggle}
|
||||
onClick={() => handleView(item.id)}
|
||||
headerBadges={
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
#{globalIndex}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.shipmentNo}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
title={item.siteName}
|
||||
statusBadge={
|
||||
<Badge className={`text-xs ${SHIPMENT_STATUS_STYLES[item.status]}`}>
|
||||
{SHIPMENT_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<InfoField label="로트번호" value={item.lotNo} />
|
||||
<InfoField label="발주처" value={item.customerName} />
|
||||
<InfoField label="출고예정일" value={item.scheduledDate} />
|
||||
<InfoField label="배송방식" value={DELIVERY_METHOD_LABELS[item.deliveryMethod]} />
|
||||
<InfoField label="출하가능" value={item.canShip ? '가능' : '불가'} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
isSelected && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleView(item.id);
|
||||
}}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
상세
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 탭 옵션
|
||||
const tabs: TabOption[] = mockFilterTabs.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: tab.label,
|
||||
count: tab.count,
|
||||
}));
|
||||
|
||||
// 헤더 액션
|
||||
const headerActions = (
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
출하 등록
|
||||
</Button>
|
||||
);
|
||||
|
||||
// 카드와 검색 영역 사이에 표시할 경고 알림
|
||||
const alertBanner = cannotShipCount > 0 && (
|
||||
<Alert className="mb-4 bg-orange-50 border-orange-200">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
||||
<AlertDescription className="text-orange-800">
|
||||
출고불가 {cannotShipCount}건 - 입금확인 및 세금계산서 발행 완료 후 출고 진행이 가능합니다.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
return (
|
||||
<IntegratedListTemplateV2<ShipmentItem>
|
||||
title="출하 목록"
|
||||
icon={Truck}
|
||||
stats={stats}
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchPlaceholder="출고번호, 로트번호, 발주처, 현장명 검색..."
|
||||
tabs={tabs}
|
||||
activeTab={activeFilter}
|
||||
onTabChange={handleTabChange}
|
||||
tableColumns={tableColumns}
|
||||
data={paginatedData}
|
||||
totalCount={filteredResults.length}
|
||||
allData={filteredResults}
|
||||
selectedItems={selectedItems}
|
||||
onToggleSelection={handleToggleSelection}
|
||||
onToggleSelectAll={handleToggleSelectAll}
|
||||
getItemId={(item) => item.id}
|
||||
renderTableRow={renderTableRow}
|
||||
renderMobileCard={renderMobileCard}
|
||||
pagination={pagination}
|
||||
headerActions={headerActions}
|
||||
alertBanner={alertBanner}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 납품확인서 미리보기/인쇄 문서
|
||||
*/
|
||||
|
||||
import type { ShipmentDetail } from '../types';
|
||||
|
||||
interface DeliveryConfirmationProps {
|
||||
data: ShipmentDetail;
|
||||
}
|
||||
|
||||
export function DeliveryConfirmation({ data }: DeliveryConfirmationProps) {
|
||||
return (
|
||||
<div className="bg-white p-8 max-w-3xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div>
|
||||
<div className="text-xs">경동기업</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-[1rem]">납 품 확 인 서</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
{/* 헤더: 결재 + 작성/검토/승인 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted" rowSpan={3}>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span>결</span>
|
||||
<span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">작성</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">검토</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">승인</td>
|
||||
</tr>
|
||||
{/* 내용: 서명란 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
<td className="border px-2 py-1 text-center h-10"></td>
|
||||
</tr>
|
||||
{/* 부서 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">판매/<br/>전산</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">출하</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">품질</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 출하 관리부서 */}
|
||||
<div className="text-xs text-muted-foreground mb-2">출하 관리부서</div>
|
||||
|
||||
{/* 연락처 */}
|
||||
<div className="text-xs mb-4">
|
||||
전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com
|
||||
</div>
|
||||
|
||||
{/* 발주정보 / 납품정보 */}
|
||||
<div className="grid grid-cols-2 gap-0 mb-6">
|
||||
<div className="border">
|
||||
<div className="bg-muted px-3 py-2 font-medium text-center border-b">발주정보</div>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted w-24">발주일</td>
|
||||
<td className="border-b px-3 py-2">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">발주처</td>
|
||||
<td className="border-b px-3 py-2">{data.customerName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">담당자</td>
|
||||
<td className="border-b px-3 py-2">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">연락처</td>
|
||||
<td className="border-b px-3 py-2">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r px-3 py-2 bg-muted">제품 LOT NO.</td>
|
||||
<td className="px-3 py-2">{data.lotNo}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="border border-l-0">
|
||||
<div className="bg-muted px-3 py-2 font-medium text-center border-b">납품정보</div>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted w-24">납품일</td>
|
||||
<td className="border-b px-3 py-2">{data.scheduledDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">현장명</td>
|
||||
<td className="border-b px-3 py-2">{data.siteName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">인수업체명</td>
|
||||
<td className="border-b px-3 py-2">현판업</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r border-b px-3 py-2 bg-muted">인수자연락처</td>
|
||||
<td className="border-b px-3 py-2">{data.receiverContact}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border-r px-3 py-2 bg-muted">납품지 주소</td>
|
||||
<td className="px-3 py-2">{data.deliveryAddress}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 납품 품목 */}
|
||||
<h3 className="font-medium mb-2">납품 품목</h3>
|
||||
<table className="w-full border-collapse mb-6 text-xs">
|
||||
<thead>
|
||||
<tr className="bg-muted">
|
||||
<th className="border px-2 py-2 text-center w-12">No</th>
|
||||
<th className="border px-2 py-2 text-left">품 명</th>
|
||||
<th className="border px-2 py-2 text-center w-24">규 격</th>
|
||||
<th className="border px-2 py-2 text-center w-16">단위</th>
|
||||
<th className="border px-2 py-2 text-center w-16">수량</th>
|
||||
<th className="border px-2 py-2 text-center w-20">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.products.map((product, index) => (
|
||||
<tr key={product.id}>
|
||||
<td className="border px-2 py-2 text-center">{product.no}</td>
|
||||
<td className="border px-2 py-2">{product.itemName}</td>
|
||||
<td className="border px-2 py-2 text-center">{product.specification}</td>
|
||||
<td className="border px-2 py-2 text-center">SET</td>
|
||||
<td className="border px-2 py-2 text-center">{product.quantity}</td>
|
||||
<td className="border px-2 py-2 text-center">{product.floorUnit}</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* 빈 행 채우기 (최소 10행) */}
|
||||
{Array.from({ length: Math.max(0, 10 - data.products.length) }).map((_, i) => (
|
||||
<tr key={`empty-${i}`}>
|
||||
<td className="border px-2 py-2 text-center">{data.products.length + i + 1}</td>
|
||||
<td className="border px-2 py-2"> </td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 특기사항 */}
|
||||
<div className="border p-4 mb-6">
|
||||
<h3 className="font-medium mb-2">특기사항</h3>
|
||||
<div className="min-h-16 text-xs">
|
||||
위 물품을 상기와 같이 납품합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 서명 영역 */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* 납품자 */}
|
||||
<div className="border p-4">
|
||||
<h3 className="font-medium mb-3 text-center">납 품 자</h3>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-2 w-20">회 사 명</td>
|
||||
<td className="py-2 font-medium">경동기업</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2">담 당 자</td>
|
||||
<td className="py-2">전진</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2">서명/날인</td>
|
||||
<td className="py-2"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 인수자 */}
|
||||
<div className="border p-4">
|
||||
<h3 className="font-medium mb-3 text-center">인 수 자</h3>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-2 w-20">회 사 명</td>
|
||||
<td className="py-2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2">담 당 자</td>
|
||||
<td className="py-2"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2">서명/날인</td>
|
||||
<td className="py-2"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 확인 문구 */}
|
||||
<p className="text-center text-xs text-muted-foreground mt-6">
|
||||
상기 물품을 정히 인수하였음을 확인합니다.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 출고증 미리보기/인쇄 문서
|
||||
*/
|
||||
|
||||
import type { ShipmentDetail } from '../types';
|
||||
|
||||
interface ShippingSlipProps {
|
||||
data: ShipmentDetail;
|
||||
}
|
||||
|
||||
export function ShippingSlip({ data }: ShippingSlipProps) {
|
||||
return (
|
||||
<div className="bg-white p-8 max-w-4xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 헤더 */}
|
||||
<div className="flex justify-between items-start mb-6 border-b pb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-2xl font-bold">KD</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">경동기업</div>
|
||||
<div className="text-xs text-muted-foreground">KYUNGDONG COMPANY</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold tracking-widest">출 고 증</div>
|
||||
<table className="text-xs border-collapse">
|
||||
<tbody>
|
||||
{/* 헤더: 결재 + 작성/검토/승인 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted" rowSpan={3}>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<span>결</span>
|
||||
<span>재</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-20">작성</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">검토</td>
|
||||
<td className="border px-2 py-1 bg-muted text-center w-16">승인</td>
|
||||
</tr>
|
||||
{/* 내용: 담당자 정보 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center">
|
||||
<div>판매1팀 임</div>
|
||||
<div>판매</div>
|
||||
<div className="text-muted-foreground">12-20</div>
|
||||
</td>
|
||||
<td className="border px-2 py-1 text-center"></td>
|
||||
<td className="border px-2 py-1 text-center"></td>
|
||||
</tr>
|
||||
{/* 부서 */}
|
||||
<tr>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">판매/전진</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">출하</td>
|
||||
<td className="border px-2 py-1 text-center bg-muted/50">생산관리</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 출하 관리 */}
|
||||
<div className="text-xs text-muted-foreground mb-2">출하 관리</div>
|
||||
|
||||
{/* LOT 및 연락처 정보 */}
|
||||
<div className="grid grid-cols-5 gap-2 mb-4 text-xs">
|
||||
<div className="border px-2 py-1 bg-muted font-medium">제품 LOT NO.</div>
|
||||
<div className="border px-2 py-1">{data.lotNo}</div>
|
||||
<div className="border px-2 py-1 col-span-3">
|
||||
전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품목 정보 */}
|
||||
<div className="grid grid-cols-5 gap-0 mb-4 text-xs border">
|
||||
<div className="border px-2 py-1 bg-muted font-medium">상품명</div>
|
||||
<div className="border px-2 py-1">국산철물스크린폴리스페서</div>
|
||||
<div className="border px-2 py-1 bg-muted font-medium">제품코드</div>
|
||||
<div className="border px-2 py-1">KWE01</div>
|
||||
<div className="border px-2 py-1 bg-muted font-medium">단발호</div>
|
||||
<div className="border px-2 py-1 col-span-4">FDS-0T523-0117-4</div>
|
||||
</div>
|
||||
|
||||
{/* 신청업체 / 신청내용 / 납품정보 */}
|
||||
<table className="w-full border-collapse mb-6 text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted text-center" colSpan={2}>신청업체</th>
|
||||
<th className="border px-2 py-1 bg-muted text-center" colSpan={2}>신청내용</th>
|
||||
<th className="border px-2 py-1 bg-muted text-center" colSpan={2}>납품정보</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted">발주일</td>
|
||||
<td className="border px-2 py-1">-</td>
|
||||
<td className="border px-2 py-1 bg-muted">현장</td>
|
||||
<td className="border px-2 py-1">위브 청라</td>
|
||||
<td className="border px-2 py-1 bg-muted">인수업체</td>
|
||||
<td className="border px-2 py-1">{data.customerName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted">발주처</td>
|
||||
<td className="border px-2 py-1">{data.customerName}</td>
|
||||
<td className="border px-2 py-1 bg-muted">납기일정일</td>
|
||||
<td className="border px-2 py-1">-</td>
|
||||
<td className="border px-2 py-1 bg-muted">인수자연락처</td>
|
||||
<td className="border px-2 py-1">{data.receiverContact}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted">발주 담당자</td>
|
||||
<td className="border px-2 py-1">-</td>
|
||||
<td className="border px-2 py-1 bg-muted">출고일</td>
|
||||
<td className="border px-2 py-1">{data.scheduledDate}</td>
|
||||
<td className="border px-2 py-1 bg-muted">인수자</td>
|
||||
<td className="border px-2 py-1">{data.receiver || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted">담당자 연락처</td>
|
||||
<td className="border px-2 py-1">-</td>
|
||||
<td className="border px-2 py-1 bg-muted">세미물류수</td>
|
||||
<td className="border px-2 py-1">-</td>
|
||||
<td className="border px-2 py-1 bg-muted">출고 방법</td>
|
||||
<td className="border px-2 py-1">직접배차 직접</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1 bg-muted" colSpan={2}>배송지 주소</td>
|
||||
<td className="border px-2 py-1" colSpan={4}>{data.deliveryAddress}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 1. 부자재 */}
|
||||
<div className="mb-4">
|
||||
<h3 className="font-medium mb-2">1. 부자재 - 강기샤프트, 라파이프, 앵글</h3>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted">강기샤프트</th>
|
||||
<th className="border px-2 py-1 bg-muted">수량</th>
|
||||
<th className="border px-2 py-1 bg-muted">강기샤프트</th>
|
||||
<th className="border px-2 py-1 bg-muted">수량</th>
|
||||
<th className="border px-2 py-1 bg-muted">라파이프</th>
|
||||
<th className="border px-2 py-1 bg-muted">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">4인치<br />L : 3,000<br />L : 4,500</td>
|
||||
<td className="border px-2 py-1 text-center">1</td>
|
||||
<td className="border px-2 py-1">5인치<br />L : 6,000<br />L : 7,000</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
<td className="border px-2 py-1">(50*30*1.4T)<br />L : 6,000</td>
|
||||
<td className="border px-2 py-1 text-center">5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1" colSpan={4}>※ 별도 추가사항 - 부자재</td>
|
||||
<td className="border px-2 py-1 bg-muted">앵글<br />(40*40*3T)</td>
|
||||
<td className="border px-2 py-1 text-center">4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 2. 모터 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="font-medium mb-2">2. 모터</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted" colSpan={3}>2-1. 모터(220V 단상)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted">모터 용량</th>
|
||||
<th className="border px-2 py-1 bg-muted">수량</th>
|
||||
<th className="border px-2 py-1 bg-muted">입고 LOT NO.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">KD-300K</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
<td className="border px-2 py-1"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted" colSpan={3}>2-2. 모터(380V 삼상)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted">모터 용량</th>
|
||||
<th className="border px-2 py-1 bg-muted">수량</th>
|
||||
<th className="border px-2 py-1 bg-muted">입고 LOT NO.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">KD-300K<br />KD-400K</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
<td className="border px-2 py-1"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted" colSpan={2}>2-3. 브라켓트</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">380*180 (2-4")</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">380*180 (2-5")</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border px-2 py-1 bg-muted" colSpan={2}>2-4. 연동파이프</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">매립형</td>
|
||||
<td className="border px-2 py-1 text-center">1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border px-2 py-1">노출형</td>
|
||||
<td className="border px-2 py-1 text-center">0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="flex justify-end gap-2 print:hidden">
|
||||
<button className="px-4 py-1 border rounded text-sm">출고 담당</button>
|
||||
<button className="px-4 py-1 border rounded text-sm">인수 확인</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 거래명세서 미리보기/인쇄 문서
|
||||
*/
|
||||
|
||||
import type { ShipmentDetail } from '../types';
|
||||
|
||||
interface TransactionStatementProps {
|
||||
data: ShipmentDetail;
|
||||
}
|
||||
|
||||
export function TransactionStatement({ data }: TransactionStatementProps) {
|
||||
// 제품 합계 계산
|
||||
const totalAmount = data.products.reduce((sum, product) => {
|
||||
// 실제로는 단가 * 수량
|
||||
return sum + 0;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-8 max-w-3xl mx-auto text-sm print:p-0 print:max-w-none">
|
||||
{/* 제목 */}
|
||||
<h1 className="text-2xl font-bold text-center tracking-[1rem] mb-8">
|
||||
거 래 명 세 서
|
||||
</h1>
|
||||
<p className="text-center text-xs text-muted-foreground mb-6">
|
||||
TRANSACTION STATEMENT
|
||||
</p>
|
||||
|
||||
{/* 공급받는자 / 공급자 정보 */}
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
{/* 공급받는자 */}
|
||||
<div className="border p-4">
|
||||
<h2 className="font-bold mb-3 text-muted-foreground">공급받는자</h2>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-1 w-16">상 호</td>
|
||||
<td className="py-1 font-medium">{data.customerName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">현장명</td>
|
||||
<td className="py-1">{data.siteName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">주 소</td>
|
||||
<td className="py-1">{data.deliveryAddress}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 공급자 */}
|
||||
<div className="border p-4">
|
||||
<h2 className="font-bold mb-3 text-muted-foreground">공급자</h2>
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="py-1 w-16">상 호</td>
|
||||
<td className="py-1 font-medium">경동기업</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">대표자</td>
|
||||
<td className="py-1">홍길동</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">주 소</td>
|
||||
<td className="py-1">경기도 화성시 팔탄면 산업로 123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-1">연락처</td>
|
||||
<td className="py-1">031-123-4567</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 거래일자 / 출고번호 */}
|
||||
<div className="grid grid-cols-2 gap-6 mb-6 text-xs">
|
||||
<div className="flex">
|
||||
<span className="font-medium mr-4">거래일자</span>
|
||||
<span>{data.scheduledDate}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium mr-4">출고번호</span>
|
||||
<span>{data.shipmentNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품목 테이블 */}
|
||||
<table className="w-full border-collapse mb-6 text-xs">
|
||||
<thead>
|
||||
<tr className="bg-muted">
|
||||
<th className="border px-2 py-2 text-center w-12">No</th>
|
||||
<th className="border px-2 py-2 text-left">품 목 명</th>
|
||||
<th className="border px-2 py-2 text-center w-24">규 격</th>
|
||||
<th className="border px-2 py-2 text-center w-16">수량</th>
|
||||
<th className="border px-2 py-2 text-center w-20">단가</th>
|
||||
<th className="border px-2 py-2 text-center w-20">금액</th>
|
||||
<th className="border px-2 py-2 text-center w-20">비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.products.map((product, index) => (
|
||||
<tr key={product.id}>
|
||||
<td className="border px-2 py-2 text-center">{product.no}</td>
|
||||
<td className="border px-2 py-2">{product.itemName}</td>
|
||||
<td className="border px-2 py-2 text-center">{product.specification}</td>
|
||||
<td className="border px-2 py-2 text-center">{product.quantity}</td>
|
||||
<td className="border px-2 py-2 text-right">0</td>
|
||||
<td className="border px-2 py-2 text-right">0</td>
|
||||
<td className="border px-2 py-2 text-center">{product.floorUnit}</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* 빈 행 채우기 */}
|
||||
{Array.from({ length: Math.max(0, 5 - data.products.length) }).map((_, i) => (
|
||||
<tr key={`empty-${i}`}>
|
||||
<td className="border px-2 py-2"> </td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-muted">
|
||||
<td className="border px-2 py-2 text-center font-medium" colSpan={5}>
|
||||
합 계
|
||||
</td>
|
||||
<td className="border px-2 py-2 text-right font-medium">{totalAmount}</td>
|
||||
<td className="border px-2 py-2"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{/* 확인 문구 */}
|
||||
<p className="text-center text-sm text-muted-foreground mb-4">
|
||||
위와 같이 거래하였음을 확인합니다.
|
||||
</p>
|
||||
|
||||
{/* 회사명 */}
|
||||
<p className="text-center text-lg font-bold">경동기업</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/components/outbound/ShipmentManagement/index.ts
Normal file
11
src/components/outbound/ShipmentManagement/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 출하관리 컴포넌트 Export
|
||||
*/
|
||||
|
||||
export { ShipmentList } from './ShipmentList';
|
||||
export { ShipmentCreate } from './ShipmentCreate';
|
||||
export { ShipmentDetail } from './ShipmentDetail';
|
||||
export { ShipmentEdit } from './ShipmentEdit';
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
289
src/components/outbound/ShipmentManagement/mockData.ts
Normal file
289
src/components/outbound/ShipmentManagement/mockData.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 출하관리 Mock 데이터
|
||||
*/
|
||||
|
||||
import type {
|
||||
ShipmentItem,
|
||||
ShipmentDetail,
|
||||
ShipmentStats,
|
||||
ShipmentFilterTab,
|
||||
LotOption,
|
||||
LogisticsOption,
|
||||
VehicleTonnageOption,
|
||||
} from './types';
|
||||
|
||||
// 통계 데이터
|
||||
export const mockStats: ShipmentStats = {
|
||||
todayShipmentCount: 3,
|
||||
scheduledCount: 5,
|
||||
shippingCount: 1,
|
||||
urgentCount: 4,
|
||||
};
|
||||
|
||||
// 필터 탭
|
||||
export const mockFilterTabs: ShipmentFilterTab[] = [
|
||||
{ key: 'all', label: '전체', count: 20 },
|
||||
{ key: 'scheduled', label: '출고예정', count: 5 },
|
||||
{ key: 'ready', label: '출하대기', count: 1 },
|
||||
{ key: 'shipping', label: '배송중', count: 1 },
|
||||
{ key: 'completed', label: '배송완료', count: 12 },
|
||||
{ key: 'calendar', label: '출하일정', count: 0 },
|
||||
];
|
||||
|
||||
// 출하 목록 Mock 데이터
|
||||
export const mockShipmentItems: ShipmentItem[] = [
|
||||
{
|
||||
id: 'sl-251220-01',
|
||||
shipmentNo: 'SL-251220-01',
|
||||
lotNo: 'KD-TS-251217-10',
|
||||
scheduledDate: '2025-12-20',
|
||||
status: 'completed',
|
||||
priority: 'urgent',
|
||||
deliveryMethod: 'direct',
|
||||
customerName: '두산건설(주)',
|
||||
siteName: '위브 청라',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251222-03',
|
||||
shipmentNo: 'SL-251222-03',
|
||||
lotNo: 'KD-TS-251217-09',
|
||||
scheduledDate: '2025-12-22',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'logistics',
|
||||
customerName: '태영건설(주)',
|
||||
siteName: '대시앙 동탄',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251224-01',
|
||||
shipmentNo: 'SL-251224-01',
|
||||
lotNo: 'KD-TS-251217-08',
|
||||
scheduledDate: '2025-12-24',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'logistics',
|
||||
customerName: '호반건설(주)',
|
||||
siteName: '버킷 광교',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251221-03',
|
||||
shipmentNo: 'SL-251221-03',
|
||||
lotNo: 'KD-TS-251217-06',
|
||||
scheduledDate: '2025-12-21',
|
||||
status: 'shipping',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
customerName: '호반건설(주)',
|
||||
siteName: '버킷 광교',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251223-02',
|
||||
shipmentNo: 'SL-251223-02',
|
||||
lotNo: 'KD-TS-251217-05',
|
||||
scheduledDate: '2025-12-23',
|
||||
status: 'ready',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'direct',
|
||||
customerName: '포스코건설(주)',
|
||||
siteName: '더샵 송도',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251222-02',
|
||||
shipmentNo: 'SL-251222-02',
|
||||
lotNo: 'KD-TS-251217-04',
|
||||
scheduledDate: '2025-12-22',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'direct',
|
||||
customerName: 'GS건설(주)',
|
||||
siteName: '자이 위례',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251222-01',
|
||||
shipmentNo: 'SL-251222-01',
|
||||
lotNo: 'KD-TS-251217-03',
|
||||
scheduledDate: '2025-12-22',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
customerName: '대우건설(주)',
|
||||
siteName: '푸르지오 일산',
|
||||
manager: '-',
|
||||
canShip: true,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
},
|
||||
{
|
||||
id: 'sl-251223-02b',
|
||||
shipmentNo: 'SL-251223-02',
|
||||
lotNo: 'KD-TS-251217-02',
|
||||
scheduledDate: '2025-12-23',
|
||||
status: 'scheduled',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'logistics',
|
||||
customerName: '현대건설(주)',
|
||||
siteName: '힐스테이트 판교',
|
||||
manager: '-',
|
||||
canShip: false,
|
||||
depositConfirmed: false,
|
||||
invoiceIssued: false,
|
||||
},
|
||||
{
|
||||
id: 'sl-251221-02',
|
||||
shipmentNo: 'SL-251221-02',
|
||||
lotNo: 'KD-TS-251217-02',
|
||||
scheduledDate: '2025-12-23',
|
||||
status: 'scheduled',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
customerName: '현대건설(주)',
|
||||
siteName: '힐스테이트 판교',
|
||||
manager: '-',
|
||||
canShip: false,
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: false,
|
||||
},
|
||||
{
|
||||
id: 'sl-251221-01',
|
||||
shipmentNo: 'SL-251221-01',
|
||||
lotNo: 'KD-TS-251217-01',
|
||||
scheduledDate: '2025-12-21',
|
||||
status: 'scheduled',
|
||||
priority: 'normal',
|
||||
deliveryMethod: 'pickup',
|
||||
customerName: '삼성물산(주)',
|
||||
siteName: '래미안 강남 포레스트',
|
||||
manager: '-',
|
||||
canShip: false,
|
||||
depositConfirmed: false,
|
||||
invoiceIssued: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 출하 상세 Mock 데이터
|
||||
export const mockShipmentDetail: ShipmentDetail = {
|
||||
id: 'sl-251220-01',
|
||||
shipmentNo: 'SL-251220-01',
|
||||
lotNo: 'KD-TS-251217-10',
|
||||
scheduledDate: '2025-12-20',
|
||||
status: 'completed',
|
||||
priority: 'urgent',
|
||||
deliveryMethod: 'direct',
|
||||
depositConfirmed: true,
|
||||
invoiceIssued: true,
|
||||
customerGrade: 'B등급',
|
||||
canShip: true,
|
||||
loadingManager: '김상차',
|
||||
loadingCompleted: '2025.12.20.',
|
||||
registrant: '판매1팀 임판매',
|
||||
|
||||
customerName: '두산건설(주)',
|
||||
siteName: '위브 청라',
|
||||
deliveryAddress: '인천시 서구 청라동 456',
|
||||
receiver: '임현장',
|
||||
receiverContact: '010-8901-2345',
|
||||
|
||||
products: [
|
||||
{
|
||||
id: 'prod-1',
|
||||
no: 1,
|
||||
itemCode: 'SH3225',
|
||||
itemName: '스크린 셔터 (와이어)',
|
||||
floorUnit: '18층/M-01',
|
||||
specification: '3200×2500',
|
||||
quantity: 1,
|
||||
lotNo: 'LOT-20251217-001',
|
||||
},
|
||||
],
|
||||
|
||||
logisticsCompany: '-',
|
||||
vehicleTonnage: '3.5톤',
|
||||
shippingCost: undefined,
|
||||
|
||||
vehicleNo: '34사 5678',
|
||||
driverName: '정운전',
|
||||
driverContact: '010-5656-7878',
|
||||
|
||||
remarks: '[통합테스트10] 두산건설 - 전체 플로우 완료 (견적→수주→생산→품질→출하→회계)',
|
||||
};
|
||||
|
||||
// LOT 선택 옵션 (등록 시)
|
||||
export const mockLotOptions: LotOption[] = [
|
||||
{
|
||||
value: 'KD-TS-251217-10',
|
||||
label: 'KD-TS-251217-10',
|
||||
customerName: '두산건설(주)',
|
||||
siteName: '위브 청라',
|
||||
deliveryAddress: '인천시 서구 청라동 456',
|
||||
},
|
||||
{
|
||||
value: 'KD-TS-251217-09',
|
||||
label: 'KD-TS-251217-09',
|
||||
customerName: '태영건설(주)',
|
||||
siteName: '대시앙 동탄',
|
||||
deliveryAddress: '경기도 화성시 동탄순환대로 100',
|
||||
},
|
||||
{
|
||||
value: 'KD-TS-251217-08',
|
||||
label: 'KD-TS-251217-08',
|
||||
customerName: '호반건설(주)',
|
||||
siteName: '버킷 광교',
|
||||
deliveryAddress: '경기도 수원시 영통구 광교로 200',
|
||||
},
|
||||
];
|
||||
|
||||
// 물류사 옵션
|
||||
export const mockLogisticsOptions: LogisticsOption[] = [
|
||||
{ value: 'hanjin', label: '한진물류' },
|
||||
{ value: 'cj', label: 'CJ대한통운' },
|
||||
{ value: 'lotte', label: '롯데글로벌로지스' },
|
||||
{ value: 'hyundai', label: '현대글로비스' },
|
||||
];
|
||||
|
||||
// 차량 톤수 옵션
|
||||
export const mockVehicleTonnageOptions: VehicleTonnageOption[] = [
|
||||
{ value: '1t', label: '1톤' },
|
||||
{ value: '2.5t', label: '2.5톤' },
|
||||
{ value: '3.5t', label: '3.5톤' },
|
||||
{ value: '5t', label: '5톤' },
|
||||
{ value: '11t', label: '11톤' },
|
||||
{ value: '25t', label: '25톤' },
|
||||
];
|
||||
|
||||
// 우선순위 옵션
|
||||
export const priorityOptions = [
|
||||
{ value: 'urgent', label: '긴급' },
|
||||
{ value: 'normal', label: '보통' },
|
||||
{ value: 'low', label: '낮음' },
|
||||
];
|
||||
|
||||
// 배송방식 옵션
|
||||
export const deliveryMethodOptions = [
|
||||
{ value: 'pickup', label: '상차' },
|
||||
{ value: 'direct', label: '직접배차' },
|
||||
{ value: 'logistics', label: '물류사' },
|
||||
];
|
||||
187
src/components/outbound/ShipmentManagement/types.ts
Normal file
187
src/components/outbound/ShipmentManagement/types.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 출하관리 타입 정의
|
||||
*/
|
||||
|
||||
// 출하 상태
|
||||
export type ShipmentStatus =
|
||||
| 'scheduled' // 출고예정
|
||||
| 'ready' // 출하대기
|
||||
| 'shipping' // 배송중
|
||||
| 'completed'; // 배송완료
|
||||
|
||||
// 상태 라벨
|
||||
export const SHIPMENT_STATUS_LABELS: Record<ShipmentStatus, string> = {
|
||||
scheduled: '출고예정',
|
||||
ready: '출하대기',
|
||||
shipping: '배송중',
|
||||
completed: '배송완료',
|
||||
};
|
||||
|
||||
// 상태 스타일
|
||||
export const SHIPMENT_STATUS_STYLES: Record<ShipmentStatus, string> = {
|
||||
scheduled: 'bg-gray-100 text-gray-800',
|
||||
ready: 'bg-yellow-100 text-yellow-800',
|
||||
shipping: 'bg-blue-100 text-blue-800',
|
||||
completed: 'bg-green-100 text-green-800',
|
||||
};
|
||||
|
||||
// 출고 우선순위
|
||||
export type ShipmentPriority = 'urgent' | 'normal' | 'low';
|
||||
|
||||
export const PRIORITY_LABELS: Record<ShipmentPriority, string> = {
|
||||
urgent: '긴급',
|
||||
normal: '보통',
|
||||
low: '낮음',
|
||||
};
|
||||
|
||||
export const PRIORITY_STYLES: Record<ShipmentPriority, string> = {
|
||||
urgent: 'bg-red-100 text-red-800',
|
||||
normal: 'bg-gray-100 text-gray-800',
|
||||
low: 'bg-blue-100 text-blue-800',
|
||||
};
|
||||
|
||||
// 배송방식
|
||||
export type DeliveryMethod = 'pickup' | 'direct' | 'logistics';
|
||||
|
||||
export const DELIVERY_METHOD_LABELS: Record<DeliveryMethod, string> = {
|
||||
pickup: '상차',
|
||||
direct: '직접배차',
|
||||
logistics: '물류사',
|
||||
};
|
||||
|
||||
// 출하 목록 아이템
|
||||
export interface ShipmentItem {
|
||||
id: string;
|
||||
shipmentNo: string; // 출고번호
|
||||
lotNo: string; // 로트번호
|
||||
scheduledDate: string; // 출고예정일
|
||||
status: ShipmentStatus; // 상태
|
||||
priority: ShipmentPriority; // 우선순위
|
||||
deliveryMethod: DeliveryMethod; // 배송방식
|
||||
customerName: string; // 발주처
|
||||
siteName: string; // 현장명
|
||||
manager?: string; // 담당
|
||||
canShip: boolean; // 출하가능
|
||||
depositConfirmed: boolean; // 입금확인
|
||||
invoiceIssued: boolean; // 세금계산서
|
||||
deliveryTime?: string; // 납기(수배시간)
|
||||
}
|
||||
|
||||
// 출고 품목
|
||||
export interface ShipmentProduct {
|
||||
id: string;
|
||||
no: number;
|
||||
itemCode: string; // 품목코드
|
||||
itemName: string; // 품목명
|
||||
floorUnit: string; // 층/M호
|
||||
specification: string; // 규격
|
||||
quantity: number; // 수량
|
||||
lotNo: string; // LOT번호
|
||||
}
|
||||
|
||||
// 출하 상세 정보
|
||||
export interface ShipmentDetail {
|
||||
id: string;
|
||||
shipmentNo: string; // 출고번호
|
||||
lotNo: string; // 로트번호
|
||||
scheduledDate: string; // 출고예정일
|
||||
status: ShipmentStatus; // 출고상태
|
||||
priority: ShipmentPriority; // 출고 우선순위
|
||||
deliveryMethod: DeliveryMethod; // 배송방식
|
||||
depositConfirmed: boolean; // 입금확인
|
||||
invoiceIssued: boolean; // 세금계산서
|
||||
customerGrade: string; // 거래처 등급
|
||||
canShip: boolean; // 출하가능
|
||||
loadingManager?: string; // 상차담당자
|
||||
loadingCompleted?: string; // 상차완료
|
||||
registrant?: string; // 등록자
|
||||
|
||||
// 발주처/배송 정보
|
||||
customerName: string; // 발주처
|
||||
siteName: string; // 현장명
|
||||
deliveryAddress: string; // 배송주소
|
||||
receiver?: string; // 인수자
|
||||
receiverContact?: string; // 연락처
|
||||
|
||||
// 출고 품목
|
||||
products: ShipmentProduct[];
|
||||
|
||||
// 배차 정보
|
||||
logisticsCompany?: string; // 물류사
|
||||
vehicleTonnage?: string; // 차량 톤수
|
||||
shippingCost?: number; // 운송비
|
||||
|
||||
// 차량/운전자 정보
|
||||
vehicleNo?: string; // 차량번호
|
||||
driverName?: string; // 운전자
|
||||
driverContact?: string; // 운전자 연락처
|
||||
|
||||
remarks?: string; // 비고
|
||||
}
|
||||
|
||||
// 출하 등록 폼 데이터
|
||||
export interface ShipmentCreateFormData {
|
||||
lotNo: string; // 로트번호 *
|
||||
scheduledDate: string; // 출고예정일 *
|
||||
priority: ShipmentPriority; // 출고 우선순위 *
|
||||
deliveryMethod: DeliveryMethod; // 배송방식 *
|
||||
logisticsCompany?: string; // 물류사
|
||||
vehicleTonnage?: string; // 차량 톤수(물량)
|
||||
loadingTime?: string; // 상차시간(입차예정)
|
||||
loadingManager?: string; // 상차담당자
|
||||
remarks?: string; // 비고
|
||||
}
|
||||
|
||||
// 출하 수정 폼 데이터
|
||||
export interface ShipmentEditFormData {
|
||||
scheduledDate: string; // 출고예정일 *
|
||||
priority: ShipmentPriority; // 출고 우선순위 *
|
||||
deliveryMethod: DeliveryMethod; // 배송방식 *
|
||||
loadingManager?: string; // 상차담당자
|
||||
logisticsCompany?: string; // 물류사
|
||||
vehicleTonnage?: string; // 차량 톤수
|
||||
vehicleNo?: string; // 차량번호
|
||||
shippingCost?: number; // 운송비
|
||||
driverName?: string; // 운전자명
|
||||
driverContact?: string; // 운전자 연락처
|
||||
expectedArrival?: string; // 입차예정시간
|
||||
confirmedArrival?: string; // 입차확정시간
|
||||
changeReason: string; // 변경 사유 *
|
||||
remarks?: string; // 비고
|
||||
}
|
||||
|
||||
// 통계 데이터
|
||||
export interface ShipmentStats {
|
||||
todayShipmentCount: number; // 당일 출하
|
||||
scheduledCount: number; // 출고 대기
|
||||
shippingCount: number; // 배송중
|
||||
urgentCount: number; // 긴급 출하
|
||||
}
|
||||
|
||||
// 필터 탭
|
||||
export interface ShipmentFilterTab {
|
||||
key: 'all' | 'scheduled' | 'ready' | 'shipping' | 'completed' | 'calendar';
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// LOT 선택 옵션 (등록 시)
|
||||
export interface LotOption {
|
||||
value: string;
|
||||
label: string;
|
||||
customerName: string;
|
||||
siteName: string;
|
||||
deliveryAddress: string;
|
||||
}
|
||||
|
||||
// 물류사 선택 옵션
|
||||
export interface LogisticsOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 차량 톤수 선택 옵션
|
||||
export interface VehicleTonnageOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
Reference in New Issue
Block a user