476 lines
18 KiB
TypeScript
476 lines
18 KiB
TypeScript
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,
|
||
getWorkOrderStatusText,
|
||
} from '../../../utils/erp'
|
||
import { Container } from '@/components/shared'
|
||
|
||
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 (
|
||
<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 iş 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>
|
||
{Object.values(WorkOrderStatusEnum).map((status) => (
|
||
<option key={status} value={status}>
|
||
{getWorkOrderStatusText(status)}
|
||
</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>
|
||
</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}
|
||
/>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default MaintenanceCalendar
|