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

205 lines
8.2 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect } from "react";
import { RefreshCw, Truck, Navigation, Gauge } from "lucide-react";
import { Button } from "@/components/ui/button";
interface Vehicle {
id: string;
vehicle_number: string;
vehicle_name: string;
driver_name: string;
latitude: number;
longitude: number;
status: string;
speed: number;
destination: string;
}
interface VehicleListWidgetProps {
element?: any; // 대시보드 요소 (dataSource 포함)
refreshInterval?: number;
}
export default function VehicleListWidget({ element, refreshInterval = 30000 }: VehicleListWidgetProps) {
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const [selectedStatus, setSelectedStatus] = useState<string>("all");
const loadVehicles = async () => {
setIsLoading(true);
// 설정된 쿼리가 없으면 로딩 중단 (기본 쿼리 사용 안 함)
if (!element?.dataSource?.query) {
setIsLoading(false);
return;
}
const query = element.dataSource.query;
try {
const response = await fetch("/api/dashboards/execute-query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${typeof window !== "undefined" ? localStorage.getItem("authToken") || "" : ""}`,
},
body: JSON.stringify({ query }),
});
if (response.ok) {
const result = await response.json();
if (result.success && result.data.rows.length > 0) {
setVehicles(result.data.rows);
setLastUpdate(new Date());
}
}
} catch (error) {
console.error("차량 목록 로드 실패:", error);
}
setIsLoading(false);
};
// 데이터 로드 및 자동 새로고침
useEffect(() => {
loadVehicles();
const interval = setInterval(loadVehicles, refreshInterval);
return () => clearInterval(interval);
}, [element?.dataSource?.query, refreshInterval]);
// 설정되지 않았을 때도 빈 상태로 표시 (안내 메시지 제거)
const getStatusColor = (status: string) => {
const s = status?.toLowerCase() || "";
if (s === "active" || s === "running") return "bg-green-500";
if (s === "inactive" || s === "idle") return "bg-yellow-500";
if (s === "maintenance") return "bg-orange-500";
if (s === "warning" || s === "breakdown") return "bg-red-500";
return "bg-gray-500";
};
const getStatusText = (status: string) => {
const s = status?.toLowerCase() || "";
if (s === "active" || s === "running") return "운행 중";
if (s === "inactive" || s === "idle") return "대기";
if (s === "maintenance") return "정비";
if (s === "warning" || s === "breakdown") return "고장";
return "알 수 없음";
};
const filteredVehicles =
selectedStatus === "all" ? vehicles : vehicles.filter((v) => v.status?.toLowerCase() === selectedStatus);
return (
<div className="flex h-full w-full flex-col bg-gradient-to-br from-slate-50 to-blue-50 p-4">
{/* 헤더 */}
<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={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
{/* 필터 버튼 */}
<div className="mb-3 flex gap-2 overflow-x-auto">
<button
onClick={() => setSelectedStatus("all")}
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
selectedStatus === "all" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
({vehicles.length})
</button>
<button
onClick={() => setSelectedStatus("active")}
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
selectedStatus === "active" ? "bg-green-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
({vehicles.filter((v) => v.status?.toLowerCase() === "active").length})
</button>
<button
onClick={() => setSelectedStatus("inactive")}
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
selectedStatus === "inactive" ? "bg-yellow-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
({vehicles.filter((v) => v.status?.toLowerCase() === "inactive").length})
</button>
<button
onClick={() => setSelectedStatus("maintenance")}
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
selectedStatus === "maintenance" ? "bg-orange-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
({vehicles.filter((v) => v.status?.toLowerCase() === "maintenance").length})
</button>
<button
onClick={() => setSelectedStatus("warning")}
className={`whitespace-nowrap rounded-md px-3 py-1 text-xs font-medium transition-colors ${
selectedStatus === "warning" ? "bg-red-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
}`}
>
({vehicles.filter((v) => v.status?.toLowerCase() === "warning").length})
</button>
</div>
{/* 차량 목록 */}
<div className="flex-1 overflow-y-auto">
{filteredVehicles.length === 0 ? (
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-white">
<div className="text-center">
<Truck className="mx-auto h-12 w-12 text-gray-300" />
<p className="mt-2 text-sm text-gray-500"> </p>
</div>
</div>
) : (
<div className="space-y-2">
{filteredVehicles.map((vehicle) => (
<div
key={vehicle.id}
className="rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:shadow-md"
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<Truck className="h-4 w-4 text-gray-600" />
<span className="font-semibold text-gray-900">{vehicle.vehicle_name}</span>
</div>
<span className={`rounded-full px-2 py-0.5 text-xs font-semibold text-white ${getStatusColor(vehicle.status)}`}>
{getStatusText(vehicle.status)}
</span>
</div>
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center justify-between">
<span className="text-gray-500"></span>
<span className="font-mono font-medium">{vehicle.vehicle_number}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-500"></span>
<span className="font-medium">{vehicle.driver_name || "미배정"}</span>
</div>
<div className="flex items-center gap-1">
<Navigation className="h-3 w-3 text-gray-400" />
<span className="flex-1 truncate text-gray-700">{vehicle.destination || "대기 중"}</span>
</div>
<div className="flex items-center gap-1">
<Gauge className="h-3 w-3 text-gray-400" />
<span className="text-gray-700">{vehicle.speed || 0} km/h</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}