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="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-background to-success/10 p-2">
{/* 헤더 */}
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
<div className="flex-1">
<h3 className="text-sm font-bold text-foreground">📊 </h3>
{statusData.total > 0 ? (
<p className="text-xs text-muted-foreground">{lastUpdate.toLocaleTimeString("ko-KR")}</p>
) : (
<p className="text-xs text-warning"> </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="mb-1 rounded border border-border bg-background p-1.5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-foreground"> </div>
<div className="text-base font-bold text-foreground">{statusData.total}</div>
</div>
<div className="text-right">
<div className="text-xs text-foreground"></div>
<div className="flex items-center gap-0.5 text-sm font-bold text-success">{activeRate}%</div>
</div>
</div>
</div>
{/* 상태별 카드 */}
<div className="grid grid-cols-2 gap-1.5">
{/* 운행 중 */}
<div className="rounded border-l-2 border-success bg-background p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="h-1.5 w-1.5 rounded-full bg-success"></div>
<div className="text-xs font-medium text-foreground"></div>
</div>
<div className="text-lg font-bold text-success">{statusData.active}</div>
</div>
{/* 대기 */}
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="h-1.5 w-1.5 rounded-full bg-warning/100"></div>
<div className="text-xs font-medium text-foreground"></div>
</div>
<div className="text-lg font-bold text-warning">{statusData.inactive}</div>
</div>
{/* 정비 */}
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="h-1.5 w-1.5 rounded-full bg-warning"></div>
<div className="text-xs font-medium text-foreground"></div>
</div>
<div className="text-lg font-bold text-warning">{statusData.maintenance}</div>
</div>
{/* 고장 */}
<div className="rounded border-l-2 border-destructive bg-background p-1.5 shadow-sm">
<div className="mb-0.5 flex items-center gap-1">
<div className="h-1.5 w-1.5 rounded-full bg-destructive"></div>
<div className="text-xs font-medium text-foreground"></div>
</div>
<div className="text-lg font-bold text-destructive">{statusData.warning}</div>
</div>
</div>
</div>
</div>
);
}