ERP-node/frontend/components/vehicle/VehicleReport.tsx

661 lines
24 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import {
getSummaryReport,
getDailyReport,
getMonthlyReport,
getDriverReport,
getRouteReport,
formatDistance,
formatDuration,
SummaryReport,
DailyStat,
MonthlyStat,
DriverStat,
RouteStat,
} from "@/lib/api/vehicleTrip";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
RefreshCw,
Car,
Route,
Clock,
Users,
TrendingUp,
MapPin,
} from "lucide-react";
import { format } from "date-fns";
import { ko } from "date-fns/locale";
export default function VehicleReport() {
// 요약 통계
const [summary, setSummary] = useState<SummaryReport | null>(null);
const [summaryPeriod, setSummaryPeriod] = useState("month");
const [summaryLoading, setSummaryLoading] = useState(false);
// 일별 통계
const [dailyData, setDailyData] = useState<DailyStat[]>([]);
const [dailyStartDate, setDailyStartDate] = useState(
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]
);
const [dailyEndDate, setDailyEndDate] = useState(
new Date().toISOString().split("T")[0]
);
const [dailyLoading, setDailyLoading] = useState(false);
// 월별 통계
const [monthlyData, setMonthlyData] = useState<MonthlyStat[]>([]);
const [monthlyYear, setMonthlyYear] = useState(new Date().getFullYear());
const [monthlyLoading, setMonthlyLoading] = useState(false);
// 운전자별 통계
const [driverData, setDriverData] = useState<DriverStat[]>([]);
const [driverLoading, setDriverLoading] = useState(false);
// 구간별 통계
const [routeData, setRouteData] = useState<RouteStat[]>([]);
const [routeLoading, setRouteLoading] = useState(false);
// 요약 로드
const loadSummary = useCallback(async () => {
setSummaryLoading(true);
try {
const response = await getSummaryReport(summaryPeriod);
if (response.success) {
setSummary(response.data);
}
} catch (error) {
console.error("요약 통계 조회 실패:", error);
} finally {
setSummaryLoading(false);
}
}, [summaryPeriod]);
// 일별 로드
const loadDaily = useCallback(async () => {
setDailyLoading(true);
try {
const response = await getDailyReport({
startDate: dailyStartDate,
endDate: dailyEndDate,
});
if (response.success) {
setDailyData(response.data?.data || []);
}
} catch (error) {
console.error("일별 통계 조회 실패:", error);
} finally {
setDailyLoading(false);
}
}, [dailyStartDate, dailyEndDate]);
// 월별 로드
const loadMonthly = useCallback(async () => {
setMonthlyLoading(true);
try {
const response = await getMonthlyReport({ year: monthlyYear });
if (response.success) {
setMonthlyData(response.data?.data || []);
}
} catch (error) {
console.error("월별 통계 조회 실패:", error);
} finally {
setMonthlyLoading(false);
}
}, [monthlyYear]);
// 운전자별 로드
const loadDrivers = useCallback(async () => {
setDriverLoading(true);
try {
const response = await getDriverReport({ limit: 20 });
if (response.success) {
setDriverData(response.data || []);
}
} catch (error) {
console.error("운전자별 통계 조회 실패:", error);
} finally {
setDriverLoading(false);
}
}, []);
// 구간별 로드
const loadRoutes = useCallback(async () => {
setRouteLoading(true);
try {
const response = await getRouteReport({ limit: 20 });
if (response.success) {
setRouteData(response.data || []);
}
} catch (error) {
console.error("구간별 통계 조회 실패:", error);
} finally {
setRouteLoading(false);
}
}, []);
// 초기 로드
useEffect(() => {
loadSummary();
}, [loadSummary]);
// 기간 레이블
const getPeriodLabel = (period: string) => {
switch (period) {
case "today":
return "오늘";
case "week":
return "최근 7일";
case "month":
return "최근 30일";
case "year":
return "올해";
default:
return period;
}
};
return (
<div className="space-y-6">
{/* 요약 통계 카드 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"> </h2>
<div className="flex items-center gap-2">
<Select value={summaryPeriod} onValueChange={setSummaryPeriod}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="today"></SelectItem>
<SelectItem value="week"> 7</SelectItem>
<SelectItem value="month"> 30</SelectItem>
<SelectItem value="year"></SelectItem>
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
onClick={loadSummary}
disabled={summaryLoading}
>
<RefreshCw
className={`h-4 w-4 ${summaryLoading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</div>
{summary && (
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Car className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold">
{summary.totalTrips.toLocaleString()}
</div>
<div className="text-xs text-muted-foreground">
{getPeriodLabel(summaryPeriod)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<TrendingUp className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold">
{summary.completionRate}%
</div>
<div className="text-xs text-muted-foreground">
{summary.completedTrips} / {summary.totalTrips}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Route className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold">
{formatDistance(summary.totalDistance)}
</div>
<div className="text-xs text-muted-foreground">
{formatDistance(summary.avgDistance)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold">
{formatDuration(summary.totalDuration)}
</div>
<div className="text-xs text-muted-foreground">
{formatDuration(Math.round(summary.avgDuration))}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Users className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold">
{summary.activeDrivers}
</div>
<div className="text-xs text-muted-foreground"> </div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Car className="h-3 w-3" />
</div>
<div className="mt-1 text-2xl font-bold text-green-600">
{summary.activeTrips}
</div>
<div className="text-xs text-muted-foreground"> </div>
</CardContent>
</Card>
</div>
)}
</div>
{/* 상세 통계 탭 */}
<Tabs defaultValue="daily" className="space-y-4">
<TabsList>
<TabsTrigger value="daily" onClick={loadDaily}>
</TabsTrigger>
<TabsTrigger value="monthly" onClick={loadMonthly}>
</TabsTrigger>
<TabsTrigger value="drivers" onClick={loadDrivers}>
</TabsTrigger>
<TabsTrigger value="routes" onClick={loadRoutes}>
</TabsTrigger>
</TabsList>
{/* 일별 통계 */}
<TabsContent value="daily">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Label className="text-xs"></Label>
<Input
type="date"
value={dailyStartDate}
onChange={(e) => setDailyStartDate(e.target.value)}
className="h-8 w-[130px]"
/>
</div>
<div className="flex items-center gap-1">
<Label className="text-xs"></Label>
<Input
type="date"
value={dailyEndDate}
onChange={(e) => setDailyEndDate(e.target.value)}
className="h-8 w-[130px]"
/>
</div>
<Button size="sm" onClick={loadDaily} disabled={dailyLoading}>
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dailyLoading ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
...
</TableCell>
</TableRow>
) : dailyData.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
.
</TableCell>
</TableRow>
) : (
dailyData.map((row) => (
<TableRow key={row.date}>
<TableCell>
{format(new Date(row.date), "MM/dd (E)", {
locale: ko,
})}
</TableCell>
<TableCell className="text-right">
{row.tripCount}
</TableCell>
<TableCell className="text-right">
{row.completedCount}
</TableCell>
<TableCell className="text-right">
{row.cancelledCount}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.totalDistance)}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.avgDistance)}
</TableCell>
<TableCell className="text-right">
{formatDuration(row.totalDuration)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 월별 통계 */}
<TabsContent value="monthly">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<div className="flex items-center gap-2">
<Select
value={String(monthlyYear)}
onValueChange={(v) => setMonthlyYear(parseInt(v))}
>
<SelectTrigger className="w-[100px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[0, 1, 2].map((offset) => {
const year = new Date().getFullYear() - offset;
return (
<SelectItem key={year} value={String(year)}>
{year}
</SelectItem>
);
})}
</SelectContent>
</Select>
<Button
size="sm"
onClick={loadMonthly}
disabled={monthlyLoading}
>
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{monthlyLoading ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
...
</TableCell>
</TableRow>
) : monthlyData.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
.
</TableCell>
</TableRow>
) : (
monthlyData.map((row) => (
<TableRow key={row.month}>
<TableCell>{row.month}</TableCell>
<TableCell className="text-right">
{row.tripCount}
</TableCell>
<TableCell className="text-right">
{row.completedCount}
</TableCell>
<TableCell className="text-right">
{row.cancelledCount}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.totalDistance)}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.avgDistance)}
</TableCell>
<TableCell className="text-right">
{row.driverCount}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 운전자별 통계 */}
<TabsContent value="drivers">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<Button
size="sm"
onClick={loadDrivers}
disabled={driverLoading}
>
<RefreshCw
className={`mr-1 h-4 w-4 ${driverLoading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{driverLoading ? (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
...
</TableCell>
</TableRow>
) : driverData.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
.
</TableCell>
</TableRow>
) : (
driverData.map((row) => (
<TableRow key={row.userId}>
<TableCell className="font-medium">
{row.userName}
</TableCell>
<TableCell className="text-right">
{row.tripCount}
</TableCell>
<TableCell className="text-right">
{row.completedCount}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.totalDistance)}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.avgDistance)}
</TableCell>
<TableCell className="text-right">
{formatDuration(row.totalDuration)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 구간별 통계 */}
<TabsContent value="routes">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base"> </CardTitle>
<Button size="sm" onClick={loadRoutes} disabled={routeLoading}>
<RefreshCw
className={`mr-1 h-4 w-4 ${routeLoading ? "animate-spin" : ""}`}
/>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
</div>
</TableHead>
<TableHead>
<div className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
</div>
</TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
<TableHead className="text-right"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{routeLoading ? (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
...
</TableCell>
</TableRow>
) : routeData.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
.
</TableCell>
</TableRow>
) : (
routeData.map((row, idx) => (
<TableRow key={idx}>
<TableCell>{row.departureName}</TableCell>
<TableCell>{row.destinationName}</TableCell>
<TableCell className="text-right">
{row.tripCount}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.totalDistance)}
</TableCell>
<TableCell className="text-right">
{formatDistance(row.avgDistance)}
</TableCell>
<TableCell className="text-right">
{formatDuration(Math.round(row.avgDuration))}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}