246 lines
9.5 KiB
TypeScript
246 lines
9.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { Calendar, Wrench, Truck, Check, Clock, AlertTriangle } from "lucide-react";
|
|
|
|
interface MaintenanceSchedule {
|
|
id: string;
|
|
vehicleNumber: string;
|
|
vehicleType: string;
|
|
maintenanceType: "정기점검" | "수리" | "타이어교체" | "오일교환" | "기타";
|
|
scheduledDate: string;
|
|
status: "scheduled" | "in_progress" | "completed" | "overdue";
|
|
notes?: string;
|
|
estimatedCost?: number;
|
|
}
|
|
|
|
// 목 데이터 (하드코딩 - 주석처리됨)
|
|
// const mockSchedules: MaintenanceSchedule[] = [
|
|
// {
|
|
// id: "1",
|
|
// vehicleNumber: "서울12가3456",
|
|
// vehicleType: "1톤 트럭",
|
|
// maintenanceType: "정기점검",
|
|
// scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
// status: "scheduled",
|
|
// notes: "6개월 정기점검",
|
|
// estimatedCost: 300000,
|
|
// },
|
|
// {
|
|
// id: "2",
|
|
// vehicleNumber: "경기34나5678",
|
|
// vehicleType: "2.5톤 트럭",
|
|
// maintenanceType: "오일교환",
|
|
// scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
// status: "scheduled",
|
|
// estimatedCost: 150000,
|
|
// },
|
|
// {
|
|
// id: "3",
|
|
// vehicleNumber: "인천56다7890",
|
|
// vehicleType: "라보",
|
|
// maintenanceType: "타이어교체",
|
|
// scheduledDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
// status: "overdue",
|
|
// notes: "긴급",
|
|
// estimatedCost: 400000,
|
|
// },
|
|
// {
|
|
// id: "4",
|
|
// vehicleNumber: "부산78라1234",
|
|
// vehicleType: "1톤 트럭",
|
|
// maintenanceType: "수리",
|
|
// scheduledDate: new Date().toISOString(),
|
|
// status: "in_progress",
|
|
// notes: "엔진 점검 중",
|
|
// estimatedCost: 800000,
|
|
// },
|
|
// ];
|
|
|
|
export default function MaintenanceWidget() {
|
|
// TODO: 실제 API 연동 필요
|
|
const [schedules] = useState<MaintenanceSchedule[]>([]);
|
|
const [filter, setFilter] = useState<"all" | MaintenanceSchedule["status"]>("all");
|
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
|
|
|
const filteredSchedules = schedules.filter(
|
|
(s) => filter === "all" || s.status === filter
|
|
);
|
|
|
|
const getStatusBadge = (status: MaintenanceSchedule["status"]) => {
|
|
switch (status) {
|
|
case "scheduled":
|
|
return <span className="rounded bg-primary/10 px-2 py-1 text-xs font-medium text-primary">예정</span>;
|
|
case "in_progress":
|
|
return <span className="rounded bg-warning/10 px-2 py-1 text-xs font-medium text-warning">진행중</span>;
|
|
case "completed":
|
|
return <span className="rounded bg-success/10 px-2 py-1 text-xs font-medium text-success">완료</span>;
|
|
case "overdue":
|
|
return <span className="rounded bg-destructive/10 px-2 py-1 text-xs font-medium text-destructive">지연</span>;
|
|
}
|
|
};
|
|
|
|
const getMaintenanceIcon = (type: MaintenanceSchedule["maintenanceType"]) => {
|
|
switch (type) {
|
|
case "정기점검":
|
|
return "🔍";
|
|
case "수리":
|
|
return "🔧";
|
|
case "타이어교체":
|
|
return "⚙️";
|
|
case "오일교환":
|
|
return "🛢️";
|
|
default:
|
|
return "🔧";
|
|
}
|
|
};
|
|
|
|
const getDaysUntil = (date: string) => {
|
|
const now = new Date();
|
|
const scheduled = new Date(date);
|
|
const diff = scheduled.getTime() - now.getTime();
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days < 0) return `${Math.abs(days)}일 지연`;
|
|
if (days === 0) return "오늘";
|
|
if (days === 1) return "내일";
|
|
return `${days}일 후`;
|
|
};
|
|
|
|
const stats = {
|
|
total: schedules.length,
|
|
scheduled: schedules.filter((s) => s.status === "scheduled").length,
|
|
inProgress: schedules.filter((s) => s.status === "in_progress").length,
|
|
overdue: schedules.filter((s) => s.status === "overdue").length,
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-gradient-to-br from-background to-primary/10">
|
|
{/* 헤더 */}
|
|
<div className="border-b border-border bg-background px-4 py-3">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-lg font-bold text-foreground">🔧 정비 일정 관리</h3>
|
|
<button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-primary-foreground transition-colors hover:bg-primary/90">
|
|
+ 일정 추가
|
|
</button>
|
|
</div>
|
|
|
|
{/* 통계 */}
|
|
<div className="mb-3 grid grid-cols-4 gap-2 text-xs">
|
|
<div className="rounded bg-primary/10 px-2 py-1.5 text-center">
|
|
<div className="font-bold text-primary">{stats.scheduled}</div>
|
|
<div className="text-primary">예정</div>
|
|
</div>
|
|
<div className="rounded bg-warning/10 px-2 py-1.5 text-center">
|
|
<div className="font-bold text-warning">{stats.inProgress}</div>
|
|
<div className="text-warning">진행중</div>
|
|
</div>
|
|
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
|
<div className="font-bold text-destructive">{stats.overdue}</div>
|
|
<div className="text-destructive">지연</div>
|
|
</div>
|
|
<div className="rounded bg-muted px-2 py-1.5 text-center">
|
|
<div className="font-bold text-foreground">{stats.total}</div>
|
|
<div className="text-foreground">전체</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 필터 */}
|
|
<div className="flex gap-2">
|
|
{(["all", "scheduled", "in_progress", "overdue"] as const).map((f) => (
|
|
<button
|
|
key={f}
|
|
onClick={() => setFilter(f)}
|
|
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
|
filter === f ? "bg-primary text-primary-foreground" : "bg-muted text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
{f === "all" ? "전체" : f === "scheduled" ? "예정" : f === "in_progress" ? "진행중" : "지연"}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 일정 리스트 */}
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{filteredSchedules.length === 0 ? (
|
|
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
<div className="text-center">
|
|
<div className="mb-2 text-4xl">📅</div>
|
|
<div>정비 일정이 없습니다</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{filteredSchedules.map((schedule) => (
|
|
<div
|
|
key={schedule.id}
|
|
className={`group rounded-lg border-2 bg-background p-4 shadow-sm transition-all hover:shadow-md ${
|
|
schedule.status === "overdue" ? "border-destructive" : "border-border"
|
|
}`}
|
|
>
|
|
<div className="mb-2 flex items-start justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-2xl">{getMaintenanceIcon(schedule.maintenanceType)}</span>
|
|
<div>
|
|
<div className="font-bold text-foreground">{schedule.vehicleNumber}</div>
|
|
<div className="text-xs text-foreground">{schedule.vehicleType}</div>
|
|
</div>
|
|
</div>
|
|
{getStatusBadge(schedule.status)}
|
|
</div>
|
|
|
|
<div className="mb-3 rounded bg-muted p-2">
|
|
<div className="text-sm font-medium text-foreground">{schedule.maintenanceType}</div>
|
|
{schedule.notes && <div className="mt-1 text-xs text-foreground">{schedule.notes}</div>}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div className="flex items-center gap-1 text-foreground">
|
|
<Calendar className="h-3 w-3" />
|
|
{new Date(schedule.scheduledDate).toLocaleDateString()}
|
|
</div>
|
|
<div
|
|
className={`flex items-center gap-1 font-medium ${
|
|
schedule.status === "overdue" ? "text-destructive" : "text-primary"
|
|
}`}
|
|
>
|
|
<Clock className="h-3 w-3" />
|
|
{getDaysUntil(schedule.scheduledDate)}
|
|
</div>
|
|
{schedule.estimatedCost && (
|
|
<div className="col-span-2 font-bold text-primary">
|
|
예상 비용: {schedule.estimatedCost.toLocaleString()}원
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 액션 버튼 */}
|
|
{schedule.status === "scheduled" && (
|
|
<div className="mt-3 flex gap-2">
|
|
<button className="flex-1 rounded bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90">
|
|
시작
|
|
</button>
|
|
<button className="flex-1 rounded bg-muted px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted/90">
|
|
일정 변경
|
|
</button>
|
|
</div>
|
|
)}
|
|
{schedule.status === "in_progress" && (
|
|
<div className="mt-3">
|
|
<button className="w-full rounded bg-success px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-success/90">
|
|
<Check className="mr-1 inline h-3 w-3" />
|
|
완료 처리
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|