erp-platform/ui/src/views/maintenance/components/MaintenanceWorkOrders.tsx
2025-09-17 12:02:03 +03:00

638 lines
26 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 {
FaPlus,
FaSearch,
FaFilter,
FaFileAlt,
FaClock,
FaCheckCircle,
FaExclamationTriangle,
FaEdit,
FaTrash,
FaEye,
FaDollarSign,
} from 'react-icons/fa'
import { PmMaintenanceWorkOrder, WorkOrderStatusEnum, WorkOrderTypeEnum } from '../../../types/pm'
import { mockMaintenanceWorkOrders } from '../../../mocks/mockMaintenanceWorkOrders'
import NewWorkOrderModal from './NewWorkOrderModal'
import ViewWorkOrderModal from './ViewWorkOrderModal'
import EditWorkOrderModal from './EditWorkOrderModal'
import StartWorkOrderModal from './StartWorkOrderModal'
import AssignWorkOrdersModal from './AssignWorkOrdersModal'
import ChangeWorkOrderStatusModal from './ChangeWorkOrderStatusModal'
import Widget from '../../../components/common/Widget'
import {
getPriorityColor,
getPriorityText,
getWorkOrderStatusColor,
getWorkOrderStatusIcon,
getWorkOrderStatusText,
getWorkOrderTypeColor,
getWorkOrderTypeText,
} from '../../../utils/erp'
import { Container } from '@/components/shared'
const MaintenanceWorkOrders: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<WorkOrderStatusEnum | 'all'>('all')
const [typeFilter, setTypeFilter] = useState<WorkOrderTypeEnum | 'all'>('all')
const [showModal, setShowModal] = useState(false)
const [editingWorkOrder, setEditingWorkOrder] = useState<PmMaintenanceWorkOrder | null>(null)
const [viewingWorkOrder, setViewingWorkOrder] = useState<PmMaintenanceWorkOrder | null>(null)
const [selectedWorkOrders, setSelectedWorkOrders] = useState<string[]>([])
const [showStartModal, setShowStartModal] = useState(false)
const [showAssignModal, setShowAssignModal] = useState(false)
const [showStatusChangeModal, setShowStatusChangeModal] = useState(false)
// Mock data - replace with actual API calls
const [workOrders, setWorkOrders] = useState<PmMaintenanceWorkOrder[]>(mockMaintenanceWorkOrders)
const filteredWorkOrders = workOrders.filter((workOrder) => {
const matchesSearch =
workOrder.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
workOrder.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
workOrder.assignedTo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
workOrder.reportedBy.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === 'all' || workOrder.status === statusFilter
const matchesType = typeFilter === 'all' || workOrder.orderType === typeFilter
return matchesSearch && matchesStatus && matchesType
})
const getTotalMaterialCost = (workOrder: PmMaintenanceWorkOrder) => {
return workOrder.materials.reduce((total, material) => total + material.totalCost, 0)
}
const getCompletionPercentage = (workOrder: PmMaintenanceWorkOrder) => {
const completedActivities = workOrder.activities.filter(
(activity) => activity.completedAt,
).length
const totalActivities = workOrder.activities.length
return totalActivities > 0 ? Math.round((completedActivities / totalActivities) * 100) : 0
}
const isOverdue = (workOrder: PmMaintenanceWorkOrder) => {
if (!workOrder.scheduledEnd) return false
const now = new Date()
return workOrder.scheduledEnd < now && workOrder.status !== WorkOrderStatusEnum.Completed
}
const handleAddWorkOrder = () => {
setEditingWorkOrder(null)
setShowModal(true)
}
const handleEdit = (workOrder: PmMaintenanceWorkOrder) => {
setEditingWorkOrder(workOrder)
setShowModal(true)
}
const handleView = (workOrder: PmMaintenanceWorkOrder) => {
setViewingWorkOrder(workOrder)
}
const handleSelectWorkOrder = (workOrderId: string) => {
setSelectedWorkOrders((prev) =>
prev.includes(workOrderId) ? prev.filter((id) => id !== workOrderId) : [...prev, workOrderId],
)
}
const handleSaveWorkOrder = (
workOrderData: Omit<PmMaintenanceWorkOrder, 'id' | 'creationTime' | 'lastModificationTime'>,
) => {
const newWorkOrder: PmMaintenanceWorkOrder = {
...workOrderData,
id: `wo-${Date.now()}`,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setWorkOrders([...workOrders, newWorkOrder])
}
const handleUpdateWorkOrder = (updatedWorkOrder: PmMaintenanceWorkOrder) => {
setWorkOrders(workOrders.map((wo) => (wo.id === updatedWorkOrder.id ? updatedWorkOrder : wo)))
}
const handleStartWorkOrders = (
selectedWorkOrders: PmMaintenanceWorkOrder[],
startData: {
actualStart: Date
assignedTo?: string
maintenanceTeamId?: string
notes?: string
},
) => {
const updatedWorkOrders = workOrders.map((wo) => {
if (selectedWorkOrders.some((swo) => swo.id === wo.id)) {
return {
...wo,
status: WorkOrderStatusEnum.InProgress,
actualStart: startData.actualStart,
assignedTo: startData.assignedTo || wo.assignedTo,
maintenanceTeamId: startData.maintenanceTeamId || wo.maintenanceTeamId,
notes: startData.notes
? `${wo.notes ? wo.notes + '\n' : ''}${startData.notes}`
: wo.notes,
lastModificationTime: new Date(),
}
}
return wo
})
setWorkOrders(updatedWorkOrders)
setSelectedWorkOrders([])
}
const handleAssignWorkOrders = (
selectedWorkOrders: PmMaintenanceWorkOrder[],
assignmentData: {
assignmentType: 'person' | 'team'
assignedTo?: string
maintenanceTeamId?: string
notes?: string
},
) => {
const updatedWorkOrders = workOrders.map((wo) => {
if (selectedWorkOrders.some((swo) => swo.id === wo.id)) {
return {
...wo,
assignedTo: assignmentData.assignedTo || wo.assignedTo,
maintenanceTeamId: assignmentData.maintenanceTeamId || wo.maintenanceTeamId,
notes: assignmentData.notes
? `${wo.notes ? wo.notes + '\n' : ''}${assignmentData.notes}`
: wo.notes,
lastModificationTime: new Date(),
}
}
return wo
})
setWorkOrders(updatedWorkOrders)
setSelectedWorkOrders([])
}
const handleChangeWorkOrderStatus = (
selectedWorkOrders: PmMaintenanceWorkOrder[],
statusData: {
newStatus: WorkOrderStatusEnum
notes?: string
completionNotes?: string
},
) => {
const updatedWorkOrders = workOrders.map((wo) => {
if (selectedWorkOrders.some((swo) => swo.id === wo.id)) {
const updates: Partial<PmMaintenanceWorkOrder> = {
status: statusData.newStatus,
lastModificationTime: new Date(),
}
if (statusData.notes) {
updates.notes = statusData.notes
}
if (statusData.completionNotes) {
updates.completionNotes = statusData.completionNotes
}
if (statusData.newStatus === WorkOrderStatusEnum.Completed) {
updates.actualEnd = new Date()
}
return { ...wo, ...updates }
}
return wo
})
setWorkOrders(updatedWorkOrders)
setSelectedWorkOrders([])
}
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 İş Emirleri</h2>
<p className="text-gray-600">Bakım emirlerini takip edin ve yönetin</p>
</div>
<button
onClick={handleAddWorkOrder}
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 İş Emri</span>
</button>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
<Widget title="Toplam" value={workOrders.length} color="gray" icon="FaFileAlt" />
<Widget
title="Devam Ediyor"
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.InProgress).length}
color="orange"
icon="FaWrench"
/>
<Widget
title="Planlandı"
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.Planned).length}
color="blue"
icon="FaCalendar"
/>
<Widget
title="Tamamlandı"
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.Completed).length}
color="green"
icon="FaCheckCircle"
/>
<Widget
title="Geciken"
value={workOrders.filter((wo) => isOverdue(wo)).length}
color="red"
icon="FaExclamationTriangle"
/>
</div>
{/* Filters */}
<div className="flex space-x-3">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="İş emri ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="relative">
<FaFilter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as WorkOrderStatusEnum | 'all')}
className="pl-10 pr-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">Tüm Durumlar</option>
{Object.values(WorkOrderStatusEnum).map((status) => (
<option key={status} value={status}>
{getWorkOrderStatusText(status)}
</option>
))}
</select>
</div>
<div className="relative">
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as WorkOrderTypeEnum | 'all')}
className="pl-4 pr-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all">Tüm Tipler</option>
{Object.values(WorkOrderTypeEnum).map((type) => (
<option key={type} value={type}>
{getWorkOrderTypeText(type)}
</option>
))}
</select>
</div>
</div>
{/* Work Orders List */}
<div className="space-y-3 pt-2">
{filteredWorkOrders.map((workOrder) => {
const completionPercentage = getCompletionPercentage(workOrder)
const materialCost = getTotalMaterialCost(workOrder)
return (
<div
key={workOrder.id}
className="bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<input
type="checkbox"
checked={selectedWorkOrders.includes(workOrder.id)}
onChange={() => handleSelectWorkOrder(workOrder.id)}
className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-base font-semibold text-gray-900">
{workOrder.workOrderNumber}
</h3>
<span
className={`px-2 py-1 rounded-full text-xs font-medium flex items-center space-x-1 ${getWorkOrderStatusColor(
workOrder.status,
)}`}
>
{getWorkOrderStatusIcon(workOrder.status)}
<span>{getWorkOrderStatusText(workOrder.status)}</span>
</span>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${getWorkOrderTypeColor(
workOrder.orderType,
)}`}
>
{getWorkOrderTypeText(workOrder.orderType)}
</span>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${getPriorityColor(
workOrder.priority,
)}`}
>
{getPriorityText(workOrder.priority)}
</span>
{isOverdue(workOrder) && (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 flex items-center space-x-1">
<FaExclamationTriangle className="w-3 h-3" />
<span>GECİKMİŞ</span>
</span>
)}
</div>
<h4 className="font-medium text-gray-800 mb-2 text-sm">
{workOrder.description}
</h4>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 text-xs mb-3">
<div>
<span className="text-gray-500">Bildiren:</span>
<p className="font-medium text-gray-900">{workOrder.reportedBy}</p>
</div>
<div>
<span className="text-gray-500">Atanan:</span>
<p className="font-medium text-gray-900">
{workOrder.assignedTo || 'Atanmadı'}
</p>
</div>
<div>
<span className="text-gray-500">Planlanan Süre:</span>
<p className="font-medium text-gray-900">
{workOrder.scheduledStart && workOrder.scheduledEnd ? (
<>
{workOrder.scheduledStart.toLocaleDateString('tr-TR')} -{' '}
{workOrder.scheduledEnd.toLocaleDateString('tr-TR')}
</>
) : (
'Belirsiz'
)}
</p>
</div>
<div>
<span className="text-gray-500">Maliyet:</span>
<p className="font-medium text-gray-900 flex items-center">
<FaDollarSign className="w-3 h-3 mr-1" />
{workOrder.actualCost > 0
? workOrder.actualCost.toLocaleString('tr-TR')
: workOrder.estimatedCost.toLocaleString('tr-TR')}
{workOrder.actualCost === 0 && (
<span className="text-xs text-gray-500 ml-1">(tahmini)</span>
)}
</p>
</div>
</div>
{/* Progress Bar */}
{workOrder.status === WorkOrderStatusEnum.InProgress && (
<div className="mb-3">
<div className="flex items-center justify-between text-sm text-gray-600 mb-1">
<span>İlerleme</span>
<span>{completionPercentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${completionPercentage}%` }}
></div>
</div>
</div>
)}
{/* Materials Summary */}
{workOrder.materials.length > 0 && (
<div className="bg-blue-50 rounded-lg p-2 mb-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-2">
<span className="text-gray-600">Malzemeler:</span>
<span className="font-medium text-gray-900">
{workOrder.materials.length} kalem
</span>
</div>
<span className="font-medium text-gray-900">
{materialCost.toLocaleString('tr-TR')}
</span>
</div>
</div>
)}
{/* Activities Summary */}
<div className="bg-gray-50 rounded-lg p-2 mb-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-2">
<span className="text-gray-600">Aktiviteler:</span>
<span className="font-medium text-gray-900">
{workOrder.activities.filter((a) => a.completedAt).length} /{' '}
{workOrder.activities.length} tamamlandı
</span>
</div>
<div className="text-gray-600">
Toplam:{' '}
{workOrder.activities.reduce(
(total, activity) => total + activity.plannedDuration,
0,
)}{' '}
dk
</div>
</div>
</div>
{workOrder.notes && (
<div className="bg-yellow-50 rounded-lg p-2 mb-2">
<div className="flex items-start space-x-2 text-sm">
<FaFileAlt className="w-4 h-4 text-yellow-600 mt-0.5" />
<div>
<span className="text-gray-600">Notlar:</span>
<p className="text-gray-900 mt-1">{workOrder.notes}</p>
</div>
</div>
</div>
)}
{workOrder.completionNotes && (
<div className="bg-green-50 rounded-lg p-2 mb-2">
<div className="flex items-start space-x-2 text-sm">
<FaCheckCircle className="w-4 h-4 text-green-600 mt-0.5" />
<div>
<span className="text-gray-600">Tamamlanma Notları:</span>
<p className="text-gray-900 mt-1">{workOrder.completionNotes}</p>
</div>
</div>
</div>
)}
<div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center space-x-2">
<FaClock className="w-3 h-3" />
<span>
Oluşturuldu: {workOrder.creationTime.toLocaleDateString('tr-TR')}
</span>
</div>
<div className="flex items-center space-x-2">
<span>
Son Güncelleme:{' '}
{workOrder.lastModificationTime.toLocaleDateString('tr-TR')}
</span>
</div>
</div>
</div>
</div>
<div className="flex space-x-1">
<button
onClick={() => handleView(workOrder)}
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(workOrder)}
className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-md transition-colors"
>
<FaEdit className="w-4 h-4" />
</button>
<button className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors">
<FaTrash className="w-4 h-4" />
</button>
{workOrder.status === WorkOrderStatusEnum.InProgress && (
<button className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-md transition-colors">
<FaCheckCircle className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>
)
})}
</div>
{filteredWorkOrders.length === 0 && (
<div className="text-center py-12">
<FaFileAlt className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">İş emri bulunamadı</h3>
<p className="text-gray-500 mb-4">
Arama kriterlerinizi değiştirin veya yeni bir emri oluşturun.
</p>
<button
onClick={handleAddWorkOrder}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
Yeni İş Emri Oluştur
</button>
</div>
)}
{/* Bulk Actions */}
{selectedWorkOrders.length > 0 && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-white rounded-lg shadow-lg border border-gray-200 p-4">
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">
{selectedWorkOrders.length} emri seçildi
</span>
<div className="flex space-x-2">
<button
onClick={() => setShowStartModal(true)}
className="bg-green-600 text-white px-2.5 py-1.5 rounded text-sm hover:bg-green-700"
>
Başlat
</button>
<button
onClick={() => setShowAssignModal(true)}
className="bg-blue-600 text-white px-2.5 py-1.5 rounded text-sm hover:bg-blue-700"
>
Atama Yap
</button>
<button
onClick={() => setShowStatusChangeModal(true)}
className="bg-orange-600 text-white px-2.5 py-1.5 rounded text-sm hover:bg-orange-700"
>
Durum Değiştir
</button>
<button
onClick={() => setSelectedWorkOrders([])}
className="bg-gray-600 text-white px-2.5 py-1.5 rounded text-sm hover:bg-gray-700"
>
Temizle
</button>
</div>
</div>
</div>
)}
</div>
{/* Modals */}
{showModal && !editingWorkOrder && (
<NewWorkOrderModal
isOpen={showModal}
onClose={() => setShowModal(false)}
onSave={handleSaveWorkOrder}
/>
)}
{editingWorkOrder && (
<EditWorkOrderModal
isOpen={showModal}
onClose={() => {
setShowModal(false)
setEditingWorkOrder(null)
}}
onSave={handleUpdateWorkOrder}
workOrder={editingWorkOrder}
/>
)}
{viewingWorkOrder && (
<ViewWorkOrderModal
isOpen={!!viewingWorkOrder}
onClose={() => setViewingWorkOrder(null)}
onEdit={(workOrder) => {
setViewingWorkOrder(null)
setEditingWorkOrder(workOrder)
setShowModal(true)
}}
workOrder={viewingWorkOrder}
/>
)}
{showStartModal && selectedWorkOrders.length > 0 && (
<StartWorkOrderModal
isOpen={showStartModal}
onClose={() => setShowStartModal(false)}
onStart={handleStartWorkOrders}
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
/>
)}
{showAssignModal && selectedWorkOrders.length > 0 && (
<AssignWorkOrdersModal
isOpen={showAssignModal}
onClose={() => setShowAssignModal(false)}
onAssign={handleAssignWorkOrders}
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
/>
)}
{showStatusChangeModal && selectedWorkOrders.length > 0 && (
<ChangeWorkOrderStatusModal
isOpen={showStatusChangeModal}
onClose={() => setShowStatusChangeModal(false)}
onStatusChange={handleChangeWorkOrderStatus}
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
/>
)}
</Container>
)
}
export default MaintenanceWorkOrders