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

228 lines
7.8 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 CargoListWidgetProps {
element: DashboardElement;
}
interface Cargo {
id: string | number;
tracking_number?: string;
trackingNumber?: string;
customer_name?: string;
customerName?: string;
destination?: string;
status?: string;
weight?: number;
}
/**
* 화물 목록 위젯
* - 화물 목록 테이블 표시
* - 상태별 배지 표시
*/
export default function CargoListWidget({ element }: CargoListWidgetProps) {
const [cargoList, setCargoList] = useState<Cargo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
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) {
setCargoList(result.data.rows);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
setLoading(false);
}
};
const getStatusBadge = (status: string) => {
const statusLower = status?.toLowerCase() || "";
if (statusLower.includes("배송중") || statusLower.includes("delivering")) {
return "bg-primary text-primary-foreground";
} else if (statusLower.includes("완료") || statusLower.includes("delivered")) {
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
} else if (statusLower.includes("지연") || statusLower.includes("delayed")) {
return "bg-destructive text-destructive-foreground";
} else if (statusLower.includes("픽업") || statusLower.includes("pending")) {
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100";
}
return "bg-muted text-muted-foreground";
};
const filteredList = cargoList.filter((cargo) => {
if (!searchTerm) return true;
const trackingNum = cargo.tracking_number || cargo.trackingNumber || "";
const customerName = cargo.customer_name || cargo.customerName || "";
const destination = cargo.destination || "";
const searchLower = searchTerm.toLowerCase();
return (
trackingNum.toLowerCase().includes(searchLower) ||
customerName.toLowerCase().includes(searchLower) ||
destination.toLowerCase().includes(searchLower)
);
});
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-muted-foreground"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-destructive">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
className="mt-2 rounded-md bg-destructive/10 px-3 py-1 text-xs hover:bg-destructive/20"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-muted-foreground">
<p className="text-sm"> </p>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col overflow-hidden bg-background p-4">
{/* 헤더 */}
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">📦 </h3>
<div className="flex items-center gap-2">
<input
type="text"
placeholder="검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="rounded-md border border-input bg-background px-3 py-1 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button
onClick={loadData}
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
title="새로고침"
>
🔄
</button>
</div>
</div>
{/* 총 건수 */}
<div className="mb-3 text-sm text-muted-foreground">
<span className="font-semibold text-foreground">{filteredList.length}</span>
</div>
{/* 테이블 */}
<div className="flex-1 overflow-auto rounded-md border border-border">
<table className="w-full text-sm">
<thead className="bg-muted/50 text-muted-foreground">
<tr>
<th className="border-b border-border p-2 text-left font-medium"></th>
<th className="border-b border-border p-2 text-left font-medium"></th>
<th className="border-b border-border p-2 text-left font-medium"></th>
<th className="border-b border-border p-2 text-left font-medium">(kg)</th>
<th className="border-b border-border p-2 text-left font-medium"></th>
</tr>
</thead>
<tbody>
{filteredList.length === 0 ? (
<tr>
<td colSpan={5} className="p-8 text-center text-muted-foreground">
{searchTerm ? "검색 결과가 없습니다" : "화물이 없습니다"}
</td>
</tr>
) : (
filteredList.map((cargo, index) => (
<tr
key={cargo.id || index}
className="border-b border-border hover:bg-muted/30 transition-colors"
>
<td className="p-2 font-medium text-foreground">
{cargo.tracking_number || cargo.trackingNumber || "-"}
</td>
<td className="p-2 text-foreground">
{cargo.customer_name || cargo.customerName || "-"}
</td>
<td className="p-2 text-muted-foreground">
{cargo.destination || "-"}
</td>
<td className="p-2 text-right text-muted-foreground">
{cargo.weight ? `${cargo.weight}kg` : "-"}
</td>
<td className="p-2">
<span
className={`inline-block rounded-full px-2 py-1 text-xs font-medium ${getStatusBadge(cargo.status || "")}`}
>
{cargo.status || "알 수 없음"}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}