From b531e3a9068d721bc6e4a3020d240ed4bbfd4b9e Mon Sep 17 00:00:00 2001 From: Kritkhanin Anantakul <65160144@go.buu.ac.th> Date: Mon, 3 Mar 2025 21:46:37 +0700 Subject: [PATCH] order dialog edit delete btn / split gant file and carlendar --- src/components/GanttChart/GanttCalendar.vue | 55 ++ src/components/GanttChart/GanttChart.vue | 604 ++++++++++---------- src/components/GanttChart/OrderDialog.vue | 35 ++ src/components/GanttChart/ganttChart.css | 175 ++++++ src/views/ProductQueueView.vue | 26 +- 5 files changed, 562 insertions(+), 333 deletions(-) create mode 100644 src/components/GanttChart/GanttCalendar.vue create mode 100644 src/components/GanttChart/OrderDialog.vue create mode 100644 src/components/GanttChart/ganttChart.css diff --git a/src/components/GanttChart/GanttCalendar.vue b/src/components/GanttChart/GanttCalendar.vue new file mode 100644 index 0000000..376830a --- /dev/null +++ b/src/components/GanttChart/GanttCalendar.vue @@ -0,0 +1,55 @@ +<script setup> +import { ref, watch } from 'vue'; + +const props = defineProps({ + modelValue: { + type: String, + default: '1/1/2024', + }, +}); +const emits = defineEmits(['update:modelValue']); + +const selectedDate = ref(props.modelValue); + +// เมื่อ selectedDate เปลี่ยน ให้ emit ออกไปเป็น v-model +watch(selectedDate, (newVal) => { + emits('update:modelValue', newVal); +}); + +// (ตัวอย่าง) คุณอาจทำปุ่ม Back/Next ให้เปลี่ยนวันที่ได้จริง +// แต่ตอนนี้เป็นเพียง placeholder +</script> + +<template> + <v-col + cols="auto" + class="d-inline-flex align-center" + style="background-color: #7E9CD3; border-radius: 15px; max-width: fit-content; padding: 8px 16px;" +> + <v-btn + class="mr-2" + style="background-color: #2B2E3F; color: white; border-radius: 10px;" + @click="selectedDate.value = '1/1/2024'" + > + < Back + </v-btn> + <v-btn + style="background-color: white; color: black; min-width: 200px; border-radius: 15px;" + > + {{ selectedDate }} + </v-btn> + <v-btn + class="ml-2" + style="background-color: #2B2E3F; color: white; border-radius: 10px;" + @click="selectedDate.value = '1/2/2024'" + > + Next > + </v-btn> + <v-btn + class="ml-2" + style="background-color: white; color: black; border-radius: 10px;" + > + <v-icon>mdi-dots-horizontal</v-icon> + </v-btn> + </v-col> +</template> diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index 054f5d9..de84e2a 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -1,107 +1,216 @@ <template> - <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 class="gantt-container"> + <!-- Calendar: เชื่อมกับ selectedDate --> + <GanttCalendar v-model:modelValue="selectedDate" /> + + <!-- 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> - </div> - - <!-- Rows: เครื่องจักรแต่ละตัว --> - <div class="rows"> - <div v-for="machine in machines" :key="machine.id" class="row" @dragover.prevent="onDragOver($event)" - @drop="onDrop($event, machine.name)"> - <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> - <!-- แสดง Order --> - <div - v-for="order in orders.filter(o => pages.includes(o.page) && o.page === currentPage && o.machine === machine.name)" - :key="order.id" class="order" :class="{ 'faded': draggingOrder && draggingOrder.id === order.id }" - :style="getOrderStyle(order)"> - <!-- Handle สำหรับ Resize ด้านซ้าย --> - <div class="resize-handle left" @mousedown="onResizeStart($event, order, 'left')"></div> - - <!-- ส่วนกลางของ Order ใช้สำหรับลาก --> - <div class="order-content" draggable="true" @dragstart="onDragStart($event, order)" - @dragend="onDragEnd($event, order)"> - {{ order.name }} ({{ order.start }} - {{ order.end }}) + <!-- 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.machineID }} + </div> + <div class="row-timeline"> + <!-- เส้นแนวตั้ง (Grid Lines) --> + <div + v-for="hour in hours" + :key="'line-' + hour" + class="vertical-line" + :style="getLineStyle(hour)" + ></div> + + <!-- แสดง Order ที่ตรงกับวันที่ (selectedDate), Page, และ Machine --> + <div + v-for="order in filteredOrders(machine.machineID)" + :key="order.queueID" + class="order" + :class="{ 'faded': draggingOrder && draggingOrder.queueID === order.queueID }" + :style="getOrderStyle(order)" + > + <!-- Handle สำหรับ Resize ด้านซ้าย --> + <div + class="resize-handle left" + @mousedown="onResizeStart($event, order, 'left')" + ></div> + + <!-- ส่วนกลางของ Order ใช้สำหรับลาก และเปิด Dialog เมื่อคลิก --> + <div + class="order-content" + draggable="true" + @dragstart="onDragStart($event, order)" + @dragend="onDragEnd($event, order)" + @click.stop="openOrderDialog(order)" + > + {{ order.orderID }} ({{ getTimeString(order.startTime) }} - {{ getTimeString(order.finishTime) }}) + </div> + + <!-- Handle สำหรับ Resize ด้านขวา --> + <div + class="resize-handle right" + @mousedown="onResizeStart($event, order, 'right')" + ></div> </div> - - <!-- Handle สำหรับ Resize ด้านขวา --> - <div class="resize-handle right" @mousedown="onResizeStart($event, order, 'right')"></div> </div> </div> </div> - </div> - <!-- Ghost Order ขณะลาก --> - <div v-if="ghostOrder" class="drag-ghost" :style="ghostStyle"> - {{ ghostOrder.name }} ({{ ghostOrder.start }} - {{ ghostOrder.end }}) - </div> + <!-- Ghost Order ขณะลาก --> + <div v-if="ghostOrder" class="drag-ghost" :style="ghostStyle"> + {{ ghostOrder.orderID }} ({{ getTimeString(ghostOrder.startTime) }} - {{ getTimeString(ghostOrder.finishTime) }}) + </div> + + <v-divider :thickness="7"></v-divider> + + <!-- Pagination --> + <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">+</button> + </div> - <v-divider :thickness="7"></v-divider> - - <!-- Pagination --> - <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 }} - <!-- ปุ่ม Delete จะแสดงเฉพาะเมื่อคลิกขวาที่ปุ่มนั้น --> - <button v-if="pageToShowDelete === p" class="delete-btn" @click.stop="deletePage(p)">Delete</button> - </button> - <!-- ปุ่ม + สำหรับเพิ่มหน้าใหม่ --> - <button class="page-btn add-page" @click="addPage">+</button> + <!-- Order Dialog --> + <OrderDialog + v-if="selectedOrder" + :order="selectedOrder" + @close="closeOrderDialog" + @edit="editOrder" + @delete="deleteOrder" + /> </div> </div> </template> <script> +import GanttCalendar from './GanttCalendar.vue'; +import OrderDialog from './OrderDialog.vue'; +import './ganttChart.css'; + export default { name: 'GanttChart', + components: { + GanttCalendar, + OrderDialog, + }, data() { return { - // เวลางาน + // ค่าจาก Calendar + selectedDate: '1/1/2024', + + // กำหนดช่วงเวลาใน Gantt startTime: '08:00', endTime: '17:00', // รายการเครื่องจักร machines: [ - { id: 1, name: 'MC1' }, - { id: 2, name: 'MC2' }, - { id: 3, name: 'MC3' }, - { id: 4, name: 'MC4' }, - { id: 5, name: 'MC5' }, + { id: 1, machineID: 'MC1' }, + { id: 2, machineID: 'MC2' }, + { id: 3, machineID: 'MC3' }, + { id: 4, machineID: 'MC4' }, + { id: 5, machineID: 'MC5' }, ], - // รายการ Order + // ข้อมูล Order ตามโครงสร้าง Queue orders: [ - { page: 1, id: 1, name: 'Order 1', start: '13:00', end: '15:00', machine: 'MC1', color: 'blue' }, - { page: 1, id: 2, name: 'Order 2', start: '09:00', end: '11:00', machine: 'MC2' }, - { page: 2, id: 3, name: 'Order 3', start: '10:00', end: '12:00', machine: 'MC1' }, - { page: 2, id: 4, name: 'Order 4', start: '14:00', end: '16:00', machine: 'MC5' }, + { + queueID: 1, + machineID: 'MC1', + orderID: 1, + pageNumber: 1, + startTime: '1/1/2024 09:00', + finishTime: '1/1/2024 11:00', + status: 'Process', + bottleSize: '600ml', + producedQuantity: 400, + }, + { + queueID: 2, + machineID: 'MC2', + orderID: 2, + pageNumber: 1, + startTime: '1/1/2024 13:00', + finishTime: '1/1/2024 15:00', + status: 'Waiting', + bottleSize: '500ml', + producedQuantity: 200, + }, + { + queueID: 3, + machineID: 'MC1', + orderID: 3, + pageNumber: 2, + startTime: '1/1/2024 10:00', + finishTime: '1/1/2024 12:00', + status: 'Process', + bottleSize: '700ml', + producedQuantity: 350, + }, + { + queueID: 4, + machineID: 'MC5', + orderID: 4, + pageNumber: 2, + startTime: '1/1/2024 14:00', + finishTime: '1/1/2024 16:00', + status: 'Process', + bottleSize: '600ml', + producedQuantity: 500, + }, + { + queueID: 5, + machineID: 'MC5', + orderID: 4, + pageNumber: 1, + startTime: '2/1/2024 14:00', + finishTime: '2/1/2024 16:00', + status: 'Process', + bottleSize: '600ml', + producedQuantity: 500, + }, ], - // สำหรับการลากและการ Resize + // จัดการการลาก, Resize draggingOrder: null, dragOffset: 0, resizingOrder: null, resizeDirection: null, - // Ghost Order (ตัวอย่างที่ขยับตามเม้าส์) - ghostOrder: null, + // Ghost Order (ขณะลาก) + ghostOrder: null, ghostStyle: { position: 'fixed', top: '0px', @@ -118,13 +227,14 @@ export default { zIndex: 9999, }, - // จัดการ Pagination + // Pagination pages: [1, 2], currentPage: 1, - - // สำหรับการแสดงปุ่ม delete เมื่อคลิกขวาที่ page pageToShowDelete: null, - } + + // Dialog + selectedOrder: null, + }; }, computed: { // สร้าง list ของชั่วโมงทั้งหมดจาก startTime ถึง endTime @@ -132,92 +242,99 @@ export default { 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: { + // แยก date ส่วนหน้า (1/1/2024) ออกจาก startTime/finishTime + getDateString(dateTimeStr) { + // สมมติ format = '1/1/2024 09:00' + return dateTimeStr.split(' ')[0]; + }, + // แยกเฉพาะส่วนเวลา (09:00) + getTimeString(dateTimeStr) { + return dateTimeStr.split(' ')[1]; + }, + + // ฟิลเตอร์ order ตาม machineID, pageNumber, และ selectedDate + filteredOrders(machineID) { + return this.orders.filter((o) => { + const orderDate = this.getDateString(o.startTime); + return ( + o.machineID === machineID && + o.pageNumber === this.currentPage && + orderDate === this.selectedDate + ); + }); + }, + // แปลงเลขชั่วโมงเป็น "HH:00" formatHour(hour) { return (hour < 10 ? '0' + hour : hour) + ':00'; }, - // สไตล์การวาง Order (ตำแหน่ง left, width, และ backgroundColor) + // สไตล์ตำแหน่งของ Order getOrderStyle(order) { - const startDecimal = this.timeToDecimal(order.start); - const endDecimal = this.timeToDecimal(order.end); + // แยกเวลาเฉพาะส่วน HH:mm + const start = this.getTimeString(order.startTime); // ex "09:00" + const end = this.getTimeString(order.finishTime); // ex "11:00" + + 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); - const left = ratioStart * 100 + '%'; - const width = (ratioEnd - ratioStart) * 100 + '%'; - return { - left, - width, - backgroundColor: order.color || '#4caf50' + left: ratioStart * 100 + '%', + width: (ratioEnd - ratioStart) * 100 + '%', + backgroundColor: '#4caf50', // สามารถปรับตาม status หรืออื่นๆ ได้ }; }, - // สไตล์ของเส้นแนวตั้ง (grid lines) ตามชั่วโมง + // สไตล์ของเส้นแนวตั้ง (grid lines) getLineStyle(hour) { - const startDecimal = this.timeToDecimal(this.startTime); - const endDecimal = this.timeToDecimal(this.endTime); - const ratio = (hour - startDecimal) / (endDecimal - startDecimal); + const timelineStart = this.timeToDecimal(this.startTime); + const timelineEnd = this.timeToDecimal(this.endTime); + const ratio = (hour - timelineStart) / (timelineEnd - timelineStart); return { - left: ratio * 100 + '%' + left: ratio * 100 + '%', }; }, - // แปลงเวลา "HH:MM" -> เลขทศนิยม (เช่น 9:30 => 9.5) + // แปลงเวลา "HH:MM" -> เลขทศนิยม timeToDecimal(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return hours + minutes / 60; }, - // แปลงเลขทศนิยม -> "HH:MM" 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); }, - onDragEndGlobal() { - // ลบ Ghost Order ออกจาก UI - this.ghostOrder = null; - this.ghostStyle.display = 'none'; - this.draggingOrder = null; - // เอา event listener ออก (ไม่งั้นมันจะถูกเรียกทุกครั้งที่ drag) - document.removeEventListener('dragend', this.onDragEndGlobal); - }, - - // เริ่มลาก Order (DragStart) + // Event ลาก (DragStart) onDragStart(event, order) { this.draggingOrder = order; const rect = event.target.getBoundingClientRect(); - this.dragOffset = event.target.getBoundingClientRect().width / 2; - + this.dragOffset = rect.width / 2; // ปิด preview เริ่มต้นของ HTML5 Drag & Drop const emptyImg = document.createElement('img'); emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; event.dataTransfer.setDragImage(emptyImg, 0, 0); - // สร้าง Ghost Order this.ghostOrder = { ...order }; this.ghostStyle.display = 'block'; - this.ghostStyle.backgroundColor = order.color || '#4caf50'; + this.ghostStyle.backgroundColor = '#4caf50'; document.addEventListener('dragover', this.onDragOverGlobal); document.addEventListener('dragend', this.onDragEndGlobal); }, - - // ขณะลาก (DragOver) ที่ผูกใน template onDragOver(event) { event.preventDefault(); }, - - // onDragOverGlobal - อัปเดตตำแหน่ง Ghost และเวลา onDragOverGlobal(event) { event.preventDefault(); const rowTimeline = document.querySelector('.row-timeline'); @@ -228,34 +345,43 @@ export default { let ratio = offsetX / timelineWidth; ratio = Math.min(Math.max(ratio, 0), 1); - + const timelineStart = this.timeToDecimal(this.startTime); const timelineEnd = this.timeToDecimal(this.endTime); - let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart); + // คำนวณเวลาเริ่มใหม่ (เฉพาะส่วนเวลา) + const startDecimal = this.timeToDecimal(this.getTimeString(this.draggingOrder.startTime)); + const endDecimal = this.timeToDecimal(this.getTimeString(this.draggingOrder.finishTime)); + const duration = endDecimal - startDecimal; + + let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart); newStartDecimal = Math.round(newStartDecimal * 2) / 2; - const duration = this.timeToDecimal(this.draggingOrder.end) - this.timeToDecimal(this.draggingOrder.start); let newEndDecimal = newStartDecimal + duration; if (newEndDecimal > timelineEnd) { newEndDecimal = timelineEnd; newStartDecimal = newEndDecimal - duration; } - this.ghostOrder.start = this.decimalToTime(newStartDecimal); - this.ghostOrder.end = this.decimalToTime(newEndDecimal); + // สร้าง String ใหม่ "วัน/เดือน/ปี เวลา" + const datePart = this.getDateString(this.draggingOrder.startTime); // ex "1/1/2024" + const newStartStr = datePart + ' ' + this.decimalToTime(newStartDecimal); + const newEndStr = datePart + ' ' + this.decimalToTime(newEndDecimal); + + this.ghostOrder.startTime = newStartStr; + this.ghostOrder.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; + // หาแถว (machine) ที่ใกล้ที่สุด const rows = document.querySelectorAll('.row'); let closestRow = null; let minDistance = Infinity; - rows.forEach(row => { + rows.forEach((row) => { const rect = row.getBoundingClientRect(); const distance = Math.abs(event.clientY - rect.top); if (distance < minDistance) { @@ -265,101 +391,97 @@ export default { }); if (closestRow) { - this.ghostOrder.machine = closestRow.querySelector('.machine-label').textContent.trim(); + this.ghostOrder.machineID = closestRow.querySelector('.machine-label').textContent.trim(); this.ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; } - + this.ghostStyle.left = snappedLeft + 'px'; this.ghostStyle.width = snappedWidth + 'px'; }, - - // จบการลาก Order (DragEnd) => คำนวณตำแหน่งใหม่ + onDragEndGlobal() { + this.ghostOrder = null; + this.ghostStyle.display = 'none'; + this.draggingOrder = null; + document.removeEventListener('dragend', this.onDragEndGlobal); + }, onDragEnd(event, order) { if (!this.ghostOrder) return; - - order.start = this.ghostOrder.start; - order.end = this.ghostOrder.end; - order.machine = this.ghostOrder.machine; - + order.startTime = this.ghostOrder.startTime; + order.finishTime = this.ghostOrder.finishTime; + order.machineID = this.ghostOrder.machineID; document.removeEventListener('dragover', this.onDragOverGlobal); this.ghostOrder = null; this.ghostStyle.display = 'none'; this.draggingOrder = null; }, - - // เมื่อปล่อย Drag บนเครื่องจักรใหม่ => เปลี่ยน machine ของ Order onDrop(event, newMachine) { event.preventDefault(); - - if (this.draggingOrder) { - this.draggingOrder.start = this.ghostOrder.start; - this.draggingOrder.end = this.ghostOrder.end; - this.draggingOrder.machine = newMachine; + if (this.draggingOrder && this.ghostOrder) { + this.draggingOrder.startTime = this.ghostOrder.startTime; + this.draggingOrder.finishTime = this.ghostOrder.finishTime; + this.draggingOrder.machineID = newMachine; } - this.ghostOrder = null; this.ghostStyle.display = 'none'; this.draggingOrder = null; }, - // เริ่ม Resize (mousedown ที่ handle) + // Resize onResizeStart(event, order, direction) { event.preventDefault(); this.resizingOrder = order; this.resizeDirection = direction; - document.addEventListener("mousemove", this.onResizing); - document.addEventListener("mouseup", this.onResizeEnd); + document.addEventListener('mousemove', this.onResizing); + document.addEventListener('mouseup', this.onResizeEnd); }, - - // ขณะ Resize onResizing(event) { if (!this.resizingOrder) 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); - let newTimeDecimal = timelineStart + ratio * (timelineEnd - timelineStart); + const startDec = this.timeToDecimal(this.getTimeString(this.resizingOrder.startTime)); + const endDec = this.timeToDecimal(this.getTimeString(this.resizingOrder.finishTime)); + + let newTimeDecimal = timelineStart + ratio * (timelineEnd - timelineStart); newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; + const datePart = this.getDateString(this.resizingOrder.startTime); // ex: "1/1/2024" + if (this.resizeDirection === 'left') { - if (newTimeDecimal >= this.timeToDecimal(this.resizingOrder.end)) return; - this.resizingOrder.start = this.decimalToTime(newTimeDecimal); + if (newTimeDecimal >= endDec) return; + const newStartStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); + this.resizingOrder.startTime = newStartStr; } else if (this.resizeDirection === 'right') { - if (newTimeDecimal <= this.timeToDecimal(this.resizingOrder.start)) return; - this.resizingOrder.end = this.decimalToTime(newTimeDecimal); + if (newTimeDecimal <= startDec) return; + const newEndStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); + this.resizingOrder.finishTime = newEndStr; } }, - - // จบการ Resize onResizeEnd() { this.resizingOrder = null; this.resizeDirection = null; - document.removeEventListener("mousemove", this.onResizing); - document.removeEventListener("mouseup", this.onResizeEnd); + document.removeEventListener('mousemove', this.onResizing); + document.removeEventListener('mouseup', this.onResizeEnd); }, - // เพิ่มหน้าใหม่ + // Pagination addPage() { const newPage = this.pages.length + 1; this.pages.push(newPage); }, - - // เมื่อคลิกขวาที่ปุ่ม page onPageRightClick(page, event) { event.preventDefault(); this.pageToShowDelete = page; }, - - // ลบหน้า deletePage(page) { const index = this.pages.indexOf(page); if (index !== -1) { @@ -370,170 +492,32 @@ export default { } this.pageToShowDelete = null; }, - - // ซ่อนปุ่ม delete เมื่อคลิกนอก page-btn onDocumentClick(event) { if (!event.target.closest('.page-btn')) { this.pageToShowDelete = null; } - } + }, + + // Dialog + openOrderDialog(order) { + this.selectedOrder = { ...order }; + }, + closeOrderDialog() { + this.selectedOrder = null; + }, + editOrder(order) { + alert('Edit functionality not implemented yet.\nOrder ID: ' + order.orderID); + }, + deleteOrder(order) { + this.orders = this.orders.filter((o) => o.queueID !== order.queueID); + this.closeOrderDialog(); + }, }, mounted() { document.addEventListener('click', this.onDocumentClick); }, beforeUnmount() { document.removeEventListener('click', this.onDocumentClick); - } -} + }, +}; </script> - -<style scoped> -.gantt-chart { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; -} - -.header { - display: flex; - background: #fff; -} - -.machine-label { - width: 80px; - text-align: center; - font-weight: bold; - background: #ffffff; - line-height: 40px; - border-bottom: 1px solid #ffffff; -} - -.time-scale { - display: flex; - flex: 1; -} - -.time-cell { - flex: 1; - text-align: center; - border-bottom: 1px solid #ddd; - font-size: 15px; - line-height: 40px; - font-weight: bold; -} - -.rows { - flex: 1; - overflow-y: auto; -} - -.row { - display: flex; - min-height: 60px; - border-bottom: 1px solid #ddd; -} - -.row-timeline { - position: relative; - flex: 1; - overflow: hidden; -} - -.vertical-line { - position: absolute; - top: 0; - bottom: 0; - width: 1px; - background-color: #ddd; -} - -.order { - position: absolute; - top: 10px; - height: 40px; - color: #fff; - border-radius: 20px; - user-select: none; - display: flex; - align-items: center; - z-index: 1; -} - -.order-content { - flex: 1; - text-align: center; - line-height: 40px; - cursor: grab; - padding: 0 10px; -} - -.resize-handle { - width: 5px; - height: 100%; - cursor: ew-resize; - position: absolute; -} - -.resize-handle.left { - left: 0; - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; -} - -.resize-handle.right { - right: 0; - border-top-right-radius: 10px; - border-bottom-right-radius: 10px; -} - -.order.faded { - opacity: 0.3; -} - -.drag-ghost { - box-sizing: border-box; - text-align: center; - pointer-events: none; -} - -.pagination { - display: flex; - justify-content: flex-start; - align-items: center; - gap: 5px; - width: 100%; - margin-top: 10px; -} - -.page-btn { - background: #ffffff; - border: 1px solid #ffffff; - padding: 5px 10px; - cursor: pointer; - font-size: 14px; - position: relative; -} - -.page-btn.active { - background: #007bff; - color: #ffffff; -} - -.page-btn.add-page { - font-weight: bold; - background: #ffffff; -} - -/* สไตล์สำหรับปุ่ม Delete ที่แสดงเมื่อคลิกขวา */ -.delete-btn { - margin-left: 5px; - background-color: red; - color: #fff; - border: none; - padding: 2px 5px; - cursor: pointer; - border-radius: 3px; - font-size: 12px; -} -</style> diff --git a/src/components/GanttChart/OrderDialog.vue b/src/components/GanttChart/OrderDialog.vue new file mode 100644 index 0000000..81adfb3 --- /dev/null +++ b/src/components/GanttChart/OrderDialog.vue @@ -0,0 +1,35 @@ +<template> + <div class="order-dialog-overlay"> + <div class="order-dialog"> + <h3>Order Details</h3> + <p><strong>QueueID:</strong> {{ order.queueID }}</p> + <p><strong>Machine:</strong> {{ order.machineID }}</p> + <p><strong>OrderID:</strong> {{ order.orderID }}</p> + <p><strong>PageNumber:</strong> {{ order.pageNumber }}</p> + <p><strong>Start:</strong> {{ order.startTime }}</p> + <p><strong>Finish:</strong> {{ order.finishTime }}</p> + <p><strong>Status:</strong> {{ order.status }}</p> + <p><strong>Bottle Size:</strong> {{ order.bottleSize }}</p> + <p><strong>Produced:</strong> {{ order.producedQuantity }}</p> + + <div class="dialog-buttons"> + <button @click="$emit('edit', order)">Edit</button> + <button @click="$emit('delete', order)">Delete</button> + <button @click="$emit('close')">Close</button> + </div> + </div> + </div> + </template> + + <script> + export default { + name: 'OrderDialog', + props: { + order: { + type: Object, + required: true, + }, + }, + }; + </script> + \ No newline at end of file diff --git a/src/components/GanttChart/ganttChart.css b/src/components/GanttChart/ganttChart.css new file mode 100644 index 0000000..b290d1e --- /dev/null +++ b/src/components/GanttChart/ganttChart.css @@ -0,0 +1,175 @@ +.gantt-chart { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + } + + .header { + display: flex; + background: #fff; + } + + .machine-label { + width: 80px; + text-align: center; + font-weight: bold; + background: #ffffff; + line-height: 40px; + border-bottom: 1px solid #ffffff; + } + + .time-scale { + display: flex; + flex: 1; + } + + .time-cell { + flex: 1; + text-align: center; + border-bottom: 1px solid #ddd; + font-size: 15px; + line-height: 40px; + font-weight: bold; + } + + .rows { + flex: 1; + overflow-y: auto; + } + + .row { + display: flex; + min-height: 60px; + border-bottom: 1px solid #ddd; + } + + .row-timeline { + position: relative; + flex: 1; + overflow: hidden; + } + + .vertical-line { + position: absolute; + top: 0; + bottom: 0; + width: 1px; + background-color: #ddd; + } + + .order { + position: absolute; + top: 10px; + height: 40px; + color: #fff; + border-radius: 20px; + user-select: none; + display: flex; + align-items: center; + z-index: 1; + } + + .order-content { + flex: 1; + text-align: center; + line-height: 40px; + cursor: grab; + padding: 0 10px; + } + + .resize-handle { + width: 5px; + height: 100%; + cursor: ew-resize; + position: absolute; + } + + .resize-handle.left { + left: 0; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + } + + .resize-handle.right { + right: 0; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + } + + .order.faded { + opacity: 0.3; + } + + .drag-ghost { + box-sizing: border-box; + text-align: center; + pointer-events: none; + } + + .pagination { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 5px; + width: 100%; + margin-top: 10px; + } + + .page-btn { + background: #ffffff; + border: 1px solid #ffffff; + padding: 5px 10px; + cursor: pointer; + font-size: 14px; + position: relative; + } + + .page-btn.active { + background: #007bff; + color: #ffffff; + } + + .page-btn.add-page { + font-weight: bold; + background: #ffffff; + } + + /* สไตล์สำหรับปุ่ม Delete ที่แสดงเมื่อคลิกขวา */ + .delete-btn { + margin-left: 5px; + background-color: red; + color: #fff; + border: none; + padding: 2px 5px; + cursor: pointer; + border-radius: 3px; + font-size: 12px; + } + + /* สไตล์สำหรับ Order Dialog */ + .order-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + } + + .order-dialog { + background: #fff; + padding: 20px; + border-radius: 8px; + width: 300px; + text-align: center; + } + + .dialog-buttons button { + margin: 5px; + } + \ No newline at end of file diff --git a/src/views/ProductQueueView.vue b/src/views/ProductQueueView.vue index 1d818e7..df11423 100644 --- a/src/views/ProductQueueView.vue +++ b/src/views/ProductQueueView.vue @@ -7,7 +7,8 @@ const selectedDate = ref('1/1/2024') const employees = Array.from({ length: 10 }, (_, i) => `EM${i + 1}`) const orders = ref([ { id: 1, name: 'ORDER1' }, - { id: 2, name: 'ORDER2' } + { id: 2, name: 'ORDER2' }, + { id: 3, name: 'ORDER3' } ]) // Drag and drop functionality @@ -43,28 +44,7 @@ const handleDrop = (targetOrder) => { <v-container class="pa-0"> <!-- Gantt chart --> <v-sheet class="pa-1 mb-3" - style="border-radius: 15px; max-width: 98%; margin-left: auto; margin-right: auto; min-height: 500px;"> - - <!-- Timeline --> - <v-sheet class="pa-3 mb-3"> - <v-col cols="auto" class="d-flex align-center"> - <v-col cols="auto" class="d-flex align-center" style="background-color: #7E9CD3; border-radius: 15px; "> - <v-btn class="mr-2" style="background-color: #2B2E3F; color: white; border-radius: 10px;">< Back</v-btn> - <v-btn style="background-color: white; color: black; min-width: 200px; border-radius: 15px;">{{ selectedDate - }}</v-btn> - <v-btn class="ml-2" style="background-color: #2B2E3F; color: white; border-radius: 10px;">Next ></v-btn> - <v-btn class="ml-2" - style="background-color: white; color: black; border-radius: 10px;"><v-icon>mdi-dots-horizontal</v-icon></v-btn> - </v-col> - <v-btn class="ml-auto" - style="background-color: #0077D8; font-weight: bold; color: white; border-radius: 12px; padding: 28px; display: flex; align-items: center; justify-content: center; font-size: 18px;"> - Make a queue - </v-btn> - - </v-col> - <v-row> - </v-row> - </v-sheet> + style="border-radius: 15px; max-width: 98%; margin-left: auto; margin-right: auto; min-height: 460px;"> <!-- Gantt Chart --> <GanttChart /> </v-sheet> -- GitLab