<template> <div class="gantt-container"> <div class="gantt-header"> <!-- Calendar: เชื่อมกับ selectedDate --> <GanttCalendar /> <!-- ปุ่มจัดเรียงและเพิ่มระยะห่าง --> <div class="gantt-buttons"> <MakequeueBtn @click="openAddQueueDialog" /> <SettingBtn class="mr-4" /> </div> </div> <!-- Gantt Chart UI --> <div class="gantt-chart"> <!-- Header: Time Scale --> <div class="header"> <div class="machine-label"></div> <div class="time-scale"> <div v-for="hour in hours" :key="hour" class="time-cell"> {{ formatHour(hour) }} </div> </div> </div> <!-- Rows: แสดงเครื่องจักรจาก Machine Store --> <div class="rows"> <div v-for="machine in machineStore.machines" :key="machine.MachineID" class="row" :data-machine-id="machine.MachineID" @dragover.prevent="onDragOver($event)" @drop="onDrop($event, machine.MachineID)" > <div class="machine-label"> {{ machine.name }} </div> <div class="row-timeline"> <!-- เส้นแนวตั้ง (Grid Lines) --> <div v-for="hour in hours" :key="'line-' + hour" class="vertical-line" :style="getLineStyle(hour)" ></div> <!-- แสดง Queue ที่ตรงกับวันที่ (selectedDate), Page (pagenum), และ Machine --> <div v-for="item in filteredQueue(machine.MachineID)" :key="item.QueueID" class="order" :class="{ faded: draggingQueue && draggingQueue.QueueID === item.QueueID }" :style="getQueueStyle(item)" > <!-- Handle สำหรับ Resize ด้านซ้าย --> <div class="resize-handle left" @mousedown="onResizeStart($event, item, 'left')"></div> <!-- ส่วนกลางของ Order ใช้สำหรับลาก และเปิด Dialog เมื่อคลิก --> <div class="order-content" draggable="true" @dragstart="onDragStart($event, item)" @dragend="onDragEnd($event, item)" @click.stop="openQueueDialog(item)" > {{ item.orderID }} ({{ getTimeString(item.startTime) }} - {{ getTimeString(item.finishTime) }}) </div> <!-- Handle สำหรับ Resize ด้านขวา --> <div class="resize-handle right" @mousedown="onResizeStart($event, item, 'right')"></div> </div> </div> </div> </div> <!-- Ghost Queue (ขณะลาก/resize) --> <div v-if="ghostQueue" class="drag-ghost" :style="ghostStyle"> {{ ghostQueue.orderID }} ({{ getTimeString(ghostQueue.startTime) }} - {{ getTimeString(ghostQueue.finishTime) }}) </div> <v-divider :thickness="7"></v-divider> <!-- Pagination --> <div class="pagination-container"> <div class="pagination"> <button v-for="p in pages" :key="p" :class="['page-btn', { active: p === currentPage }]" @click="currentPage = p" @contextmenu.prevent="onPageRightClick(p, $event)" > {{ p }} <button v-if="pageToShowDelete === p" class="delete-btn" @click.stop="deletePage(p)" > Delete </button> </button> <button class="page-btn add-page" @click="addPage" :disabled="pages.length >= 10"> + </button> </div> <div class="pagination-right"> <v-btn class="add-page-btn" @click="openAddQueueDialog" icon> <v-icon>mdi-plus</v-icon> </v-btn> </div> <AddQueueDialog :visible="showAddDialog" :queueItem="selectedQueueItem" :currentPage="currentPage" @close="closeAddQueueDialog" /> </div> <OrderDialog v-if="selectedQueueItem && !showAddDialog" :queueItem="selectedQueueItem" :color="getQueueColor(selectedQueueItem.orderID)" @close="closeQueueDialog" @edit="openAddQueueDialogForEdit" @delete="handleDelete" /> </div> </div> </template> <script lang="ts" setup> import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { useQueueStore } from '@/stores/queue'; import { useMachineStore } from '@/stores/machine'; import { usePageStore } from '@/stores/page'; import { usePageContextStore } from '@/stores/pageContext'; import { useDateStore } from '@/stores/dateStore'; import { storeToRefs } from 'pinia'; import GanttCalendar from './GanttCalendar.vue'; import OrderDialog from './OrderDialog.vue'; import AddQueueDialog from './AddQueueDialog.vue'; import MakequeueBtn from './MakequeueBtn.vue'; import SettingBtn from './SettingBtn.vue'; import './ganttChart.css'; // Stores const queueStore = useQueueStore(); const machineStore = useMachineStore(); const pageStore = usePageStore(); const dateStore = useDateStore(); const { currentDate: selectedDate } = storeToRefs(dateStore); const pageContext = usePageContextStore(); // ใช้ currentPage เป็น pagenum จาก pageContext const currentPage = computed({ get: () => pageContext.currentPage, set: (val) => (pageContext.currentPage = val), }); // เมื่อเข้าหน้า ให้ดึงข้อมูลจาก backend onMounted(() => { machineStore.fetchMachines(); queueStore.fetchQueues(); pageStore.fetchPages(); }); // State สำหรับ Gantt Chart const showAddDialog = ref(false); const startTime = ref('06:00'); const endTime = ref('20:00'); const draggingQueue = ref<any | null>(null); const dragOffset = ref(0); const resizingQueue = ref<any | null>(null); const resizeDirection = ref<string | null>(null); const ghostQueue = ref<any | null>(null); const ghostStyle = reactive({ position: 'fixed', top: '0px', left: '0px', display: 'none', width: 'auto', height: '40px', lineHeight: '40px', padding: '0 10px', borderRadius: '10px', pointerEvents: 'none', backgroundColor: '#4caf50', color: '#fff', zIndex: 9999, }); // แปลงข้อมูล Queue จาก backend ให้เข้ากับรูปแบบที่ Gantt Chart ใช้ const formattedQueues = computed(() => { return queueStore.queues.map(q => ({ QueueID: q.QueueID, orderID: q.order?.OrderID || "Unknown", machineID: q.machine?.MachineID || 0, machineName: q.machine?.name || "Unknown Machine", startTime: q.startTime ? convertToLocalTime(q.startTime) : "Unknown", finishTime: q.finishTime ? convertToLocalTime(q.finishTime) : "Unknown", status: q.status || "Unknown", bottleSize: q.bottleSize || "Unknown", producedQuantity: q.producedQuantity || 0, // เปลี่ยนจาก PageID เป็น pagenum pageNumber: q.page?.pagenum || 0, })); }); function convertToLocalTime(utcString: string): string { const date = new Date(utcString); date.setHours(date.getHours() + 7); return date.toISOString().slice(0, 16).replace("T", " "); } // Pagination & Dialog const pages = computed(() => pageStore.pages.map(p => p.pagenum)); const pageToShowDelete = ref<number | null>(null); const selectedQueueItem = ref<any | null>(null); watch( () => queueStore.queues, (newVal) => { console.log("🔄 Queues updated in GanttChart.vue:", newVal); }, { deep: true } ); const hours = computed(() => { const startHour = parseInt(startTime.value.split(':')[0]); const endHour = parseInt(endTime.value.split(':')[0]); return Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i); }); // Methods function openQueueDialog(item: any) { selectedQueueItem.value = { ...item }; } function closeQueueDialog() { selectedQueueItem.value = null; } function openAddQueueDialog() { selectedQueueItem.value = null; showAddDialog.value = true; } function openAddQueueDialogForEdit(queueItem: any) { selectedQueueItem.value = { ...queueItem }; showAddDialog.value = true; } function handleDelete(queueItem: { QueueID: number; }) { if (!queueItem) return; queueStore .deleteQueue(queueItem.QueueID) .then(() => { console.log(`✅ Queue ID ${queueItem.QueueID} ถูกลบสำเร็จ`); closeAddQueueDialog(); }) .catch((error) => console.error(`❌ Error deleting queue:`, error)); } function closeAddQueueDialog() { showAddDialog.value = false; selectedQueueItem.value = null; } function formatHour(hour: number): string { return (hour < 10 ? '0' + hour : hour) + ':00'; } function getDateString(dateTimeStr: string): string { return dateTimeStr.split(' ')[0]; } function getTimeString(dateTimeStr: string): string { return dateTimeStr.split(' ')[1]; } function filteredQueue(machineID: number) { return formattedQueues.value.filter(q => { if (!q || !q.startTime || !q.finishTime) return false; const queueDate = getDateString(q.startTime); return ( q.machineID === machineID && q.pageNumber === currentPage.value && queueDate === selectedDate.value ); }); } const colorMap: Record<string, string> = {}; function getQueueColor(orderID: string | number): string { const key = String(orderID); if (key === "ผลิตเผื่อ") return "#FFC1C1"; if (key === "เปลี่ยนขนาด") return "#E1BEE7"; if (colorMap[key]) return colorMap[key]; const newColor = generateWaterPastel(); colorMap[key] = newColor; return newColor; } function generateWaterPastel(): string { const waterHues = [180, 190, 200, 210, 220, 230, 240]; const hue = waterHues[Math.floor(Math.random() * waterHues.length)]; return `hsl(${hue}, 60%, 80%)`; } function getQueueStyle(item: any) { const start = getTimeString(item.startTime); const end = getTimeString(item.finishTime); const startDecimal = timeToDecimal(start); const endDecimal = timeToDecimal(end); const totalHours = hours.value.length; const leftPercent = ((startDecimal - timeToDecimal(startTime.value)) / totalHours) * 100; const widthPercent = ((endDecimal - startDecimal) / totalHours) * 100; const orderID = item.orderID || "Unknown"; const backgroundColor = getQueueColor(orderID); console.log(`Queue ${item.QueueID}: orderID=${orderID}, color=${backgroundColor}`); return { left: `${leftPercent}%`, width: `${widthPercent}%`, backgroundColor, color: '#333', borderRadius: '10px', textAlign: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '5px', fontSize: '14px', fontWeight: 'bold', boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', }; } function getLineStyle(hour: number) { const totalHours = hours.value.length; const leftPercent = ((hour - timeToDecimal(startTime.value)) / totalHours) * 100; return { left: `${leftPercent}%` }; } function timeToDecimal(timeStr: string): number { const [h, m] = timeStr.split(':').map(Number); return h + m / 60; } function decimalToTime(decimal: number): string { const hrs = Math.floor(decimal); const minutes = Math.round((decimal - hrs) * 60); return (hrs < 10 ? '0' + hrs : hrs) + ':' + (minutes < 10 ? '0' + minutes : minutes); } // Drag/Drop & Resize Functions function onDragStart(event: DragEvent, item: any) { draggingQueue.value = item; const target = event.target as HTMLElement; const rect = target.getBoundingClientRect(); dragOffset.value = rect.width / 2; const emptyImg = new Image(); emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; event.dataTransfer?.setDragImage(emptyImg, 0, 0); ghostQueue.value = { ...item }; ghostStyle.display = 'block'; ghostStyle.backgroundColor = getQueueColor(item.order?.OrderID || "Unknown"); document.addEventListener('dragover', onDragOverGlobal); document.addEventListener('dragend', onDragEndGlobal); } function onDragOver(event: DragEvent) { event.preventDefault(); } function onDragOverGlobal(event: DragEvent) { event.preventDefault(); const rowTimeline = document.querySelector('.row-timeline') as HTMLElement; if (!rowTimeline) return; const timelineRect = rowTimeline.getBoundingClientRect(); const timelineWidth = timelineRect.width; const offsetX = event.clientX - timelineRect.left; let ratio = offsetX / timelineWidth; ratio = Math.min(Math.max(ratio, 0), 1); const timelineStartDec = timeToDecimal(startTime.value); const timelineEndDec = timeToDecimal(endTime.value); if (!draggingQueue.value) return; const startDecimal = timeToDecimal(getTimeString(draggingQueue.value.startTime)); const endDecimal = timeToDecimal(getTimeString(draggingQueue.value.finishTime)); const duration = endDecimal - startDecimal; let newStartDecimal = timelineStartDec + ratio * (timelineEndDec - timelineStartDec); newStartDecimal = Math.round(newStartDecimal * 2) / 2; let newEndDecimal = newStartDecimal + duration; if (newEndDecimal > timelineEndDec) { newEndDecimal = timelineEndDec; newStartDecimal = newEndDecimal - duration; } const datePart = getDateString(draggingQueue.value.startTime); const newStartStr = datePart + ' ' + decimalToTime(newStartDecimal); const newEndStr = datePart + ' ' + decimalToTime(newEndDecimal); if (ghostQueue.value) { ghostQueue.value.startTime = newStartStr; ghostQueue.value.finishTime = newEndStr; } // คำนวณตำแหน่ง ghost element const snappedRatioStart = (newStartDecimal - timelineStartDec) / (timelineEndDec - timelineStartDec); const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth; const snappedRatioEnd = (newEndDecimal - timelineStartDec) / (timelineEndDec - timelineStartDec); const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth; // หาจุด row ที่ใกล้เคียงที่สุดและดึง machine id จาก attribute const rows = document.querySelectorAll('.row'); let closestRow: HTMLElement | null = null; let minDistance = Infinity; rows.forEach((row) => { const rect = (row as HTMLElement).getBoundingClientRect(); const distance = Math.abs(event.clientY - rect.top); if (distance < minDistance) { minDistance = distance; closestRow = row as HTMLElement; } }); if (closestRow) { const machineIDString = (closestRow as HTMLElement).dataset.machineId; if (machineIDString) { ghostQueue.value.machineID = parseInt(machineIDString, 10); } ghostStyle.top = (closestRow as HTMLElement).getBoundingClientRect().top + 'px'; } ghostStyle.left = snappedLeft + 'px'; ghostStyle.width = snappedWidth + 'px'; } function onDragEndGlobal() { ghostQueue.value = null; ghostStyle.display = 'none'; draggingQueue.value = null; document.removeEventListener('dragend', onDragEndGlobal); } async function onDragEnd(event: DragEvent, item: any) { if (!ghostQueue.value) return; console.log(`🔄 Updating Queue ${item.QueueID} after drag...`); item.startTime = ghostQueue.value.startTime; item.finishTime = ghostQueue.value.finishTime; const newMachineID = ghostQueue.value.machine?.MachineID || item.machineID; await queueStore.updateQueue(item.QueueID, { startTime: item.startTime, finishTime: item.finishTime, MachineID: newMachineID, }); ghostQueue.value = null; ghostStyle.display = 'none'; draggingQueue.value = null; } async function onDrop(event: DragEvent, newMachineID: number) { event.preventDefault(); if (!draggingQueue.value || !ghostQueue.value) return; console.log(`🔄 Dropped Queue ${draggingQueue.value.QueueID} to Machine ${newMachineID}`); draggingQueue.value.startTime = ghostQueue.value.startTime; draggingQueue.value.finishTime = ghostQueue.value.finishTime; draggingQueue.value.machineID = newMachineID; try { await queueStore.updateQueue(draggingQueue.value.QueueID, { startTime: draggingQueue.value.startTime, finishTime: draggingQueue.value.finishTime, MachineID: newMachineID, }); console.log(`✅ Queue ${draggingQueue.value.QueueID} updated to Machine ${newMachineID}`); } catch (error) { console.error(`❌ Error updating queue:`, error); } ghostQueue.value = null; ghostStyle.display = 'none'; draggingQueue.value = null; } function onResizeStart(event: MouseEvent, item: any, direction: string) { event.preventDefault(); ghostQueue.value = { ...item }; resizingQueue.value = item; resizeDirection.value = direction; document.addEventListener('mousemove', onResizing); document.addEventListener('mouseup', onResizeEnd); } function onResizing(event: MouseEvent) { if (!resizingQueue.value) return; const rowTimeline = document.querySelector('.row-timeline') as HTMLElement; if (!rowTimeline) return; const timelineRect = rowTimeline.getBoundingClientRect(); const timelineWidth = timelineRect.width; const offsetX = event.clientX - timelineRect.left; let ratio = offsetX / timelineWidth; ratio = Math.min(Math.max(ratio, 0), 1); const timelineStartDec = timeToDecimal(startTime.value); const timelineEndDec = timeToDecimal(endTime.value); const startDec = timeToDecimal(getTimeString(resizingQueue.value.startTime)); const endDec = timeToDecimal(getTimeString(resizingQueue.value.finishTime)); let newTimeDecimal = timelineStartDec + ratio * (timelineEndDec - timelineStartDec); newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; const datePart = getDateString(resizingQueue.value.startTime); if (resizeDirection.value === 'left') { if (newTimeDecimal >= endDec) return; const newStartStr = datePart + ' ' + decimalToTime(newTimeDecimal); ghostQueue.value.startTime = newStartStr; } else if (resizeDirection.value === 'right') { if (newTimeDecimal <= startDec) return; const newEndStr = datePart + ' ' + decimalToTime(newTimeDecimal); ghostQueue.value.finishTime = newEndStr; } } async function onResizeEnd() { if (!resizingQueue.value) return; console.log(`🔄 Updating Queue ${resizingQueue.value.QueueID} after resize...`); await queueStore.updateQueue(resizingQueue.value.QueueID, { startTime: ghostQueue.value.startTime, finishTime: ghostQueue.value.finishTime, }); resizingQueue.value = null; resizeDirection.value = null; ghostQueue.value = null; ghostStyle.display = 'none'; document.removeEventListener('mousemove', onResizing); document.removeEventListener('mouseup', onResizeEnd); } // Pagination Functions async function addPage() { if (pageStore.pages.length < 10) { await pageStore.addPage(); await pageStore.fetchPages(); // 👈 โหลดหน้าทั้งหมดใหม่ } else { alert('Maximum of 10 pages allowed.'); } } function onPageRightClick(page: number, event: MouseEvent) { event.preventDefault(); pageToShowDelete.value = page; } async function deletePage(pageNum: number) { await pageStore.removePage(pageNum); await pageStore.fetchPages(); // 👈 โหลดหน้าทั้งหมดใหม่ if (currentPage.value === pageNum) { const remaining = pageStore.pages.map(p => p.pagenum); currentPage.value = remaining.length > 0 ? remaining[0] : 1; } pageToShowDelete.value = null; } function onDocumentClick(event: MouseEvent) { const target = event.target as HTMLElement; if (!target.closest('.page-btn')) { pageToShowDelete.value = null; } } onMounted(() => { document.addEventListener('click', onDocumentClick); }); onBeforeUnmount(() => { document.removeEventListener('click', onDocumentClick); }); </script>