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

201 lines
7.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 { RefreshCw, TrendingUp, TrendingDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { getApiUrl } from "@/lib/utils/apiUrl";
interface VehicleStatusWidgetProps {
element?: any; // 대시보드 요소 (dataSource 포함)
refreshInterval?: number;
}
interface StatusData {
active: number; // 운행 중
inactive: number; // 대기
maintenance: number; // 정비
warning: number; // 고장
total: number;
}
export default function VehicleStatusWidget({ element, refreshInterval = 30000 }: VehicleStatusWidgetProps) {
const [statusData, setStatusData] = useState<StatusData>({
active: 0,
inactive: 0,
maintenance: 0,
warning: 0,
total: 0,
});
const [isLoading, setIsLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
const loadStatusData = async () => {
setIsLoading(true);
// 설정된 쿼리가 없으면 로딩 중단 (기본 쿼리 사용 안 함)
if (!element?.dataSource?.query) {
setIsLoading(false);
return;
}
const query = element.dataSource.query;
try {
const response = await fetch(getApiUrl("/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) {
const newStatus: StatusData = {
active: 0,
inactive: 0,
maintenance: 0,
warning: 0,
total: 0,
};
// 쿼리 결과가 GROUP BY 형식인지 확인
const isGroupedData = result.data.rows[0].count !== undefined;
if (isGroupedData) {
// GROUP BY 형식: SELECT status, COUNT(*) as count
result.data.rows.forEach((row: any) => {
const count = parseInt(row.count) || 0;
const status = row.status?.toLowerCase() || "";
if (status === "active" || status === "running") {
newStatus.active = count;
} else if (status === "inactive" || status === "idle") {
newStatus.inactive = count;
} else if (status === "maintenance") {
newStatus.maintenance = count;
} else if (status === "warning" || status === "breakdown") {
newStatus.warning = count;
}
newStatus.total += count;
});
} else {
// SELECT * 형식: 전체 데이터를 가져와서 카운트
result.data.rows.forEach((row: any) => {
const status = row.status?.toLowerCase() || "";
if (status === "active" || status === "running") {
newStatus.active++;
} else if (status === "inactive" || status === "idle") {
newStatus.inactive++;
} else if (status === "maintenance") {
newStatus.maintenance++;
} else if (status === "warning" || status === "breakdown") {
newStatus.warning++;
}
newStatus.total++;
});
}
setStatusData(newStatus);
setLastUpdate(new Date());
}
}
} catch (error) {
console.error("차량 상태 데이터 로드 실패:", error);
}
setIsLoading(false);
};
// 데이터 로드 및 자동 새로고침
useEffect(() => {
loadStatusData();
const interval = setInterval(loadStatusData, refreshInterval);
return () => clearInterval(interval);
}, [element?.dataSource?.query, refreshInterval]);
// 설정되지 않았을 때도 빈 상태로 표시 (안내 메시지 제거)
const activeRate = statusData.total > 0 ? ((statusData.active / statusData.total) * 100).toFixed(1) : "0";
return (
<div className="from-background to-success/10 flex h-full w-full flex-col overflow-hidden bg-gradient-to-br p-2">
{/* 헤더 */}
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
<div className="flex-1">
<h3 className="text-foreground text-sm font-bold">📊 </h3>
{statusData.total > 0 ? (
<p className="text-muted-foreground text-xs">{lastUpdate.toLocaleTimeString("ko-KR")}</p>
) : (
<p className="text-warning text-xs"> </p>
)}
</div>
<Button variant="outline" size="sm" onClick={loadStatusData} disabled={isLoading} className="h-7 w-7 p-0">
<RefreshCw className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</div>
{/* 스크롤 가능한 콘텐츠 영역 */}
<div className="flex-1 overflow-y-auto">
{/* 총 차량 수 */}
<div className="border-border bg-background mb-1 rounded border p-1.5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<div className="text-foreground text-xs"> </div>
<div className="text-foreground text-base font-bold">{statusData.total}</div>
</div>
<div className="text-right">
<div className="text-foreground text-xs"></div>
<div className="text-success flex items-center gap-0.5 text-sm font-bold">{activeRate}%</div>
</div>
</div>
</div>
{/* 상태별 카드 */}
<div className="grid grid-cols-2 gap-1.5">
{/* 운행 중 */}
<div className="border-success bg-background rounded border-l-2 p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="bg-success h-1.5 w-1.5 rounded-full"></div>
<div className="text-foreground text-xs font-medium"></div>
</div>
<div className="text-success text-lg font-bold">{statusData.active}</div>
</div>
{/* 대기 */}
<div className="border-warning bg-background rounded border-l-2 p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="bg-warning/100 h-1.5 w-1.5 rounded-full"></div>
<div className="text-foreground text-xs font-medium"></div>
</div>
<div className="text-warning text-lg font-bold">{statusData.inactive}</div>
</div>
{/* 정비 */}
<div className="border-warning bg-background rounded border-l-2 p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="bg-warning h-1.5 w-1.5 rounded-full"></div>
<div className="text-foreground text-xs font-medium"></div>
</div>
<div className="text-warning text-lg font-bold">{statusData.maintenance}</div>
</div>
{/* 고장 */}
<div className="border-destructive bg-background rounded border-l-2 p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="bg-destructive h-1.5 w-1.5 rounded-full"></div>
<div className="text-foreground text-xs font-medium"></div>
</div>
<div className="text-destructive text-lg font-bold">{statusData.warning}</div>
</div>
</div>
</div>
</div>
);
}