erp-platform/ui/src/views/maintenance/components/MaintenanceCalendar.tsx
2025-09-17 11:58:20 +03:00

476 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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