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

477 lines
18 KiB
TypeScript
Raw Normal View History

2025-09-15 21:29:07 +00:00
import React, { useState } from 'react'
2025-09-15 09:31:47 +00:00
import {
FaCalendar,
FaClock,
FaPlus,
FaFilter,
FaChevronLeft,
FaChevronRight,
FaEdit,
FaUser,
2025-09-15 21:29:07 +00:00
} from 'react-icons/fa'
import { WorkOrderStatusEnum, CalendarView, PmCalendarEvent } from '../../../types/pm'
import { mockCalendarEvents } from '../../../mocks/mockMaintenanceCalendarEvent'
import NewCalendarEventModal from './NewCalendarEventModal'
2025-09-15 09:31:47 +00:00
import {
getPriorityColor,
getWorkOrderStatusColor,
getWorkOrderStatusIcon,
2025-09-17 08:58:20 +00:00
getWorkOrderStatusText,
2025-09-15 21:29:07 +00:00
} from '../../../utils/erp'
import { Container } from '@/components/shared'
2025-09-15 09:31:47 +00:00
const MaintenanceCalendar: React.FC = () => {
2025-09-15 21:29:07 +00:00
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')
2025-09-15 09:31:47 +00:00
// Mock data - replace with actual API calls
2025-09-15 21:29:07 +00:00
const [events, setEvents] = useState<PmCalendarEvent[]>(mockCalendarEvents)
2025-09-15 09:31:47 +00:00
const getDaysInMonth = (date: Date) => {
2025-09-15 21:29:07 +00:00
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
}
2025-09-15 09:31:47 +00:00
const getFirstDayOfMonth = (date: Date) => {
2025-09-15 21:29:07 +00:00
return new Date(date.getFullYear(), date.getMonth(), 1).getDay()
}
2025-09-15 09:31:47 +00:00
const formatDate = (date: Date) => {
2025-09-15 21:29:07 +00:00
return date.toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'long',
})
}
2025-09-15 09:31:47 +00:00
const getEventsForDate = (date: Date) => {
return events.filter((event) => {
2025-09-15 21:29:07 +00:00
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)
2025-09-15 09:31:47 +00:00
} else {
2025-09-15 21:29:07 +00:00
newDate.setMonth(newDate.getMonth() + 1)
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:29:07 +00:00
setCurrentDate(newDate)
}
2025-09-15 09:31:47 +00:00
const handleEventClick = (event: PmCalendarEvent) => {
2025-09-15 21:29:07 +00:00
setSelectedEvent(event)
setShowEventDetailModal(true)
}
2025-09-15 09:31:47 +00:00
const handleDayClick = (date: Date) => {
2025-09-15 21:29:07 +00:00
setSelectedDate(date)
setShowNewEventModal(true)
}
2025-09-15 09:31:47 +00:00
const handleNewEventSave = (newEvent: Partial<PmCalendarEvent>) => {
if (newEvent.id) {
2025-09-15 21:29:07 +00:00
setEvents((prevEvents) => [...prevEvents, newEvent as PmCalendarEvent])
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:29:07 +00:00
setShowNewEventModal(false)
setSelectedDate(null)
}
2025-09-15 09:31:47 +00:00
const renderMonthView = () => {
2025-09-15 21:29:07 +00:00
const daysInMonth = getDaysInMonth(currentDate)
const firstDay = getFirstDayOfMonth(currentDate)
const days = []
2025-09-15 09:31:47 +00:00
// Empty cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
2025-09-15 21:29:07 +00:00
days.push(<div key={`empty-${i}`} className="p-1 border border-gray-200"></div>)
2025-09-15 09:31:47 +00:00
}
// Days of the month
for (let day = 1; day <= daysInMonth; day++) {
2025-09-15 21:29:07 +00:00
const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day)
const dayEvents = getEventsForDate(date)
2025-09-15 09:31:47 +00:00
const filteredDayEvents = dayEvents.filter(
2025-09-15 21:29:07 +00:00
(event) => statusFilter === 'all' || event.status === statusFilter,
)
2025-09-15 09:31:47 +00:00
2025-09-15 21:29:07 +00:00
const isToday = date.toDateString() === new Date().toDateString()
2025-09-15 09:31:47 +00:00
days.push(
<div
key={day}
className={`p-1.5 border border-gray-200 min-h-[100px] cursor-pointer hover:bg-gray-50 ${
2025-09-15 21:29:07 +00:00
isToday ? 'bg-blue-50' : 'bg-white'
2025-09-15 09:31:47 +00:00
}`}
onClick={() => handleDayClick(date)}
>
<div
2025-09-15 21:29:07 +00:00
className={`text-xs font-medium mb-1 ${isToday ? 'text-blue-600' : 'text-gray-900'}`}
2025-09-15 09:31:47 +00:00
>
{day}
</div>
<div className="space-y-1">
{filteredDayEvents.slice(0, 3).map((event) => (
<div
key={event.id}
onClick={(e) => {
2025-09-15 21:29:07 +00:00
e.stopPropagation()
handleEventClick(event)
2025-09-15 09:31:47 +00:00
}}
className={`text-xs p-0.5 rounded border-l-2 cursor-pointer hover:bg-gray-50 ${getPriorityColor(
2025-09-15 21:29:07 +00:00
event.priority,
2025-09-15 09:31:47 +00:00
)} ${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 && (
2025-09-15 21:29:07 +00:00
<span className="text-gray-500">({event.workCenterCode})</span>
2025-09-15 09:31:47 +00:00
)}
</div>
</div>
))}
{filteredDayEvents.length > 3 && (
<div className="text-xs text-gray-500 p-1">
+{filteredDayEvents.length - 3} daha...
</div>
)}
</div>
2025-09-15 21:29:07 +00:00
</div>,
)
2025-09-15 09:31:47 +00:00
}
return (
<div className="grid grid-cols-7 gap-0 bg-white rounded-lg shadow overflow-hidden">
{/* Header */}
2025-09-15 21:29:07 +00:00
{['Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt', 'Paz'].map((day) => (
2025-09-15 09:31:47 +00:00
<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>
2025-09-15 21:29:07 +00:00
)
}
2025-09-15 09:31:47 +00:00
const renderWeekView = () => {
2025-09-15 21:29:07 +00:00
const startOfWeek = new Date(currentDate)
startOfWeek.setDate(currentDate.getDate() - currentDate.getDay() + 1) // Start from Monday
2025-09-15 09:31:47 +00:00
2025-09-15 21:29:07 +00:00
const weekDays: Date[] = []
2025-09-15 09:31:47 +00:00
for (let i = 0; i < 7; i++) {
2025-09-15 21:29:07 +00:00
const date = new Date(startOfWeek)
date.setDate(startOfWeek.getDate() + i)
weekDays.push(date)
2025-09-15 09:31:47 +00:00
}
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="grid grid-cols-8 border-b border-gray-200">
2025-09-15 21:29:07 +00:00
<div className="p-2 bg-gray-50 text-xs font-medium text-gray-700">Saat</div>
2025-09-15 09:31:47 +00:00
{weekDays.map((date, index) => (
2025-09-15 21:29:07 +00:00
<div key={index} className="p-2 bg-gray-50 text-center border-l border-gray-200">
2025-09-15 09:31:47 +00:00
<div className="text-sm font-medium text-gray-700">
2025-09-15 21:29:07 +00:00
{date.toLocaleDateString('tr-TR', { weekday: 'short' })}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:29:07 +00:00
<div className="text-lg font-bold text-gray-900">{date.getDate()}</div>
2025-09-15 09:31:47 +00:00
</div>
))}
</div>
<div className="max-h-96 overflow-y-auto">
{Array.from({ length: 12 }, (_, hour) => hour + 8).map((hour) => (
2025-09-15 21:29:07 +00:00
<div key={hour} className="grid grid-cols-8 border-b border-gray-100">
2025-09-15 09:31:47 +00:00
<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) => {
2025-09-15 21:29:07 +00:00
const eventHour = parseInt(event.startTime?.split(':')[0] || '0')
2025-09-15 09:31:47 +00:00
return (
2025-09-15 21:29:07 +00:00
eventHour === hour && (statusFilter === 'all' || event.status === statusFilter)
)
})
2025-09-15 09:31:47 +00:00
return (
<div
key={dayIndex}
className="p-1 border-l border-gray-200 min-h-[50px] cursor-pointer hover:bg-gray-50"
onClick={() => {
2025-09-15 21:29:07 +00:00
const selectedDateTime = new Date(date)
selectedDateTime.setHours(hour, 0, 0, 0)
handleDayClick(selectedDateTime)
2025-09-15 09:31:47 +00:00
}}
>
{dayEvents.map((event) => (
<div
key={event.id}
onClick={(e) => {
2025-09-15 21:29:07 +00:00
e.stopPropagation()
handleEventClick(event)
2025-09-15 09:31:47 +00:00
}}
className={`text-xs p-1 rounded mb-1 border-l-2 cursor-pointer hover:bg-gray-50 ${getPriorityColor(
2025-09-15 21:29:07 +00:00
event.priority,
2025-09-15 09:31:47 +00:00
)} ${getWorkOrderStatusColor(event.status)}`}
>
2025-09-15 21:29:07 +00:00
<div className="font-medium truncate">{event.title}</div>
2025-09-15 09:31:47 +00:00
<div className="text-gray-500">
{event.startTime}-{event.endTime}
</div>
</div>
))}
</div>
2025-09-15 21:29:07 +00:00
)
2025-09-15 09:31:47 +00:00
})}
</div>
))}
</div>
</div>
2025-09-15 21:29:07 +00:00
)
}
2025-09-15 09:31:47 +00:00
const getTodayEvents = () => {
2025-09-15 21:29:07 +00:00
const today = new Date()
2025-09-15 09:31:47 +00:00
return getEventsForDate(today).filter(
2025-09-15 21:29:07 +00:00
(event) => statusFilter === 'all' || event.status === statusFilter,
)
}
2025-09-15 09:31:47 +00:00
return (
2025-09-15 21:29:07 +00:00
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Bakım Takvimi</h2>
<p className="text-gray-600">
Bakım planları ve emirlerini takip edin. Yeni planlama için gün/saat seçin.
</p>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:29:07 +00:00
<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>
2025-09-15 09:31:47 +00:00
2025-09-15 21:29:07 +00:00
{/* 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">
2025-09-15 09:31:47 +00:00
<button
2025-09-15 21:29:07 +00:00
onClick={() => navigateMonth('prev')}
className="p-1.5 hover:bg-gray-100 rounded-md"
2025-09-15 09:31:47 +00:00
>
2025-09-15 21:29:07 +00:00
<FaChevronLeft className="w-4 h-4" />
2025-09-15 09:31:47 +00:00
</button>
2025-09-15 21:29:07 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:29:07 +00:00
<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>
2025-09-17 08:58:20 +00:00
{Object.values(WorkOrderStatusEnum).map((status) => (
<option key={status} value={status}>
{getWorkOrderStatusText(status)}
</option>
))}
2025-09-15 21:29:07 +00:00
</select>
</div>
<button
onClick={() => setCurrentDate(new Date())}
className="px-3 py-1.5 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
2025-09-15 09:31:47 +00:00
>
2025-09-15 21:29:07 +00:00
Bugün
</button>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 21:29:07 +00:00
{/* 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>
2025-09-15 09:31:47 +00:00
2025-09-15 21:29:07 +00:00
{/* 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" />
2025-09-15 09:31:47 +00:00
<span className="text-xs text-gray-500">
2025-09-15 21:29:07 +00:00
{event.startTime} - {event.endTime}
2025-09-15 09:31:47 +00:00
</span>
</div>
2025-09-15 21:29:07 +00:00
{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>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 21:29:07 +00:00
))}
{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>
2025-09-15 09:31:47 +00:00
</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>
2025-09-15 21:29:07 +00:00
<h3 className="text-base font-semibold text-gray-900">{selectedEvent.title}</h3>
<p className="text-sm text-gray-500 mt-1">{selectedEvent.workCenterCode}</p>
2025-09-15 09:31:47 +00:00
</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>
2025-09-15 21:29:07 +00:00
<label className="text-sm font-medium text-gray-500">Tarih & Saat</label>
2025-09-15 09:31:47 +00:00
<p className="text-sm text-gray-900">
2025-09-15 21:29:07 +00:00
{selectedEvent.date.toLocaleDateString('tr-TR')} - {selectedEvent.startTime} /{' '}
{selectedEvent.endTime}
2025-09-15 09:31:47 +00:00
</p>
</div>
<div>
2025-09-15 21:29:07 +00:00
<label className="text-sm font-medium text-gray-500">Süre</label>
<p className="text-sm text-gray-900">{selectedEvent.duration} dakika</p>
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:29:07 +00:00
<label className="text-sm font-medium text-gray-500">Atanan Kişi</label>
<p className="text-sm text-gray-900">{selectedEvent.assignedTo || 'Atanmadı'}</p>
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:29:07 +00:00
<label className="text-sm font-medium text-gray-500">Durum</label>
2025-09-15 09:31:47 +00:00
<span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getWorkOrderStatusColor(
2025-09-15 21:29:07 +00:00
selectedEvent.status,
2025-09-15 09:31:47 +00:00
)}`}
>
{selectedEvent.status === WorkOrderStatusEnum.Planned
2025-09-15 21:29:07 +00:00
? 'Planlandı'
2025-09-15 09:31:47 +00:00
: selectedEvent.status === WorkOrderStatusEnum.InProgress
2025-09-15 21:29:07 +00:00
? 'Devam Ediyor'
: selectedEvent.status === WorkOrderStatusEnum.Completed
? 'Tamamlandı'
: 'Bekliyor'}
2025-09-15 09:31:47 +00:00
</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={() => {
2025-09-15 21:29:07 +00:00
setShowNewEventModal(false)
setSelectedDate(null)
2025-09-15 09:31:47 +00:00
}}
onSave={handleNewEventSave}
selectedDate={selectedDate || undefined}
/>
2025-09-15 21:29:07 +00:00
</Container>
)
}
2025-09-15 09:31:47 +00:00
2025-09-15 21:29:07 +00:00
export default MaintenanceCalendar