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

723 lines
26 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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";
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 (
<div className="space-y-4 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">Bakım İş Emirleri</h2>
<p className="text-gray-600">
2025-09-15 09:31:47 +00:00
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>
<option value={WorkOrderStatusEnum.Created}>Oluşturuldu</option>
<option value={WorkOrderStatusEnum.Planned}>Planlandı</option>
<option value={WorkOrderStatusEnum.InProgress}>Devam Ediyor</option>
<option value={WorkOrderStatusEnum.Completed}>Tamamlandı</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>
<option value={WorkOrderTypeEnum.Preventive}>Önleyici</option>
<option value={WorkOrderTypeEnum.Corrective}>Düzeltici</option>
<option value={WorkOrderTypeEnum.Emergency}>Acil</option>
<option value={WorkOrderTypeEnum.Inspection}>İnceleme</option>
<option value={WorkOrderTypeEnum.Calibration}>Kalibrasyon</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>
)}
{/* 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)
)}
/>
)}
</div>
);
};
export default MaintenanceWorkOrders;