2025-09-16 13:19:02 +00:00
|
|
|
|
import React, { useState, useMemo } from 'react'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
import {
|
|
|
|
|
|
FaUser,
|
|
|
|
|
|
FaChevronDown,
|
|
|
|
|
|
FaChevronRight,
|
|
|
|
|
|
FaChevronLeft,
|
|
|
|
|
|
FaChevronRight as FaArrowRight,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
} 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 { getPriorityColor, getProductionOrderStatus, getWorkOrderStatus } from '../../../utils/erp'
|
|
|
|
|
|
import { Container } from '@/components/shared'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
interface PlanningGanttProps {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
workCenterId?: string
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const PlanningGantt: React.FC<PlanningGanttProps> = ({ workCenterId }) => {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const getInitialExpandedItems = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const expandedItems = new Set<string>()
|
|
|
|
|
|
const firstTwoOrders = mockProductionOrders.slice(0, 2)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
firstTwoOrders.forEach((order) => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
expandedItems.add(`prod-${order.id}`)
|
|
|
|
|
|
const orderWorkOrders = mockWorkOrders.filter((wo) => wo.productionOrderId === order.id)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
orderWorkOrders.forEach((wo) => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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())
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const generateDateRange = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const startDate = new Date(currentDate)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
if (viewMode === 'day') {
|
|
|
|
|
|
const hours = []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
for (let i = 0; i < 24; i++) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const hour = new Date(startDate)
|
|
|
|
|
|
hour.setHours(i, 0, 0, 0)
|
|
|
|
|
|
hours.push(hour)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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 = []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
for (let i = 0; i < 7; i++) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const date = new Date(weekStart)
|
|
|
|
|
|
date.setDate(weekStart.getDate() + i)
|
|
|
|
|
|
dates.push(date)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return dates
|
|
|
|
|
|
} else if (viewMode === 'month') {
|
|
|
|
|
|
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const daysInMonth = new Date(
|
|
|
|
|
|
currentDate.getFullYear(),
|
|
|
|
|
|
currentDate.getMonth() + 1,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
0,
|
|
|
|
|
|
).getDate()
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const dates = []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
for (let i = 0; i < daysInMonth; i++) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const date = new Date(monthStart)
|
|
|
|
|
|
date.setDate(monthStart.getDate() + i)
|
|
|
|
|
|
dates.push(date)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return dates
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const yearStart = new Date(currentDate.getFullYear(), 0, 1)
|
|
|
|
|
|
const months = []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
for (let i = 0; i < 12; i++) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const month = new Date(yearStart)
|
|
|
|
|
|
month.setMonth(i)
|
|
|
|
|
|
months.push(month)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return months
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const dateRange = generateDateRange()
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
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,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
type: 'task' as const,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
startDate: wo.plannedStartDate,
|
|
|
|
|
|
endDate: wo.plannedEndDate,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
progress: Math.round((wo.confirmedQuantity / wo.plannedQuantity) * 100) || 0,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
}))
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `prod-${order.id}`,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
name: `${order.orderNumber} - ${order.materials.map((m) => m.material?.name).join(', ')}`,
|
|
|
|
|
|
type: 'project' as const,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
startDate: order.plannedStartDate,
|
|
|
|
|
|
endDate: order.plannedEndDate,
|
|
|
|
|
|
progress:
|
|
|
|
|
|
order.confirmedQuantity && order.plannedQuantity
|
2025-09-16 13:19:02 +00:00
|
|
|
|
? Math.round((order.confirmedQuantity / order.plannedQuantity) * 100)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
: 0,
|
|
|
|
|
|
status: getProductionOrderStatus(order.status),
|
|
|
|
|
|
priority: order.priority,
|
|
|
|
|
|
level: 0,
|
|
|
|
|
|
children: workOrders,
|
2025-09-16 13:19:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const filterByWorkCenter = (tasks: PsGanttTask[]): PsGanttTask[] => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
if (!selectedWorkCenter) return tasks
|
2025-09-15 09:31:47 +00:00
|
|
|
|
return tasks
|
|
|
|
|
|
.map((task) => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
if (task.type === 'task' && task.assignee?.id !== selectedWorkCenter) {
|
|
|
|
|
|
return null
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (task.children) {
|
|
|
|
|
|
const filteredChildren = filterByWorkCenter(task.children).filter(
|
2025-09-16 13:19:02 +00:00
|
|
|
|
Boolean,
|
|
|
|
|
|
) as PsGanttTask[]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (filteredChildren.length > 0) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return { ...task, children: filteredChildren }
|
|
|
|
|
|
} else if (task.type === 'task' && task.assignee?.id === selectedWorkCenter) {
|
|
|
|
|
|
return task
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return null
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return task
|
2025-09-15 09:31:47 +00:00
|
|
|
|
})
|
2025-09-16 13:19:02 +00:00
|
|
|
|
.filter(Boolean) as PsGanttTask[]
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const allGanttTasks = createGanttData()
|
|
|
|
|
|
return filterByWorkCenter(allGanttTasks)
|
|
|
|
|
|
}, [selectedWorkCenter])
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const toggleExpand = (id: string) => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const newExpanded = new Set(expandedItems)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (newExpanded.has(id)) {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
newExpanded.delete(id)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
newExpanded.add(id)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
setExpandedItems(newExpanded)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const navigatePrevious = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const navigateNext = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const navigateToday = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
setCurrentDate(new Date())
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const getCurrentDateInfo = () => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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',
|
|
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return currentDate.toLocaleDateString('tr-TR', { year: 'numeric' })
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const calculateTaskPosition = (startDate: Date, endDate: Date) => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (taskEnd < dayStart || taskStart > dayEnd)
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return { left: '100%', width: '0%', isVisible: false }
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const startHour =
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (taskEnd < weekStart || taskStart > weekEnd)
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return { left: '100%', width: '0%', isVisible: false }
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const startDay =
|
|
|
|
|
|
taskStart > weekStart
|
2025-09-16 13:19:02 +00:00
|
|
|
|
? Math.floor((taskStart.getTime() - weekStart.getTime()) / (1000 * 60 * 60 * 24))
|
|
|
|
|
|
: 0
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const endDay =
|
|
|
|
|
|
taskEnd < weekEnd
|
2025-09-16 13:19:02 +00:00
|
|
|
|
? 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)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (taskEnd < monthStart || taskStart > monthEnd)
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return { left: '100%', width: '0%', isVisible: false }
|
|
|
|
|
|
const daysInMonth = dateRange.length
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const startDay =
|
|
|
|
|
|
taskStart > monthStart
|
2025-09-16 13:19:02 +00:00
|
|
|
|
? Math.floor((taskStart.getTime() - monthStart.getTime()) / (1000 * 60 * 60 * 24))
|
|
|
|
|
|
: 0
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const endDay =
|
|
|
|
|
|
taskEnd < monthEnd
|
2025-09-16 13:19:02 +00:00
|
|
|
|
? 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 }
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const yearStart = new Date(chartStart)
|
|
|
|
|
|
const yearEnd = new Date(chartEnd)
|
|
|
|
|
|
yearEnd.setMonth(11, 31)
|
|
|
|
|
|
yearEnd.setHours(23, 59, 59, 999)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (taskEnd < yearStart || taskStart > yearEnd)
|
2025-09-16 13:19:02 +00:00
|
|
|
|
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 }
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const renderTask = (task: PsGanttTask): React.ReactNode => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const hasChildren = task.children && task.children.length > 0
|
|
|
|
|
|
const isExpanded = expandedItems.has(task.id)
|
|
|
|
|
|
const indent = task.level * 20
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
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">
|
2025-09-16 13:19:02 +00:00
|
|
|
|
<div className="flex items-center" style={{ paddingLeft: `${Math.min(indent, 30)}px` }}>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
{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 ${
|
2025-09-16 13:19:02 +00:00
|
|
|
|
task.type === 'project' ? 'text-blue-900 font-semibold' : 'text-gray-900'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}`}
|
|
|
|
|
|
title={task.name}
|
|
|
|
|
|
>
|
|
|
|
|
|
{task.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`px-1 py-0 text-[10px] border rounded flex-shrink-0 hidden sm:inline ${getPriorityColor(
|
2025-09-16 13:19:02 +00:00
|
|
|
|
task.priority,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
)}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{task.priority}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{task.type === 'task' && task.assignee && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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">
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{task.startDate.toLocaleDateString('tr-TR')} -{' '}
|
|
|
|
|
|
{task.endDate.toLocaleDateString('tr-TR')}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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">
|
|
|
|
|
|
{(() => {
|
2025-09-16 13:19:02 +00:00
|
|
|
|
const position = calculateTaskPosition(task.startDate, task.endDate)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
if (task.type === 'task' && position.isVisible) {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
<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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
if (task.type !== 'task' && position.isVisible) {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
<div>Planlanan Başlangıç: {task.startDate.toLocaleDateString('tr-TR')}</div>
|
|
|
|
|
|
<div>Planlanan Bitiş: {task.endDate.toLocaleDateString('tr-TR')}</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
return null
|
2025-09-15 09:31:47 +00:00
|
|
|
|
})()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{hasChildren && isExpanded && task.children?.map((child) => renderTask(child))}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</React.Fragment>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
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-16 13:19:02 +00:00
|
|
|
|
<h2 className="text-2xl font-bold text-gray-900">Planlama Gantt Şeması</h2>
|
|
|
|
|
|
<p className="text-gray-600">Üretim ve iş emirlerinizi zaman çizelgesinde yönetin.</p>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
onChange={(e) => setViewMode(e.target.value as 'day' | 'week' | 'month' | 'year')}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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) => (
|
2025-09-16 13:19:02 +00:00
|
|
|
|
<div key={date.toISOString()} className="text-center min-w-0 flex-1">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="truncate">
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{viewMode === 'day'
|
|
|
|
|
|
? date.toLocaleTimeString('tr-TR', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
})
|
2025-09-16 13:19:02 +00:00
|
|
|
|
: viewMode === 'week'
|
|
|
|
|
|
? date.toLocaleDateString('tr-TR', { day: '2-digit' })
|
|
|
|
|
|
: viewMode === 'year'
|
|
|
|
|
|
? date.toLocaleDateString('tr-TR', { month: 'short' })
|
|
|
|
|
|
: date.toLocaleDateString('tr-TR', { day: '2-digit' })}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{viewMode !== 'day' && viewMode !== 'year' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="text-xs text-gray-500 hidden sm:block">
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{date.toLocaleDateString('tr-TR', { weekday: 'short' })}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-16 13:19:02 +00:00
|
|
|
|
{viewMode === 'year' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="text-xs text-gray-500 hidden sm:block">
|
|
|
|
|
|
{date.getFullYear()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
<div className="flex-1 overflow-auto">{filteredData.map((task) => renderTask(task))}</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
2025-09-15 19:22:43 +00:00
|
|
|
|
</Container>
|
2025-09-16 13:19:02 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 19:22:43 +00:00
|
|
|
|
|
2025-09-16 13:19:02 +00:00
|
|
|
|
export default PlanningGantt
|