2025-09-15 21:29:07 +00:00
|
|
|
|
import React, { useState } from 'react'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
import {
|
|
|
|
|
|
FaPlus,
|
|
|
|
|
|
FaSearch,
|
|
|
|
|
|
FaFilter,
|
|
|
|
|
|
FaFileAlt,
|
|
|
|
|
|
FaClock,
|
|
|
|
|
|
FaCheckCircle,
|
|
|
|
|
|
FaExclamationTriangle,
|
|
|
|
|
|
FaEdit,
|
|
|
|
|
|
FaTrash,
|
|
|
|
|
|
FaEye,
|
|
|
|
|
|
FaDollarSign,
|
2025-09-15 21:29:07 +00:00
|
|
|
|
} 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'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
import {
|
|
|
|
|
|
getPriorityColor,
|
|
|
|
|
|
getPriorityText,
|
|
|
|
|
|
getWorkOrderStatusColor,
|
|
|
|
|
|
getWorkOrderStatusIcon,
|
|
|
|
|
|
getWorkOrderStatusText,
|
|
|
|
|
|
getWorkOrderTypeColor,
|
|
|
|
|
|
getWorkOrderTypeText,
|
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 MaintenanceWorkOrders: React.FC = () => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
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)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Mock data - replace with actual API calls
|
2025-09-15 21:29:07 +00:00
|
|
|
|
const [workOrders, setWorkOrders] = useState<PmMaintenanceWorkOrder[]>(mockMaintenanceWorkOrders)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const filteredWorkOrders = workOrders.filter((workOrder) => {
|
|
|
|
|
|
const matchesSearch =
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrder.workOrderNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
2025-09-15 09:31:47 +00:00
|
|
|
|
workOrder.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
|
workOrder.assignedTo?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrder.reportedBy.toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
|
|
|
const matchesStatus = statusFilter === 'all' || workOrder.status === statusFilter
|
|
|
|
|
|
const matchesType = typeFilter === 'all' || workOrder.orderType === typeFilter
|
|
|
|
|
|
return matchesSearch && matchesStatus && matchesType
|
|
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const getTotalMaterialCost = (workOrder: PmMaintenanceWorkOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
return workOrder.materials.reduce((total, material) => total + material.totalCost, 0)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const getCompletionPercentage = (workOrder: PmMaintenanceWorkOrder) => {
|
|
|
|
|
|
const completedActivities = workOrder.activities.filter(
|
2025-09-15 21:29:07 +00:00
|
|
|
|
(activity) => activity.completedAt,
|
|
|
|
|
|
).length
|
|
|
|
|
|
const totalActivities = workOrder.activities.length
|
|
|
|
|
|
return totalActivities > 0 ? Math.round((completedActivities / totalActivities) * 100) : 0
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const isOverdue = (workOrder: PmMaintenanceWorkOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
if (!workOrder.scheduledEnd) return false
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
return workOrder.scheduledEnd < now && workOrder.status !== WorkOrderStatusEnum.Completed
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleAddWorkOrder = () => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setEditingWorkOrder(null)
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleEdit = (workOrder: PmMaintenanceWorkOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setEditingWorkOrder(workOrder)
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleView = (workOrder: PmMaintenanceWorkOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setViewingWorkOrder(workOrder)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleSelectWorkOrder = (workOrderId: string) => {
|
|
|
|
|
|
setSelectedWorkOrders((prev) =>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
prev.includes(workOrderId) ? prev.filter((id) => id !== workOrderId) : [...prev, workOrderId],
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleSaveWorkOrder = (
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrderData: Omit<PmMaintenanceWorkOrder, 'id' | 'creationTime' | 'lastModificationTime'>,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
) => {
|
|
|
|
|
|
const newWorkOrder: PmMaintenanceWorkOrder = {
|
|
|
|
|
|
...workOrderData,
|
|
|
|
|
|
id: `wo-${Date.now()}`,
|
|
|
|
|
|
creationTime: new Date(),
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 21:29:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
setWorkOrders([...workOrders, newWorkOrder])
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleUpdateWorkOrder = (updatedWorkOrder: PmMaintenanceWorkOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setWorkOrders(workOrders.map((wo) => (wo.id === updatedWorkOrder.id ? updatedWorkOrder : wo)))
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleStartWorkOrders = (
|
|
|
|
|
|
selectedWorkOrders: PmMaintenanceWorkOrder[],
|
|
|
|
|
|
startData: {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
actualStart: Date
|
|
|
|
|
|
assignedTo?: string
|
|
|
|
|
|
maintenanceTeamId?: string
|
|
|
|
|
|
notes?: string
|
|
|
|
|
|
},
|
2025-09-15 09:31:47 +00:00
|
|
|
|
) => {
|
|
|
|
|
|
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,
|
2025-09-15 21:29:07 +00:00
|
|
|
|
maintenanceTeamId: startData.maintenanceTeamId || wo.maintenanceTeamId,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
notes: startData.notes
|
2025-09-15 21:29:07 +00:00
|
|
|
|
? `${wo.notes ? wo.notes + '\n' : ''}${startData.notes}`
|
2025-09-15 09:31:47 +00:00
|
|
|
|
: wo.notes,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 21:29:07 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
return wo
|
|
|
|
|
|
})
|
|
|
|
|
|
setWorkOrders(updatedWorkOrders)
|
|
|
|
|
|
setSelectedWorkOrders([])
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleAssignWorkOrders = (
|
|
|
|
|
|
selectedWorkOrders: PmMaintenanceWorkOrder[],
|
|
|
|
|
|
assignmentData: {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
assignmentType: 'person' | 'team'
|
|
|
|
|
|
assignedTo?: string
|
|
|
|
|
|
maintenanceTeamId?: string
|
|
|
|
|
|
notes?: string
|
|
|
|
|
|
},
|
2025-09-15 09:31:47 +00:00
|
|
|
|
) => {
|
|
|
|
|
|
const updatedWorkOrders = workOrders.map((wo) => {
|
|
|
|
|
|
if (selectedWorkOrders.some((swo) => swo.id === wo.id)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...wo,
|
|
|
|
|
|
assignedTo: assignmentData.assignedTo || wo.assignedTo,
|
2025-09-15 21:29:07 +00:00
|
|
|
|
maintenanceTeamId: assignmentData.maintenanceTeamId || wo.maintenanceTeamId,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
notes: assignmentData.notes
|
2025-09-15 21:29:07 +00:00
|
|
|
|
? `${wo.notes ? wo.notes + '\n' : ''}${assignmentData.notes}`
|
2025-09-15 09:31:47 +00:00
|
|
|
|
: wo.notes,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 21:29:07 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
return wo
|
|
|
|
|
|
})
|
|
|
|
|
|
setWorkOrders(updatedWorkOrders)
|
|
|
|
|
|
setSelectedWorkOrders([])
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleChangeWorkOrderStatus = (
|
|
|
|
|
|
selectedWorkOrders: PmMaintenanceWorkOrder[],
|
|
|
|
|
|
statusData: {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
newStatus: WorkOrderStatusEnum
|
|
|
|
|
|
notes?: string
|
|
|
|
|
|
completionNotes?: string
|
|
|
|
|
|
},
|
2025-09-15 09:31:47 +00:00
|
|
|
|
) => {
|
|
|
|
|
|
const updatedWorkOrders = workOrders.map((wo) => {
|
|
|
|
|
|
if (selectedWorkOrders.some((swo) => swo.id === wo.id)) {
|
|
|
|
|
|
const updates: Partial<PmMaintenanceWorkOrder> = {
|
|
|
|
|
|
status: statusData.newStatus,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 21:29:07 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
if (statusData.notes) {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
updates.notes = statusData.notes
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (statusData.completionNotes) {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
updates.completionNotes = statusData.completionNotes
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (statusData.newStatus === WorkOrderStatusEnum.Completed) {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
updates.actualEnd = new Date()
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
return { ...wo, ...updates }
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
return wo
|
|
|
|
|
|
})
|
|
|
|
|
|
setWorkOrders(updatedWorkOrders)
|
|
|
|
|
|
setSelectedWorkOrders([])
|
|
|
|
|
|
}
|
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 İş Emirleri</h2>
|
|
|
|
|
|
<p className="text-gray-600">Bakım iş 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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{/* Summary Cards */}
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
|
|
|
|
|
|
<Widget title="Toplam" value={workOrders.length} color="gray" icon="FaFileAlt" />
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<Widget
|
|
|
|
|
|
title="Devam Ediyor"
|
|
|
|
|
|
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.InProgress).length}
|
|
|
|
|
|
color="orange"
|
|
|
|
|
|
icon="FaWrench"
|
|
|
|
|
|
/>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<Widget
|
|
|
|
|
|
title="Planlandı"
|
|
|
|
|
|
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.Planned).length}
|
|
|
|
|
|
color="blue"
|
|
|
|
|
|
icon="FaCalendar"
|
|
|
|
|
|
/>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<Widget
|
|
|
|
|
|
title="Tamamlandı"
|
|
|
|
|
|
value={workOrders.filter((wo) => wo.status === WorkOrderStatusEnum.Completed).length}
|
|
|
|
|
|
color="green"
|
|
|
|
|
|
icon="FaCheckCircle"
|
|
|
|
|
|
/>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<Widget
|
|
|
|
|
|
title="Geciken"
|
|
|
|
|
|
value={workOrders.filter((wo) => isOverdue(wo)).length}
|
|
|
|
|
|
color="red"
|
|
|
|
|
|
icon="FaExclamationTriangle"
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
2025-09-17 09:02:03 +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>
|
|
|
|
|
|
<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>
|
2025-09-17 09:02:03 +00:00
|
|
|
|
{Object.values(WorkOrderTypeEnum).map((type) => (
|
|
|
|
|
|
<option key={type} value={type}>
|
|
|
|
|
|
{getWorkOrderTypeText(type)}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{/* Work Orders List */}
|
|
|
|
|
|
<div className="space-y-3 pt-2">
|
|
|
|
|
|
{filteredWorkOrders.map((workOrder) => {
|
|
|
|
|
|
const completionPercentage = getCompletionPercentage(workOrder)
|
|
|
|
|
|
const materialCost = getTotalMaterialCost(workOrder)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</span>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|
|
|
|
|
{workOrder.description}
|
|
|
|
|
|
</h4>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{/* 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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{/* Activities Summary */}
|
|
|
|
|
|
<div className="bg-gray-50 rounded-lg p-2 mb-2">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<span className="text-gray-600">Aktiviteler:</span>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<span className="font-medium text-gray-900">
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{workOrder.activities.filter((a) => a.completedAt).length} /{' '}
|
|
|
|
|
|
{workOrder.activities.length} tamamlandı
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<div className="text-gray-600">
|
|
|
|
|
|
Toplam:{' '}
|
|
|
|
|
|
{workOrder.activities.reduce(
|
|
|
|
|
|
(total, activity) => total + activity.plannedDuration,
|
|
|
|
|
|
0,
|
|
|
|
|
|
)}{' '}
|
|
|
|
|
|
dk
|
|
|
|
|
|
</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
<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" />
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</button>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)
|
|
|
|
|
|
})}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 21:29:07 +00:00
|
|
|
|
{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 iş 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} iş 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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-15 21:29:07 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
{/* Modals */}
|
|
|
|
|
|
{showModal && !editingWorkOrder && (
|
|
|
|
|
|
<NewWorkOrderModal
|
|
|
|
|
|
isOpen={showModal}
|
|
|
|
|
|
onClose={() => setShowModal(false)}
|
|
|
|
|
|
onSave={handleSaveWorkOrder}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{editingWorkOrder && (
|
|
|
|
|
|
<EditWorkOrderModal
|
|
|
|
|
|
isOpen={showModal}
|
|
|
|
|
|
onClose={() => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setShowModal(false)
|
|
|
|
|
|
setEditingWorkOrder(null)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
onSave={handleUpdateWorkOrder}
|
|
|
|
|
|
workOrder={editingWorkOrder}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{viewingWorkOrder && (
|
|
|
|
|
|
<ViewWorkOrderModal
|
|
|
|
|
|
isOpen={!!viewingWorkOrder}
|
|
|
|
|
|
onClose={() => setViewingWorkOrder(null)}
|
|
|
|
|
|
onEdit={(workOrder) => {
|
2025-09-15 21:29:07 +00:00
|
|
|
|
setViewingWorkOrder(null)
|
|
|
|
|
|
setEditingWorkOrder(workOrder)
|
|
|
|
|
|
setShowModal(true)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
workOrder={viewingWorkOrder}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showStartModal && selectedWorkOrders.length > 0 && (
|
|
|
|
|
|
<StartWorkOrderModal
|
|
|
|
|
|
isOpen={showStartModal}
|
|
|
|
|
|
onClose={() => setShowStartModal(false)}
|
|
|
|
|
|
onStart={handleStartWorkOrders}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showAssignModal && selectedWorkOrders.length > 0 && (
|
|
|
|
|
|
<AssignWorkOrdersModal
|
|
|
|
|
|
isOpen={showAssignModal}
|
|
|
|
|
|
onClose={() => setShowAssignModal(false)}
|
|
|
|
|
|
onAssign={handleAssignWorkOrders}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showStatusChangeModal && selectedWorkOrders.length > 0 && (
|
|
|
|
|
|
<ChangeWorkOrderStatusModal
|
|
|
|
|
|
isOpen={showStatusChangeModal}
|
|
|
|
|
|
onClose={() => setShowStatusChangeModal(false)}
|
|
|
|
|
|
onStatusChange={handleChangeWorkOrderStatus}
|
2025-09-15 21:29:07 +00:00
|
|
|
|
workOrders={workOrders.filter((wo) => selectedWorkOrders.includes(wo.id))}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
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 MaintenanceWorkOrders
|