erp-platform/ui/src/views/maintenance/components/MaintenanceCalendar.tsx

536 lines
19 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
import React, { useState } from "react";
import {
FaCalendar,
FaClock,
FaPlus,
FaFilter,
FaChevronLeft,
FaChevronRight,
FaEdit,
FaUser,
} from "react-icons/fa";
import {
WorkOrderStatusEnum,
CalendarView,
PmCalendarEvent,
} from "../../../types/pm";
import { mockCalendarEvents } from "../../../mocks/mockMaintenanceCalendarEvent";
import NewCalendarEventModal from "./NewCalendarEventModal";
import {
getPriorityColor,
getWorkOrderStatusColor,
getWorkOrderStatusIcon,
} from "../../../utils/erp";
const MaintenanceCalendar: React.FC = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [view, setView] = useState<CalendarView>("month");
const [showEventDetailModal, setShowEventDetailModal] = useState(false);
const [showNewEventModal, setShowNewEventModal] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<PmCalendarEvent | null>(
null
);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [statusFilter, setStatusFilter] = useState<"all" | WorkOrderStatusEnum>(
"all"
);
// Mock data - replace with actual API calls
const [events, setEvents] = useState<PmCalendarEvent[]>(mockCalendarEvents);
const getDaysInMonth = (date: Date) => {
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
};
const getFirstDayOfMonth = (date: Date) => {
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
};
const formatDate = (date: Date) => {
return date.toLocaleDateString("tr-TR", {
year: "numeric",
month: "long",
});
};
const getEventsForDate = (date: Date) => {
return events.filter((event) => {
const eventDate = new Date(event.date);
return eventDate.toDateString() === date.toDateString();
});
};
const navigateMonth = (direction: "prev" | "next") => {
const newDate = new Date(currentDate);
if (direction === "prev") {
newDate.setMonth(newDate.getMonth() - 1);
} else {
newDate.setMonth(newDate.getMonth() + 1);
}
setCurrentDate(newDate);
};
const handleEventClick = (event: PmCalendarEvent) => {
setSelectedEvent(event);
setShowEventDetailModal(true);
};
const handleDayClick = (date: Date) => {
setSelectedDate(date);
setShowNewEventModal(true);
};
const handleNewEventSave = (newEvent: Partial<PmCalendarEvent>) => {
if (newEvent.id) {
setEvents((prevEvents) => [...prevEvents, newEvent as PmCalendarEvent]);
}
setShowNewEventModal(false);
setSelectedDate(null);
};
const renderMonthView = () => {
const daysInMonth = getDaysInMonth(currentDate);
const firstDay = getFirstDayOfMonth(currentDate);
const days = [];
// Empty cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
days.push(
<div key={`empty-${i}`} className="p-1 border border-gray-200"></div>
);
}
// Days of the month
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
day
);
const dayEvents = getEventsForDate(date);
const filteredDayEvents = dayEvents.filter(
(event) => statusFilter === "all" || event.status === statusFilter
);
const isToday = date.toDateString() === new Date().toDateString();
days.push(
<div
key={day}
className={`p-1.5 border border-gray-200 min-h-[100px] cursor-pointer hover:bg-gray-50 ${
isToday ? "bg-blue-50" : "bg-white"
}`}
onClick={() => handleDayClick(date)}
>
<div
className={`text-xs font-medium mb-1 ${
isToday ? "text-blue-600" : "text-gray-900"
}`}
>
{day}
</div>
<div className="space-y-1">
{filteredDayEvents.slice(0, 3).map((event) => (
<div
key={event.id}
onClick={(e) => {
e.stopPropagation();
handleEventClick(event);
}}
className={`text-xs p-0.5 rounded border-l-2 cursor-pointer hover:bg-gray-50 ${getPriorityColor(
event.priority
)} ${getWorkOrderStatusColor(event.status)}`}
>
<div className="font-medium truncate">{event.title}</div>
<div className="flex items-center space-x-1">
{getWorkOrderStatusIcon(event.status)}
<span>{event.startTime}</span>
{event.workCenterCode && (
<span className="text-gray-500">
({event.workCenterCode})
</span>
)}
</div>
</div>
))}
{filteredDayEvents.length > 3 && (
<div className="text-xs text-gray-500 p-1">
+{filteredDayEvents.length - 3} daha...
</div>
)}
</div>
</div>
);
}
return (
<div className="grid grid-cols-7 gap-0 bg-white rounded-lg shadow overflow-hidden">
{/* Header */}
{["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"].map((day) => (
<div
key={day}
className="p-2 bg-gray-50 text-center text-xs font-medium text-gray-700 border-r border-gray-200 last:border-r-0"
>
{day}
</div>
))}
{days}
</div>
);
};
const renderWeekView = () => {
const startOfWeek = new Date(currentDate);
startOfWeek.setDate(currentDate.getDate() - currentDate.getDay() + 1); // Start from Monday
const weekDays: Date[] = [];
for (let i = 0; i < 7; i++) {
const date = new Date(startOfWeek);
date.setDate(startOfWeek.getDate() + i);
weekDays.push(date);
}
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="grid grid-cols-8 border-b border-gray-200">
<div className="p-2 bg-gray-50 text-xs font-medium text-gray-700">
Saat
</div>
{weekDays.map((date, index) => (
<div
key={index}
className="p-2 bg-gray-50 text-center border-l border-gray-200"
>
<div className="text-sm font-medium text-gray-700">
{date.toLocaleDateString("tr-TR", { weekday: "short" })}
</div>
<div className="text-lg font-bold text-gray-900">
{date.getDate()}
</div>
</div>
))}
</div>
<div className="max-h-96 overflow-y-auto">
{Array.from({ length: 12 }, (_, hour) => hour + 8).map((hour) => (
<div
key={hour}
className="grid grid-cols-8 border-b border-gray-100"
>
<div className="p-1.5 text-xs text-gray-500 bg-gray-50 border-r border-gray-200">
{hour}:00
</div>
{weekDays.map((date, dayIndex) => {
const dayEvents = getEventsForDate(date).filter((event) => {
const eventHour = parseInt(
event.startTime?.split(":")[0] || "0"
);
return (
eventHour === hour &&
(statusFilter === "all" || event.status === statusFilter)
);
});
return (
<div
key={dayIndex}
className="p-1 border-l border-gray-200 min-h-[50px] cursor-pointer hover:bg-gray-50"
onClick={() => {
const selectedDateTime = new Date(date);
selectedDateTime.setHours(hour, 0, 0, 0);
handleDayClick(selectedDateTime);
}}
>
{dayEvents.map((event) => (
<div
key={event.id}
onClick={(e) => {
e.stopPropagation();
handleEventClick(event);
}}
className={`text-xs p-1 rounded mb-1 border-l-2 cursor-pointer hover:bg-gray-50 ${getPriorityColor(
event.priority
)} ${getWorkOrderStatusColor(event.status)}`}
>
<div className="font-medium truncate">
{event.title}
</div>
<div className="text-gray-500">
{event.startTime}-{event.endTime}
</div>
</div>
))}
</div>
);
})}
</div>
))}
</div>
</div>
);
};
const getTodayEvents = () => {
const today = new Date();
return getEventsForDate(today).filter(
(event) => statusFilter === "all" || event.status === statusFilter
);
};
return (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">Bakım Takvimi</h2>
<p className="text-sm text-gray-600 mt-1">
Bakım planları ve emirlerini takip edin. Yeni planlama için
gün/saat seçin.
</p>
</div>
<button
onClick={() => setShowNewEventModal(true)}
className="bg-blue-600 text-white px-3 py-1.5 rounded-lg hover:bg-blue-700 flex items-center space-x-2 text-sm"
>
<FaPlus className="w-4 h-4" />
<span>Yeni Planlama</span>
</button>
</div>
{/* Calendar Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<button
onClick={() => navigateMonth("prev")}
className="p-1.5 hover:bg-gray-100 rounded-md"
>
<FaChevronLeft className="w-4 h-4" />
</button>
<h3 className="text-base font-semibold text-gray-900 min-w-[180px] text-center">
{formatDate(currentDate)}
</h3>
<button
onClick={() => navigateMonth("next")}
className="p-1.5 hover:bg-gray-100 rounded-md"
>
<FaChevronRight className="w-4 h-4" />
</button>
</div>
<div className="flex space-x-1 bg-gray-100 rounded-lg p-1">
{(["month", "week"] as CalendarView[]).map((viewType) => (
<button
key={viewType}
onClick={() => setView(viewType)}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
view === viewType
? "bg-white text-gray-900 shadow-sm"
: "text-gray-600 hover:text-gray-900"
}`}
>
{viewType === "month" ? "Ay" : "Hafta"}
</button>
))}
</div>
</div>
<div className="flex items-center space-x-3">
<div className="relative">
<FaFilter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<select
value={statusFilter}
onChange={(e) =>
setStatusFilter(e.target.value as "all" | WorkOrderStatusEnum)
}
className="pl-9 pr-4 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
>
<option value="all">Tüm Durumlar</option>
<option value="scheduled">Planlanmış</option>
<option value={WorkOrderStatusEnum.Created}>Oluşturuldu</option>
<option value={WorkOrderStatusEnum.InProgress}>
Devam Ediyor
</option>
<option value={WorkOrderStatusEnum.Completed}>Tamamlandı</option>
</select>
</div>
<button
onClick={() => setCurrentDate(new Date())}
className="px-3 py-1.5 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
>
Bugün
</button>
</div>
</div>
{/* Calendar View */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="lg:col-span-3">
{view === "month" && renderMonthView()}
{view === "week" && renderWeekView()}
</div>
{/* Today's Events Sidebar */}
<div className="bg-white rounded-lg shadow p-4">
<h4 className="text-base font-semibold text-gray-900 mb-3">
Bugünün Etkinlikleri
</h4>
<div className="space-y-3">
{getTodayEvents().map((event) => (
<div
key={event.id}
onClick={() => handleEventClick(event)}
className={`p-2 border-l-4 rounded-r-lg cursor-pointer hover:bg-gray-50 ${getPriorityColor(
event.priority
)}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h5 className="text-sm font-medium text-gray-900">
{event.title}
</h5>
<p className="text-xs text-gray-500 mt-1">
{event.workCenterCode}
</p>
<div className="flex items-center space-x-2 mt-2">
<FaClock className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500">
{event.startTime} - {event.endTime}
</span>
</div>
{event.assignedTo && (
<div className="flex items-center space-x-2 mt-1">
<FaUser className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500">
{event.assignedTo}
</span>
</div>
)}
</div>
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${getWorkOrderStatusColor(
event.status
)}`}
>
{event.status === WorkOrderStatusEnum.Planned
? "Planlandı"
: event.status === WorkOrderStatusEnum.InProgress
? "Devam Ediyor"
: event.status === WorkOrderStatusEnum.Completed
? "Tamamlandı"
: "Bekliyor"}
</span>
</div>
</div>
))}
{getTodayEvents().length === 0 && (
<div className="text-center py-8">
<FaCalendar className="w-10 h-10 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">
Bugün için planlanan etkinlik yok
</p>
</div>
)}
</div>
</div>
</div>
{/* Event Details Modal */}
{showEventDetailModal && selectedEvent && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-4 w-full max-w-lg mx-4">
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="text-base font-semibold text-gray-900">
{selectedEvent.title}
</h3>
<p className="text-sm text-gray-500 mt-1">
{selectedEvent.workCenterCode}
</p>
</div>
<button
onClick={() => setShowEventDetailModal(false)}
className="text-gray-400 hover:text-gray-600"
>
×
</button>
</div>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="text-sm font-medium text-gray-500">
Tarih & Saat
</label>
<p className="text-sm text-gray-900">
{selectedEvent.date.toLocaleDateString("tr-TR")} -{" "}
{selectedEvent.startTime} / {selectedEvent.endTime}
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">
Süre
</label>
<p className="text-sm text-gray-900">
{selectedEvent.duration} dakika
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">
Atanan Kişi
</label>
<p className="text-sm text-gray-900">
{selectedEvent.assignedTo || "Atanmadı"}
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">
Durum
</label>
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getWorkOrderStatusColor(
selectedEvent.status
)}`}
>
{selectedEvent.status === WorkOrderStatusEnum.Planned
? "Planlandı"
: selectedEvent.status === WorkOrderStatusEnum.InProgress
? "Devam Ediyor"
: selectedEvent.status === WorkOrderStatusEnum.Completed
? "Tamamlandı"
: "Bekliyor"}
</span>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowEventDetailModal(false)}
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Kapat
</button>
<button className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center space-x-2">
<FaEdit className="w-4 h-4" />
<span>Düzenle</span>
</button>
</div>
</div>
</div>
)}
{/* New Calendar Event Modal */}
<NewCalendarEventModal
isOpen={showNewEventModal}
onClose={() => {
setShowNewEventModal(false);
setSelectedDate(null);
}}
onSave={handleNewEventSave}
selectedDate={selectedDate || undefined}
/>
</div>
);
};
export default MaintenanceCalendar;