219 lines
7.6 KiB
TypeScript
219 lines
7.6 KiB
TypeScript
"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>
|
||
);
|
||
}
|