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