erp-platform/ui/src/views/mrp/components/PlanningGantt.tsx

626 lines
23 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
import React, { useState, useMemo } from "react";
import {
FaUser,
FaChevronDown,
FaChevronRight,
FaChevronLeft,
FaChevronRight as FaArrowRight,
} from "react-icons/fa";
import { PsGanttTask } from "../../../types/ps";
import { mockEmployees } from "../../../mocks/mockEmployees";
import { mockProductionOrders } from "../../../mocks/mockProductionOrders";
import { mockWorkOrders } from "../../../mocks/mockWorkOrders";
import { mockWorkCenters } from "../../../mocks/mockWorkCenters";
import { PriorityEnum } from "../../../types/common";
import {
getProductionOrderStatus,
getWorkOrderStatus,
} from "../../../utils/erp";
2025-09-15 19:22:43 +00:00
import { Container } from "@/components/shared";
2025-09-15 09:31:47 +00:00
interface PlanningGanttProps {
workCenterId?: string;
}
2025-09-15 19:22:43 +00:00
const PlanningGantt: React.FC<PlanningGanttProps> = ({
2025-09-15 09:31:47 +00:00
workCenterId,
}) => {
const getInitialExpandedItems = () => {
const expandedItems = new Set<string>();
const firstTwoOrders = mockProductionOrders.slice(0, 2);
firstTwoOrders.forEach((order) => {
expandedItems.add(`prod-${order.id}`);
const orderWorkOrders = mockWorkOrders.filter(
(wo) => wo.productionOrderId === order.id
);
orderWorkOrders.forEach((wo) => {
expandedItems.add(`work-${wo.id}`);
});
});
return expandedItems;
};
const [expandedItems, setExpandedItems] = useState<Set<string>>(
getInitialExpandedItems()
);
const [selectedWorkCenter, setSelectedWorkCenter] = useState<string>(
workCenterId || ""
);
const [viewMode, setViewMode] = useState<"day" | "week" | "month" | "year">(
"week"
);
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const generateDateRange = () => {
const startDate = new Date(currentDate);
if (viewMode === "day") {
const hours = [];
for (let i = 0; i < 24; i++) {
const hour = new Date(startDate);
hour.setHours(i, 0, 0, 0);
hours.push(hour);
}
return hours;
} else if (viewMode === "week") {
const weekStart = new Date(startDate);
const dayOfWeek = weekStart.getDay();
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
weekStart.setDate(weekStart.getDate() - daysToSubtract);
weekStart.setHours(0, 0, 0, 0);
const dates = [];
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(weekStart.getDate() + i);
dates.push(date);
}
return dates;
} else if (viewMode === "month") {
const monthStart = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
1
);
const daysInMonth = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
0
).getDate();
const dates = [];
for (let i = 0; i < daysInMonth; i++) {
const date = new Date(monthStart);
date.setDate(monthStart.getDate() + i);
dates.push(date);
}
return dates;
} else {
const yearStart = new Date(currentDate.getFullYear(), 0, 1);
const months = [];
for (let i = 0; i < 12; i++) {
const month = new Date(yearStart);
month.setMonth(i);
months.push(month);
}
return months;
}
};
const dateRange = generateDateRange();
const filteredData = useMemo(() => {
const createGanttData = (): PsGanttTask[] => {
return mockProductionOrders.map((order) => {
const workOrders = mockWorkOrders
.filter((wo) => wo.productionOrderId === order.id)
.map((wo) => ({
id: `work-${wo.id}`,
name: wo.operation?.name || wo.workOrderNumber,
type: "task" as const,
startDate: wo.plannedStartDate,
endDate: wo.plannedEndDate,
progress:
Math.round((wo.confirmedQuantity / wo.plannedQuantity) * 100) ||
0,
assignee: wo.assignedOperators[0]
? mockEmployees.find((e) => e.id === wo.assignedOperators[0])
: undefined,
status: getWorkOrderStatus(wo.status),
priority: order.priority,
level: 1,
children: [],
estimatedHours: wo.processTime / 60,
hoursWorked: wo.actualProcessTime ? wo.actualProcessTime / 60 : 0,
}));
return {
id: `prod-${order.id}`,
name: `${order.orderNumber} - ${order.materials
.map((m) => m.material?.name)
.join(", ")}`,
type: "project" as const,
startDate: order.plannedStartDate,
endDate: order.plannedEndDate,
progress:
order.confirmedQuantity && order.plannedQuantity
? Math.round(
(order.confirmedQuantity / order.plannedQuantity) * 100
)
: 0,
status: getProductionOrderStatus(order.status),
priority: order.priority,
level: 0,
children: workOrders,
};
});
};
const filterByWorkCenter = (tasks: PsGanttTask[]): PsGanttTask[] => {
if (!selectedWorkCenter) return tasks;
return tasks
.map((task) => {
if (
task.type === "task" &&
task.assignee?.id !== selectedWorkCenter
) {
return null;
}
if (task.children) {
const filteredChildren = filterByWorkCenter(task.children).filter(
Boolean
) as PsGanttTask[];
if (filteredChildren.length > 0) {
return { ...task, children: filteredChildren };
} else if (
task.type === "task" &&
task.assignee?.id === selectedWorkCenter
) {
return task;
}
return null;
}
return task;
})
.filter(Boolean) as PsGanttTask[];
};
const allGanttTasks = createGanttData();
return filterByWorkCenter(allGanttTasks);
}, [selectedWorkCenter]);
const toggleExpand = (id: string) => {
const newExpanded = new Set(expandedItems);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedItems(newExpanded);
};
const getPriorityColor = (priority: PriorityEnum) => {
switch (priority) {
case PriorityEnum.High:
return "text-red-600 bg-red-50 border-red-200";
case PriorityEnum.Normal:
return "text-blue-600 bg-blue-50 border-blue-200";
case PriorityEnum.Low:
return "text-green-600 bg-green-50 border-green-200";
default:
return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const navigatePrevious = () => {
const newDate = new Date(currentDate);
if (viewMode === "day") newDate.setDate(newDate.getDate() - 1);
else if (viewMode === "week") newDate.setDate(newDate.getDate() - 7);
else if (viewMode === "month") newDate.setMonth(newDate.getMonth() - 1);
else newDate.setFullYear(newDate.getFullYear() - 1);
setCurrentDate(newDate);
};
const navigateNext = () => {
const newDate = new Date(currentDate);
if (viewMode === "day") newDate.setDate(newDate.getDate() + 1);
else if (viewMode === "week") newDate.setDate(newDate.getDate() + 7);
else if (viewMode === "month") newDate.setMonth(newDate.getMonth() + 1);
else newDate.setFullYear(newDate.getFullYear() + 1);
setCurrentDate(newDate);
};
const navigateToday = () => {
setCurrentDate(new Date());
};
const getCurrentDateInfo = () => {
if (viewMode === "day") {
return currentDate.toLocaleDateString("tr-TR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
} else if (viewMode === "week") {
const weekStart = new Date(currentDate);
const dayOfWeek = weekStart.getDay();
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
weekStart.setDate(weekStart.getDate() - daysToSubtract);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
return `${weekStart.toLocaleDateString("tr-TR", {
day: "numeric",
month: "short",
})} - ${weekEnd.toLocaleDateString("tr-TR", {
day: "numeric",
month: "short",
year: "numeric",
})}`;
} else if (viewMode === "month") {
return currentDate.toLocaleDateString("tr-TR", {
year: "numeric",
month: "long",
});
} else {
return currentDate.toLocaleDateString("tr-TR", { year: "numeric" });
}
};
const calculateTaskPosition = (startDate: Date, endDate: Date) => {
const chartStart = dateRange[0];
const chartEnd = dateRange[dateRange.length - 1];
const taskStart = new Date(startDate);
const taskEnd = new Date(endDate);
if (viewMode === "day") {
const dayStart = new Date(chartStart);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(chartStart);
dayEnd.setHours(23, 59, 59, 999);
if (taskEnd < dayStart || taskStart > dayEnd)
return { left: "100%", width: "0%", isVisible: false };
const startHour =
taskStart > dayStart
? taskStart.getHours() + taskStart.getMinutes() / 60
: 0;
const endHour =
taskEnd < dayEnd ? taskEnd.getHours() + taskEnd.getMinutes() / 60 : 24;
const left = (startHour / 24) * 100;
const width = Math.max(1, ((endHour - startHour) / 24) * 100);
return { left: `${left}%`, width: `${width}%`, isVisible: true };
} else if (viewMode === "week") {
const weekStart = new Date(chartStart);
const weekEnd = new Date(chartEnd);
weekEnd.setHours(23, 59, 59, 999);
if (taskEnd < weekStart || taskStart > weekEnd)
return { left: "100%", width: "0%", isVisible: false };
const startDay =
taskStart > weekStart
? Math.floor(
(taskStart.getTime() - weekStart.getTime()) /
(1000 * 60 * 60 * 24)
)
: 0;
const endDay =
taskEnd < weekEnd
? Math.ceil(
(taskEnd.getTime() - weekStart.getTime()) / (1000 * 60 * 60 * 24)
)
: 7;
const left = (startDay / 7) * 100;
const width = Math.max(1, ((endDay - startDay) / 7) * 100);
return { left: `${left}%`, width: `${width}%`, isVisible: true };
} else if (viewMode === "month") {
const monthStart = new Date(chartStart);
const monthEnd = new Date(chartEnd);
monthEnd.setHours(23, 59, 59, 999);
if (taskEnd < monthStart || taskStart > monthEnd)
return { left: "100%", width: "0%", isVisible: false };
const daysInMonth = dateRange.length;
const startDay =
taskStart > monthStart
? Math.floor(
(taskStart.getTime() - monthStart.getTime()) /
(1000 * 60 * 60 * 24)
)
: 0;
const endDay =
taskEnd < monthEnd
? Math.ceil(
(taskEnd.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24)
)
: daysInMonth;
const left = (startDay / daysInMonth) * 100;
const width = Math.max(1, ((endDay - startDay) / daysInMonth) * 100);
return { left: `${left}%`, width: `${width}%`, isVisible: true };
} else {
const yearStart = new Date(chartStart);
const yearEnd = new Date(chartEnd);
yearEnd.setMonth(11, 31);
yearEnd.setHours(23, 59, 59, 999);
if (taskEnd < yearStart || taskStart > yearEnd)
return { left: "100%", width: "0%", isVisible: false };
const startMonth = taskStart > yearStart ? taskStart.getMonth() : 0;
const endMonth = taskEnd < yearEnd ? taskEnd.getMonth() + 1 : 12;
const left = (startMonth / 12) * 100;
const width = Math.max(1, ((endMonth - startMonth) / 12) * 100);
return { left: `${left}%`, width: `${width}%`, isVisible: true };
}
};
const renderTask = (task: PsGanttTask): React.ReactNode => {
const hasChildren = task.children && task.children.length > 0;
const isExpanded = expandedItems.has(task.id);
const indent = task.level * 20;
return (
<React.Fragment key={task.id}>
<div className="flex items-center border-b border-gray-100 hover:bg-gray-50 min-h-[36px]">
{/* Task Info */}
<div className="w-1/3 sm:w-1/4 p-1 border-r border-gray-200">
<div
className="flex items-center"
style={{ paddingLeft: `${Math.min(indent, 30)}px` }}
>
{hasChildren && (
<button
onClick={() => toggleExpand(task.id)}
className="mr-1 p-0.5 hover:bg-gray-200 rounded"
>
{isExpanded ? (
<FaChevronDown className="h-2.5 w-2.5" />
) : (
<FaChevronRight className="h-2.5 w-2.5" />
)}
</button>
)}
{!hasChildren && <div className="w-3 sm:w-5"></div>}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1 sm:gap-2">
<span
className={`font-medium text-xs truncate ${
task.type === "project"
? "text-blue-900 font-semibold"
: "text-gray-900"
}`}
title={task.name}
>
{task.name}
</span>
<span
className={`px-1 py-0 text-[10px] border rounded flex-shrink-0 hidden sm:inline ${getPriorityColor(
task.priority
)}`}
>
{task.priority}
</span>
</div>
{task.type === "task" && task.assignee && (
<div className="flex items-center gap-1 mt-0.5 text-xs text-gray-600">
<FaUser className="h-2 w-2 flex-shrink-0" />
<span className="truncate text-xs">
{task.assignee.firstName} {task.assignee.lastName}
</span>
{task.hoursWorked && task.estimatedHours && (
<span className="text-xs bg-gray-100 px-1 py-0.5 rounded flex-shrink-0 hidden md:inline">
{task.hoursWorked}h / {task.estimatedHours}h
</span>
)}
</div>
)}
<div className="mt-1 text-xs text-gray-500 truncate hidden sm:block">
{task.startDate.toLocaleDateString("tr-TR")} -{" "}
{task.endDate.toLocaleDateString("tr-TR")}
</div>
</div>
</div>
</div>
{/* Gantt Chart */}
<div className="w-2/3 sm:w-3/4 p-1 relative">
<div className="relative h-6 bg-gray-100 rounded">
{(() => {
const position = calculateTaskPosition(
task.startDate,
task.endDate
);
if (task.type === "task" && position.isVisible) {
return (
<div
className={`absolute h-5 mt-0.5 rounded shadow-sm group cursor-pointer border border-white border-opacity-20`}
style={{ left: position.left, width: position.width }}
title={`${task.name} - ${task.progress}% tamamlandı`}
>
<div
className="h-full bg-black bg-opacity-15 rounded-lg relative overflow-hidden"
style={{ width: `${task.progress}%` }}
>
<div className="absolute inset-0 bg-white bg-opacity-20"></div>
</div>
<div className="absolute inset-0 flex items-center justify-center text-white text-[10px] font-bold drop-shadow-sm z-10">
{task.progress}%
</div>
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap z-20 hidden sm:block">
<div className="font-semibold">{task.name}</div>
<div className="mt-1">
İlerleme: {task.progress}% tamamlandı
</div>
<div>
Planlanan Başlangıç:{" "}
{task.startDate.toLocaleDateString("tr-TR")}
</div>
<div>
Planlanan Bitiş:{" "}
{task.endDate.toLocaleDateString("tr-TR")}
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-gray-800"></div>
</div>
</div>
);
}
if (task.type !== "task" && position.isVisible) {
return (
<div
className="absolute h-2 mt-2 bg-gradient-to-r from-gray-400 to-gray-600 rounded-full group cursor-pointer shadow-sm border border-white border-opacity-30"
style={{ left: position.left, width: position.width }}
title={`${task.name}`}
>
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap z-20 hidden sm:block">
<div className="font-semibold">{task.name}</div>
<div>
Planlanan Başlangıç:{" "}
{task.startDate.toLocaleDateString("tr-TR")}
</div>
<div>
Planlanan Bitiş:{" "}
{task.endDate.toLocaleDateString("tr-TR")}
</div>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-gray-800"></div>
</div>
</div>
);
}
return null;
})()}
</div>
</div>
</div>
{hasChildren &&
isExpanded &&
task.children?.map((child) => renderTask(child))}
</React.Fragment>
);
};
return (
2025-09-15 19:22:43 +00:00
<Container>
<div className="mb-3">
2025-09-15 09:31:47 +00:00
<div className="flex items-center gap-3">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">
2025-09-15 09:31:47 +00:00
Planlama Gantt Şeması
</h2>
2025-09-15 21:02:48 +00:00
<p className="text-gray-600">
2025-09-15 09:31:47 +00:00
Üretim ve emirlerinizi zaman çizelgesinde yönetin.
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border h-full flex flex-col">
<div className="p-2 border-b border-gray-200 flex-shrink-0">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center justify-center gap-2">
<button
onClick={navigatePrevious}
className="p-1.5 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<FaChevronLeft className="h-4 w-4" />
</button>
<div className="text-sm font-semibold text-gray-900 min-w-[180px] text-center">
{getCurrentDateInfo()}
</div>
<button
onClick={navigateNext}
className="p-1.5 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<FaArrowRight className="h-4 w-4" />
</button>
<button
onClick={navigateToday}
className="px-2 py-1 text-xs bg-blue-600 text-white hover:bg-blue-700 rounded-lg transition-colors"
>
Bugün
</button>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-2">
<select
value={selectedWorkCenter}
onChange={(e) => setSelectedWorkCenter(e.target.value)}
className="px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
>
<option value="">Tüm İş Merkezleri</option>
{mockWorkCenters.map((wc) => (
<option key={wc.id} value={wc.id}>
{wc.name}
</option>
))}
</select>
<select
value={viewMode}
onChange={(e) =>
setViewMode(
e.target.value as "day" | "week" | "month" | "year"
)
}
className="px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full sm:w-auto"
>
<option value="day">Günlük</option>
<option value="week">Haftalık</option>
<option value="month">Aylık</option>
<option value="year">Yıllık</option>
</select>
</div>
</div>
</div>
<div className="flex border-b border-gray-200 flex-shrink-0">
<div className="w-1/3 sm:w-1/4 p-2 border-r border-gray-200 bg-gray-50 font-semibold text-xs">
Üretim Emri / İş Emri
</div>
<div className="w-2/3 sm:w-3/4 p-2 bg-gray-50">
<div className="flex justify-between text-xs font-semibold">
{dateRange.map((date) => (
<div
key={date.toISOString()}
className="text-center min-w-0 flex-1"
>
<div className="truncate">
{viewMode === "day"
? date.toLocaleTimeString("tr-TR", {
hour: "2-digit",
minute: "2-digit",
})
: viewMode === "week"
? date.toLocaleDateString("tr-TR", { day: "2-digit" })
: viewMode === "year"
? date.toLocaleDateString("tr-TR", { month: "short" })
: date.toLocaleDateString("tr-TR", { day: "2-digit" })}
</div>
{viewMode !== "day" && viewMode !== "year" && (
<div className="text-xs text-gray-500 hidden sm:block">
{date.toLocaleDateString("tr-TR", { weekday: "short" })}
</div>
)}
{viewMode === "year" && (
<div className="text-xs text-gray-500 hidden sm:block">
{date.getFullYear()}
</div>
)}
</div>
))}
</div>
</div>
</div>
<div className="flex-1 overflow-auto">
{filteredData.map((task) => renderTask(task))}
</div>
</div>
2025-09-15 19:22:43 +00:00
</Container>
2025-09-15 09:31:47 +00:00
);
};
2025-09-15 19:22:43 +00:00
export default PlanningGantt;