ERP-node/frontend/components/dashboard/widgets/DeliveryStatusSummaryWidget...

214 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } from "react";
import { DashboardElement } from "@/components/admin/dashboard/types";
interface DeliveryStatusSummaryWidgetProps {
element: DashboardElement;
}
interface DeliveryStatus {
status: string;
count: number;
}
/**
* 배송 상태 요약 위젯
* - 배송중, 완료, 지연, 픽업 대기 상태별 카운트 표시
*/
export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusSummaryWidgetProps) {
const [statusData, setStatusData] = useState<DeliveryStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
// 자동 새로고침 (30초마다)
const interval = setInterval(loadData, 30000);
return () => clearInterval(interval);
}, [element]);
const loadData = async () => {
if (!element?.dataSource?.query) {
setError("쿼리가 설정되지 않았습니다");
setLoading(false);
return;
}
try {
setLoading(true);
const token = localStorage.getItem("authToken");
const response = await fetch("/api/dashboards/execute-query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: element.dataSource.query,
connectionType: element.dataSource.connectionType || "current",
connectionId: element.dataSource.connectionId,
}),
});
if (!response.ok) throw new Error("데이터 로딩 실패");
const result = await response.json();
// 데이터 처리
if (result.success && result.data?.rows) {
const rows = result.data.rows;
// 상태별 카운트 계산
const statusCounts = rows.reduce((acc: any, row: any) => {
const status = row.status || "알 수 없음";
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
const formattedData: DeliveryStatus[] = [
{ status: "배송중", count: statusCounts["배송중"] || statusCounts["delivering"] || 0 },
{ status: "완료", count: statusCounts["완료"] || statusCounts["delivered"] || 0 },
{ status: "지연", count: statusCounts["지연"] || statusCounts["delayed"] || 0 },
{ status: "픽업 대기", count: statusCounts["픽업 대기"] || statusCounts["pending"] || 0 },
];
setStatusData(formattedData);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
setLoading(false);
}
};
const getBorderColor = (status: string) => {
switch (status) {
case "배송중":
return "border-blue-500";
case "완료":
return "border-green-500";
case "지연":
return "border-red-500";
case "픽업 대기":
return "border-yellow-500";
default:
return "border-gray-500";
}
};
const getDotColor = (status: string) => {
switch (status) {
case "배송중":
return "bg-blue-500";
case "완료":
return "bg-green-500";
case "지연":
return "bg-red-500";
case "픽업 대기":
return "bg-yellow-500";
default:
return "bg-gray-500";
}
};
const getTextColor = (status: string) => {
switch (status) {
case "배송중":
return "text-blue-600";
case "완료":
return "text-green-600";
case "지연":
return "text-red-600";
case "픽업 대기":
return "text-yellow-600";
default:
return "text-gray-600";
}
};
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
<p className="mt-2 text-sm text-gray-500"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-red-500">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-gray-500">
<p className="text-sm"> </p>
</div>
</div>
);
}
const totalCount = statusData.reduce((sum, item) => sum + item.count, 0);
return (
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50 p-2">
{/* 헤더 */}
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
<div className="flex-1">
<h3 className="text-sm font-bold text-gray-900">📊 </h3>
{totalCount > 0 ? (
<p className="text-xs text-gray-500"> {totalCount.toLocaleString()}</p>
) : (
<p className="text-xs text-orange-500"> </p>
)}
</div>
<button
onClick={loadData}
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-white p-0 text-xs disabled:opacity-50"
disabled={loading}
>
{loading ? "⏳" : "🔄"}
</button>
</div>
{/* 스크롤 가능한 콘텐츠 영역 */}
<div className="flex-1 overflow-y-auto">
{/* 상태별 카드 */}
<div className="grid grid-cols-2 gap-1.5">
{statusData.map((item) => (
<div
key={item.status}
className={`rounded border-l-2 bg-white p-1.5 shadow-sm ${getBorderColor(item.status)}`}
>
<div className="mb-0.5 flex items-center gap-1">
<div className={`h-1.5 w-1.5 rounded-full ${getDotColor(item.status)}`}></div>
<div className="text-xs font-medium text-gray-600">{item.status}</div>
</div>
<div className={`text-lg font-bold ${getTextColor(item.status)}`}>{item.count.toLocaleString()}</div>
</div>
))}
</div>
</div>
</div>
);
}