ERP-node/frontend/components/dashboard/widgets/DeliveryStatusWidget.tsx

422 lines
15 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { RefreshCw, Package, TruckIcon, AlertTriangle, CheckCircle, Clock, XCircle } from "lucide-react";
interface DeliveryItem {
id: string;
trackingNumber: string;
customer: string;
origin: string;
destination: string;
status: "in_transit" | "delivered" | "delayed" | "pickup_waiting";
estimatedDelivery: string;
delayReason?: string;
priority: "high" | "normal" | "low";
}
interface CustomerIssue {
id: string;
customer: string;
trackingNumber: string;
issueType: "damage" | "delay" | "missing" | "other";
description: string;
status: "open" | "in_progress" | "resolved";
reportedAt: string;
}
interface DeliveryStatusWidgetProps {
refreshInterval?: number;
}
export default function DeliveryStatusWidget({ refreshInterval = 60000 }: DeliveryStatusWidgetProps) {
const [deliveries, setDeliveries] = useState<DeliveryItem[]>([]);
const [issues, setIssues] = useState<CustomerIssue[]>([]);
const [todayStats, setTodayStats] = useState({
shipped: 0,
delivered: 0,
});
const [isLoading, setIsLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const loadData = async () => {
setIsLoading(true);
// TODO: 실제 API 연동 시 아래 주석 해제
// try {
// const response = await fetch('/api/delivery/status', {
// headers: {
// 'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
// },
// });
// const data = await response.json();
// setDeliveries(data.deliveries);
// setIssues(data.issues);
// setTodayStats(data.todayStats);
// setLastUpdate(new Date());
// } catch (error) {
// console.error('배송 데이터 로드 실패:', error);
// } finally {
// setIsLoading(false);
// }
// 가상 배송 데이터 (개발용 - 실제 DB 연동 시 삭제)
const dummyDeliveries: DeliveryItem[] = [
{
id: "D001",
trackingNumber: "TRK-2025-001",
customer: "삼성전자",
origin: "서울 물류센터",
destination: "부산 공장",
status: "in_transit",
estimatedDelivery: "2025-10-15 14:00",
priority: "high",
},
{
id: "D002",
trackingNumber: "TRK-2025-002",
customer: "LG화학",
origin: "인천항",
destination: "광주 공장",
status: "delivered",
estimatedDelivery: "2025-10-14 16:30",
priority: "normal",
},
{
id: "D003",
trackingNumber: "TRK-2025-003",
customer: "현대자동차",
origin: "평택 물류센터",
destination: "울산 공장",
status: "delayed",
estimatedDelivery: "2025-10-14 18:00",
delayReason: "교통 체증",
priority: "high",
},
{
id: "D004",
trackingNumber: "TRK-2025-004",
customer: "SK하이닉스",
origin: "이천 물류센터",
destination: "청주 공장",
status: "pickup_waiting",
estimatedDelivery: "2025-10-15 10:00",
priority: "normal",
},
{
id: "D005",
trackingNumber: "TRK-2025-005",
customer: "포스코",
origin: "포항 물류센터",
destination: "광양 제철소",
status: "delayed",
estimatedDelivery: "2025-10-14 20:00",
delayReason: "기상 악화",
priority: "high",
},
];
// 가상 고객 이슈 데이터
const dummyIssues: CustomerIssue[] = [
{
id: "I001",
customer: "삼성전자",
trackingNumber: "TRK-2025-001",
issueType: "delay",
description: "배송 지연으로 인한 생산 일정 차질",
status: "in_progress",
reportedAt: "2025-10-14 15:30",
},
{
id: "I002",
customer: "LG디스플레이",
trackingNumber: "TRK-2024-998",
issueType: "damage",
description: "화물 일부 파손",
status: "open",
reportedAt: "2025-10-14 14:20",
},
{
id: "I003",
customer: "SK이노베이션",
trackingNumber: "TRK-2024-995",
issueType: "missing",
description: "화물 일부 누락",
status: "resolved",
reportedAt: "2025-10-13 16:45",
},
];
setTimeout(() => {
setDeliveries(dummyDeliveries);
setIssues(dummyIssues);
setTodayStats({
shipped: 24,
delivered: 18,
});
setLastUpdate(new Date());
setIsLoading(false);
}, 500);
};
useEffect(() => {
loadData();
const interval = setInterval(loadData, refreshInterval);
return () => clearInterval(interval);
}, [refreshInterval]);
const getStatusColor = (status: DeliveryItem["status"]) => {
switch (status) {
case "in_transit":
return "bg-blue-100 text-blue-700 border-blue-300";
case "delivered":
return "bg-green-100 text-green-700 border-green-300";
case "delayed":
return "bg-red-100 text-red-700 border-red-300";
case "pickup_waiting":
return "bg-yellow-100 text-yellow-700 border-yellow-300";
default:
return "bg-gray-100 text-gray-700 border-gray-300";
}
};
const getStatusText = (status: DeliveryItem["status"]) => {
switch (status) {
case "in_transit":
return "배송중";
case "delivered":
return "완료";
case "delayed":
return "지연";
case "pickup_waiting":
return "픽업 대기";
default:
return "알 수 없음";
}
};
const getStatusIcon = (status: DeliveryItem["status"]) => {
switch (status) {
case "in_transit":
return <TruckIcon className="h-4 w-4" />;
case "delivered":
return <CheckCircle className="h-4 w-4" />;
case "delayed":
return <AlertTriangle className="h-4 w-4" />;
case "pickup_waiting":
return <Clock className="h-4 w-4" />;
default:
return <Package className="h-4 w-4" />;
}
};
const getIssueTypeText = (type: CustomerIssue["issueType"]) => {
switch (type) {
case "damage":
return "파손";
case "delay":
return "지연";
case "missing":
return "누락";
case "other":
return "기타";
default:
return "알 수 없음";
}
};
const getIssueStatusColor = (status: CustomerIssue["status"]) => {
switch (status) {
case "open":
return "bg-red-100 text-red-700 border-red-300";
case "in_progress":
return "bg-yellow-100 text-yellow-700 border-yellow-300";
case "resolved":
return "bg-green-100 text-green-700 border-green-300";
default:
return "bg-gray-100 text-gray-700 border-gray-300";
}
};
const getIssueStatusText = (status: CustomerIssue["status"]) => {
switch (status) {
case "open":
return "접수";
case "in_progress":
return "처리중";
case "resolved":
return "해결";
default:
return "알 수 없음";
}
};
const statusStats = {
in_transit: deliveries.filter((d) => d.status === "in_transit").length,
delivered: deliveries.filter((d) => d.status === "delivered").length,
delayed: deliveries.filter((d) => d.status === "delayed").length,
pickup_waiting: deliveries.filter((d) => d.status === "pickup_waiting").length,
};
const delayedDeliveries = deliveries.filter((d) => d.status === "delayed");
return (
<div className="h-full w-full bg-gradient-to-br from-slate-50 to-blue-50 p-4 overflow-auto">
{/* 헤더 */}
<div className="mb-3 flex items-center justify-between">
<div>
<h3 className="text-lg font-bold text-gray-900">📦 / </h3>
<p className="text-xs text-gray-500">
: {lastUpdate.toLocaleTimeString("ko-KR")}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={loadData}
disabled={isLoading}
className="h-8 w-8 p-0"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
{/* 배송 상태 요약 */}
<div className="mb-3">
<h4 className="mb-2 text-sm font-semibold text-gray-700"> </h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-blue-500">
<div className="text-xs text-gray-600 mb-0.5"></div>
<div className="text-lg font-bold text-blue-600">{statusStats.in_transit}</div>
</div>
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-green-500">
<div className="text-xs text-gray-600 mb-0.5"></div>
<div className="text-lg font-bold text-green-600">{statusStats.delivered}</div>
</div>
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-red-500">
<div className="text-xs text-gray-600 mb-0.5"></div>
<div className="text-lg font-bold text-red-600">{statusStats.delayed}</div>
</div>
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-yellow-500">
<div className="text-xs text-gray-600 mb-0.5"> </div>
<div className="text-lg font-bold text-yellow-600">{statusStats.pickup_waiting}</div>
</div>
</div>
</div>
{/* 오늘 발송/도착 건수 */}
<div className="mb-3">
<h4 className="mb-2 text-sm font-semibold text-gray-700"> </h4>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-gray-500">
<div className="text-xs text-gray-600 mb-0.5"> </div>
<div className="text-lg font-bold text-gray-900">{todayStats.shipped}</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="rounded-lg bg-white p-1.5 shadow-sm border-l-4 border-gray-500">
<div className="text-xs text-gray-600 mb-0.5"> </div>
<div className="text-lg font-bold text-gray-900">{todayStats.delivered}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
{/* 지연 중인 화물 리스트 */}
<div className="mb-3">
<h4 className="mb-2 text-sm font-semibold text-gray-700 flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-red-600" />
({delayedDeliveries.length})
</h4>
<div className="rounded-lg bg-white shadow-sm border border-gray-200 overflow-hidden">
{delayedDeliveries.length === 0 ? (
<div className="p-6 text-center text-sm text-gray-500">
</div>
) : (
<div className="max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
{delayedDeliveries.map((delivery) => (
<div
key={delivery.id}
className="p-3 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div>
<div className="font-semibold text-sm text-gray-900">{delivery.customer}</div>
<div className="text-xs text-gray-600">{delivery.trackingNumber}</div>
</div>
<span className={`rounded-md px-2 py-1 text-xs font-semibold border ${getStatusColor(delivery.status)}`}>
{getStatusText(delivery.status)}
</span>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div className="flex items-center gap-1">
<span className="font-medium">:</span>
<span>{delivery.origin} {delivery.destination}</span>
</div>
<div className="flex items-center gap-1">
<span className="font-medium">:</span>
<span>{delivery.estimatedDelivery}</span>
</div>
{delivery.delayReason && (
<div className="flex items-center gap-1 text-red-600">
<AlertTriangle className="h-3 w-3" />
<span className="font-medium">:</span>
<span>{delivery.delayReason}</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* 고객 클레임/이슈 리포트 */}
<div>
<h4 className="mb-2 text-sm font-semibold text-gray-700 flex items-center gap-2">
<XCircle className="h-4 w-4 text-orange-600" />
/ ({issues.filter((i) => i.status !== "resolved").length})
</h4>
<div className="rounded-lg bg-white shadow-sm border border-gray-200 overflow-hidden">
{issues.length === 0 ? (
<div className="p-6 text-center text-sm text-gray-500">
</div>
) : (
<div className="max-h-[200px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
{issues.map((issue) => (
<div
key={issue.id}
className="p-3 border-b border-gray-200 last:border-b-0 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div>
<div className="font-semibold text-sm text-gray-900">{issue.customer}</div>
<div className="text-xs text-gray-600">{issue.trackingNumber}</div>
</div>
<div className="flex gap-1">
<span className="rounded-md px-2 py-1 text-xs font-semibold bg-gray-100 text-gray-700 border border-gray-300">
{getIssueTypeText(issue.issueType)}
</span>
<span className={`rounded-md px-2 py-1 text-xs font-semibold border ${getIssueStatusColor(issue.status)}`}>
{getIssueStatusText(issue.status)}
</span>
</div>
</div>
<div className="text-xs text-gray-600 space-y-1">
<div>{issue.description}</div>
<div className="text-gray-500">: {issue.reportedAt}</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}