<template> <div class="gantt-container"> <div class="gantt-header"> <!-- Calendar: เชื่อมกับ selectedDate --> <GanttCalendar v-model:modelValue="selectedDate" /> <!-- ปุ่ม Make a queue พร้อม event listener --> <MakequeueBtn @updateQueue="handleUpdateQueue" /> </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: เครื่องจักรแต่ละตัว --> <div class="rows"> <div v-for="machine in machines" :key="machine.id" class="row" @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, และ 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 (ขณะลาก) --> <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> <!-- ปุ่ม Add Queue อยู่ขวาสุด --> <div class="pagination-right"> <v-btn class="add-page-btn" @click="openAddQueueDialog" icon> <v-icon>mdi-plus</v-icon> </v-btn> </div> <!-- AddQueueDialog (สำหรับเพิ่ม/แก้ไข) --> <AddQueueDialog :visible="showAddDialog" :queueItem="selectedQueueItem" @close="closeAddQueueDialog" @add="handleAddQueue" @edit="handleEditQueue" /> </div> <!-- Order Dialog (สำหรับดูรายละเอียดเพิ่มเติม) --> <OrderDialog v-if="selectedQueueItem && !showAddDialog" :queueItem="selectedQueueItem" :color="getQueueColor(selectedQueueItem.orderID)" @close="closeQueueDialog" @edit="openAddQueueDialogForEdit" @delete="deleteQueueItem" /> </div> </div> </template> <script> import GanttCalendar from './GanttCalendar.vue'; import OrderDialog from './OrderDialog.vue'; import AddQueueDialog from './AddQueueDialog.vue'; import MakequeueBtn from './MakequeueBtn.vue'; import './ganttChart.css'; export default { name: 'GanttChart', components: { MakequeueBtn, GanttCalendar, OrderDialog, AddQueueDialog, }, data() { return { showAddDialog: false, // ค่าจาก Calendar selectedDate: '1/1/2025', // กำหนดช่วงเวลาใน Gantt startTime: '08:00', endTime: '17:00', // รายการเครื่องจักร machines: [ { machineID: 'MC1' , name: 'เครื่องเป่าขวด' }, { machineID: 'MC2' , name: 'เครื่องเป่าขวด2' }, { machineID: 'MC3' , name: 'เครื่องสวมฉลาก' }, { machineID: 'MC4' , name: 'เครื่องบรรจุ+แพ็ค' }, ], // รายการ Queue เริ่มต้น (สามารถเปลี่ยนแปลงได้) Queue: [ { queueID: 1, machineID: 'MC1', orderID: 5, pageNumber: 1, startTime: '1/1/2025 09:00', finishTime: '1/1/2025 11:00', status: 'Process', bottleSize: '600ml', producedQuantity: 400, }, { queueID: 2, machineID: 'MC2', orderID: 1, pageNumber: 1, startTime: '1/1/2025 13:00', finishTime: '1/1/2025 15:00', status: 'Waiting', bottleSize: '500ml', producedQuantity: 200, }, ], // Drag/Drop และ Resize draggingQueue: null, dragOffset: 0, resizingQueue: null, resizeDirection: null, // Ghost Queue (ขณะลาก) ghostQueue: null, ghostStyle: { 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, }, // Pagination pages: [1, 2], currentPage: 1, pageToShowDelete: null, // Dialog selectedQueueItem: null, }; }, computed: { // สร้าง list ของชั่วโมงทั้งหมดจาก startTime ถึง endTime hours() { const startHour = parseInt(this.startTime.split(':')[0]); const endHour = parseInt(this.endTime.split(':')[0]); return Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i); }, }, methods: { // รับข้อมูล Queue ที่ส่งมาจาก MakequeueBtn handleUpdateQueue(newQueue) { console.log("Received new queue:", newQueue); // ในที่นี้เราสามารถแทนที่ Queue เดิมหรือ merge กับข้อมูลที่มีอยู่ // ตัวอย่างนี้ใช้แทนที่ Queue ทั้งหมด this.Queue = newQueue; }, // -------------------- Dialog & Edit -------------------- openQueueDialog(item) { this.selectedQueueItem = { ...item }; }, closeQueueDialog() { this.selectedQueueItem = null; }, openAddQueueDialog() { this.selectedQueueItem = null; this.showAddDialog = true; }, deleteQueueItem(item) { this.Queue = this.Queue.filter(q => q.queueID !== item.queueID); this.closeQueueDialog(); }, openAddQueueDialogForEdit(queueItem) { this.selectedQueueItem = { ...queueItem }; this.showAddDialog = true; }, closeAddQueueDialog() { this.showAddDialog = false; this.selectedQueueItem = null; }, handleAddQueue(newQueueItem) { this.Queue.push({ queueID: this.Queue.length + 1, ...newQueueItem, }); this.showAddDialog = false; }, handleEditQueue(updatedQueueItem) { const index = this.Queue.findIndex(q => q.queueID === updatedQueueItem.queueID); if (index !== -1) { this.Queue.splice(index, 1, { ...updatedQueueItem }); } this.selectedQueueItem = null; this.showAddDialog = false; }, // -------------------- Utility Functions -------------------- formatHour(hour) { return (hour < 10 ? '0' + hour : hour) + ':00'; }, getDateString(dateTimeStr) { return dateTimeStr.split(' ')[0]; }, getTimeString(dateTimeStr) { return dateTimeStr.split(' ')[1]; }, filteredQueue(machineID) { return this.Queue.filter((q) => { const queueDate = this.getDateString(q.startTime); return ( q.machineID === machineID && q.pageNumber === this.currentPage && queueDate === this.selectedDate ); }); }, getQueueColor(orderID) { const colors = ['#F58181', '#FDDC5C', '#C5A1BC', '#49E060']; return colors[(orderID - 1) % colors.length]; }, getQueueStyle(item) { const start = this.getTimeString(item.startTime); const end = this.getTimeString(item.finishTime); const startDecimal = this.timeToDecimal(start); const endDecimal = this.timeToDecimal(end); const timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); const ratioStart = (startDecimal - timelineStart) / (timelineEnd - timelineStart); const ratioEnd = (endDecimal - timelineStart) / (timelineEnd - timelineStart); return { left: ratioStart * 100 + '%', width: (ratioEnd - ratioStart) * 100 + '%', backgroundColor: this.getQueueColor(item.orderID), color: '#fff', borderRadius: '10px', textAlign: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '5px', fontSize: '14px', }; }, getLineStyle(hour) { const timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); const ratio = (hour - timelineStart) / (timelineEnd - timelineStart); return { left: ratio * 100 + '%' }; }, timeToDecimal(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return hours + minutes / 60; }, decimalToTime(decimal) { const hours = Math.floor(decimal); const minutes = Math.round((decimal - hours) * 60); return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); }, // -------------------- Drag/Drop & Resize -------------------- onDragStart(event, item) { this.draggingQueue = item; const rect = event.target.getBoundingClientRect(); this.dragOffset = rect.width / 2; const emptyImg = document.createElement('img'); emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; event.dataTransfer.setDragImage(emptyImg, 0, 0); this.ghostQueue = { ...item }; this.ghostStyle.display = 'block'; this.ghostStyle.backgroundColor = this.getQueueColor(item.orderID); document.addEventListener('dragover', this.onDragOverGlobal); document.addEventListener('dragend', this.onDragEndGlobal); }, onDragOver(event) { event.preventDefault(); }, onDragOverGlobal(event) { event.preventDefault(); const rowTimeline = document.querySelector('.row-timeline'); 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 timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); const startDecimal = this.timeToDecimal(this.getTimeString(this.draggingQueue.startTime)); const endDecimal = this.timeToDecimal(this.getTimeString(this.draggingQueue.finishTime)); const duration = endDecimal - startDecimal; let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart); newStartDecimal = Math.round(newStartDecimal * 2) / 2; let newEndDecimal = newStartDecimal + duration; if (newEndDecimal > timelineEnd) { newEndDecimal = timelineEnd; newStartDecimal = newEndDecimal - duration; } const datePart = this.getDateString(this.draggingQueue.startTime); const newStartStr = datePart + ' ' + this.decimalToTime(newStartDecimal); const newEndStr = datePart + ' ' + this.decimalToTime(newEndDecimal); this.ghostQueue.startTime = newStartStr; this.ghostQueue.finishTime = newEndStr; const snappedRatioStart = (newStartDecimal - timelineStart) / (timelineEnd - timelineStart); const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth; const snappedRatioEnd = (newEndDecimal - timelineStart) / (timelineEnd - timelineStart); const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth; const rows = document.querySelectorAll('.row'); let closestRow = null; let minDistance = Infinity; rows.forEach((row) => { const rect = row.getBoundingClientRect(); const distance = Math.abs(event.clientY - rect.top); if (distance < minDistance) { minDistance = distance; closestRow = row; } }); if (closestRow) { this.ghostQueue.machineID = closestRow.querySelector('.machine-label').textContent.trim(); this.ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; } this.ghostStyle.left = snappedLeft + 'px'; this.ghostStyle.width = snappedWidth + 'px'; }, onDragEndGlobal() { this.ghostQueue = null; this.ghostStyle.display = 'none'; this.draggingQueue = null; document.removeEventListener('dragend', this.onDragEndGlobal); }, onDragEnd(event, item) { if (!this.ghostQueue) return; item.startTime = this.ghostQueue.startTime; item.finishTime = this.ghostQueue.finishTime; item.machineID = this.ghostQueue.machineID; document.removeEventListener('dragover', this.onDragOverGlobal); this.ghostQueue = null; this.ghostStyle.display = 'none'; this.draggingQueue = null; }, onDrop(event, newMachine) { event.preventDefault(); if (this.draggingQueue && this.ghostQueue) { this.draggingQueue.startTime = this.ghostQueue.startTime; this.draggingQueue.finishTime = this.ghostQueue.finishTime; this.draggingQueue.machineID = newMachine; } this.ghostQueue = null; this.ghostStyle.display = 'none'; this.draggingQueue = null; }, onResizeStart(event, item, direction) { event.preventDefault(); this.resizingQueue = item; this.resizeDirection = direction; document.addEventListener('mousemove', this.onResizing); document.addEventListener('mouseup', this.onResizeEnd); }, onResizing(event) { if (!this.resizingQueue) return; const rowTimeline = document.querySelector('.row-timeline'); 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 timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); const startDec = this.timeToDecimal(this.getTimeString(this.resizingQueue.startTime)); const endDec = this.timeToDecimal(this.getTimeString(this.resizingQueue.finishTime)); let newTimeDecimal = timelineStart + ratio * (timelineEnd - timelineStart); newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; const datePart = this.getDateString(this.resizingQueue.startTime); if (this.resizeDirection === 'left') { if (newTimeDecimal >= endDec) return; const newStartStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); this.resizingQueue.startTime = newStartStr; } else if (this.resizeDirection === 'right') { if (newTimeDecimal <= startDec) return; const newEndStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); this.resizingQueue.finishTime = newEndStr; } }, onResizeEnd() { this.resizingQueue = null; this.resizeDirection = null; document.removeEventListener('mousemove', this.onResizing); document.removeEventListener('mouseup', this.onResizeEnd); }, addPage() { if (this.pages.length < 10) { const newPage = this.pages.length + 1; this.pages.push(newPage); } else { alert("Maximum of 10 pages allowed."); } }, onPageRightClick(page, event) { event.preventDefault(); this.pageToShowDelete = page; }, deletePage(page) { const index = this.pages.indexOf(page); if (index !== -1) { this.pages.splice(index, 1); if (this.currentPage === page) { this.currentPage = this.pages.length > 0 ? this.pages[0] : 1; } } this.pageToShowDelete = null; }, onDocumentClick(event) { if (!event.target.closest('.page-btn')) { this.pageToShowDelete = null; } }, }, mounted() { document.addEventListener('click', this.onDocumentClick); }, beforeUnmount() { document.removeEventListener('click', this.onDocumentClick); }, }; </script>