214 lines
8.2 KiB
TypeScript
214 lines
8.2 KiB
TypeScript
/**
|
||
* 작업 이력 위젯
|
||
* - 작업 이력 목록 표시
|
||
* - 필터링 기능
|
||
* - 상태별 색상 구분
|
||
* - 쿼리 결과 기반 데이터 표시
|
||
*/
|
||
|
||
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { DashboardElement } from "@/components/admin/dashboard/types";
|
||
import { getApiUrl } from "@/lib/utils/apiUrl";
|
||
import { WORK_TYPE_LABELS, WORK_STATUS_LABELS, WORK_STATUS_COLORS, WorkType, WorkStatus } from "@/types/workHistory";
|
||
|
||
interface WorkHistoryWidgetProps {
|
||
element: DashboardElement;
|
||
refreshInterval?: number;
|
||
}
|
||
|
||
export default function WorkHistoryWidget({ element, refreshInterval = 60000 }: WorkHistoryWidgetProps) {
|
||
const [data, setData] = useState<any[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [selectedType, setSelectedType] = useState<WorkType | "all">("all");
|
||
const [selectedStatus, setSelectedStatus] = useState<WorkStatus | "all">("all");
|
||
|
||
// 데이터 로드
|
||
const loadData = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
// 쿼리가 설정되어 있으면 쿼리 실행
|
||
if (element.dataSource?.query) {
|
||
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) {
|
||
setData(result.data.rows);
|
||
} else {
|
||
throw new Error(result.message || "데이터 로드 실패");
|
||
}
|
||
} else {
|
||
// 쿼리 미설정 시 안내 메시지
|
||
setError("쿼리를 설정해주세요");
|
||
setData([]);
|
||
}
|
||
} catch (err) {
|
||
console.error("작업 이력 로드 실패:", err);
|
||
setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
const interval = setInterval(loadData, refreshInterval);
|
||
return () => clearInterval(interval);
|
||
}, [selectedType, selectedStatus, refreshInterval, element.dataSource]);
|
||
|
||
if (isLoading && data.length === 0) {
|
||
return (
|
||
<div className="flex h-full items-center justify-center bg-muted">
|
||
<div className="text-center">
|
||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||
<div className="mt-2 text-sm text-foreground">로딩 중...</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="flex h-full items-center justify-center bg-muted p-4">
|
||
<div className="text-center">
|
||
<div className="mb-2 text-4xl">⚠️</div>
|
||
<div className="text-sm font-medium text-foreground">{error}</div>
|
||
{!element.dataSource?.query && <div className="mt-2 text-xs text-muted-foreground">쿼리를 설정하세요</div>}
|
||
<button
|
||
onClick={loadData}
|
||
className="mt-3 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary/90"
|
||
>
|
||
다시 시도
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-full flex-col bg-background">
|
||
{/* 필터 */}
|
||
<div className="flex gap-2 border-b p-3">
|
||
<select
|
||
value={selectedType}
|
||
onChange={(e) => setSelectedType(e.target.value as WorkType | "all")}
|
||
className="rounded border px-2 py-1 text-sm"
|
||
>
|
||
<option value="all">전체 유형</option>
|
||
<option value="inbound">입고</option>
|
||
<option value="outbound">출고</option>
|
||
<option value="transfer">이송</option>
|
||
<option value="maintenance">정비</option>
|
||
</select>
|
||
|
||
<select
|
||
value={selectedStatus}
|
||
onChange={(e) => setSelectedStatus(e.target.value as WorkStatus | "all")}
|
||
className="rounded border px-2 py-1 text-sm"
|
||
>
|
||
<option value="all">전체 상태</option>
|
||
<option value="pending">대기</option>
|
||
<option value="in_progress">진행중</option>
|
||
<option value="completed">완료</option>
|
||
<option value="cancelled">취소</option>
|
||
</select>
|
||
|
||
<button
|
||
onClick={loadData}
|
||
className="ml-auto rounded bg-primary px-3 py-1 text-sm text-white hover:bg-primary/90"
|
||
>
|
||
🔄 새로고침
|
||
</button>
|
||
</div>
|
||
|
||
{/* 테이블 */}
|
||
<div className="flex-1 overflow-auto">
|
||
<table className="w-full text-sm">
|
||
<thead className="sticky top-0 bg-muted text-left">
|
||
<tr>
|
||
<th className="border-b px-3 py-2 font-medium">작업번호</th>
|
||
<th className="border-b px-3 py-2 font-medium">일시</th>
|
||
<th className="border-b px-3 py-2 font-medium">유형</th>
|
||
<th className="border-b px-3 py-2 font-medium">차량</th>
|
||
<th className="border-b px-3 py-2 font-medium">경로</th>
|
||
<th className="border-b px-3 py-2 font-medium">화물</th>
|
||
<th className="border-b px-3 py-2 font-medium">중량</th>
|
||
<th className="border-b px-3 py-2 font-medium">상태</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={8} className="py-8 text-center text-muted-foreground">
|
||
작업 이력이 없습니다
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
data
|
||
.filter((item) => selectedType === "all" || item.work_type === selectedType)
|
||
.filter((item) => selectedStatus === "all" || item.status === selectedStatus)
|
||
.map((item, index) => (
|
||
<tr key={item.id || index} className="border-b hover:bg-muted">
|
||
<td className="px-3 py-2 font-mono text-xs">{item.work_number}</td>
|
||
<td className="px-3 py-2">
|
||
{item.work_date
|
||
? new Date(item.work_date).toLocaleString("ko-KR", {
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
})
|
||
: "-"}
|
||
</td>
|
||
<td className="px-3 py-2">
|
||
<span className="rounded bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||
{WORK_TYPE_LABELS[item.work_type as WorkType] || item.work_type}
|
||
</span>
|
||
</td>
|
||
<td className="px-3 py-2">{item.vehicle_number || "-"}</td>
|
||
<td className="px-3 py-2 text-xs">
|
||
{item.origin && item.destination ? `${item.origin} → ${item.destination}` : "-"}
|
||
</td>
|
||
<td className="px-3 py-2">{item.cargo_name || "-"}</td>
|
||
<td className="px-3 py-2">
|
||
{item.cargo_weight ? `${item.cargo_weight} ${item.cargo_unit || "ton"}` : "-"}
|
||
</td>
|
||
<td className="px-3 py-2">
|
||
<span
|
||
className={`rounded px-2 py-1 text-xs font-medium ${WORK_STATUS_COLORS[item.status as WorkStatus] || "bg-muted text-foreground"}`}
|
||
>
|
||
{WORK_STATUS_LABELS[item.status as WorkStatus] || item.status}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 푸터 */}
|
||
<div className="border-t bg-muted px-3 py-2 text-xs text-foreground">전체 {data.length}건</div>
|
||
</div>
|
||
);
|
||
}
|