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

219 lines
7.6 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";
import { getApiUrl } from "@/lib/utils/apiUrl";
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(getApiUrl("/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-success/10 text-success dark:bg-success/20 dark:text-success";
} else if (statusLower.includes("지연") || statusLower.includes("delayed")) {
return "bg-destructive text-destructive-foreground";
} else if (statusLower.includes("픽업") || statusLower.includes("pending")) {
return "bg-warning/10 text-warning dark:bg-warning/20 dark:text-warning";
}
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="text-muted-foreground mt-2 text-sm"> ...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-destructive text-center">
<p className="text-sm"> {error}</p>
<button
onClick={loadData}
className="bg-destructive/10 hover:bg-destructive/20 mt-2 rounded-md px-3 py-1 text-xs"
>
</button>
</div>
</div>
);
}
if (!element?.dataSource?.query) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-muted-foreground text-center">
<p className="text-sm"> </p>
</div>
</div>
);
}
return (
<div className="bg-background flex h-full flex-col overflow-hidden p-4">
{/* 헤더 */}
<div className="mb-4 flex items-center justify-between">
<h3 className="text-foreground text-lg font-semibold">📦 </h3>
<div className="flex items-center gap-2">
<input
type="text"
placeholder="검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="border-input bg-background placeholder:text-muted-foreground focus:ring-ring rounded-md border px-3 py-1 text-sm focus:ring-2 focus:outline-none"
/>
<button
onClick={loadData}
className="text-muted-foreground hover:bg-accent hover:text-accent-foreground rounded-full p-1"
title="새로고침"
>
🔄
</button>
</div>
</div>
{/* 총 건수 */}
<div className="text-muted-foreground mb-3 text-sm">
<span className="text-foreground font-semibold">{filteredList.length}</span>
</div>
{/* 테이블 */}
<div className="border-border flex-1 overflow-auto rounded-md border">
<table className="w-full text-sm">
<thead className="bg-muted/50 text-muted-foreground">
<tr>
<th className="border-border border-b p-2 text-left font-medium"></th>
<th className="border-border border-b p-2 text-left font-medium"></th>
<th className="border-border border-b p-2 text-left font-medium"></th>
<th className="border-border border-b p-2 text-left font-medium">(kg)</th>
<th className="border-border border-b p-2 text-left font-medium"></th>
</tr>
</thead>
<tbody>
{filteredList.length === 0 ? (
<tr>
<td colSpan={5} className="text-muted-foreground p-8 text-center">
{searchTerm ? "검색 결과가 없습니다" : "화물이 없습니다"}
</td>
</tr>
) : (
filteredList.map((cargo, index) => (
<tr key={cargo.id || index} className="border-border hover:bg-muted/30 border-b transition-colors">
<td className="text-foreground p-2 font-medium">
{cargo.tracking_number || cargo.trackingNumber || "-"}
</td>
<td className="text-foreground p-2">{cargo.customer_name || cargo.customerName || "-"}</td>
<td className="text-muted-foreground p-2">{cargo.destination || "-"}</td>
<td className="text-muted-foreground p-2 text-right">{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>
);
}