From 06c1cbbabf6d001a759cf18640b4fd03a06ff6a3 Mon Sep 17 00:00:00 2001
From: Kritkhanin Anantakul <65160144@go.buu.ac.th>
Date: Fri, 28 Feb 2025 19:44:16 +0700
Subject: [PATCH] update gantt

---
 src/components/GanttChart.vue | 259 +++++++++++++++++++++++++---------
 1 file changed, 191 insertions(+), 68 deletions(-)

diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue
index 84b894e..22de627 100644
--- a/src/components/GanttChart.vue
+++ b/src/components/GanttChart.vue
@@ -12,63 +12,46 @@
 
     <!-- Rows: เครื่องจักรแต่ละตัว -->
     <div class="rows">
-      <div 
-        v-for="machine in machines" 
-        :key="machine.id" 
-        class="row"
-        @dragover.prevent
-        @drop="onDrop($event, machine.name)"
-      >
+      <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>
+          <div v-for="hour in hours" :key="'line-' + hour" class="vertical-line" :style="getLineStyle(hour)"></div>
 
-          <!-- แสดง Order เฉพาะหน้าปัจจุบันและเครื่องจักรตรงกัน -->
+          <!-- แสดง 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"
-            :style="getOrderStyle(order)"
-          >
+            :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>
+            <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)"
-            >
+            <div class="order-content" draggable="true" @dragstart="onDragStart($event, order)"
+              @dragend="onDragEnd($event, order)">
               {{ order.name }} ({{ order.start }} - {{ order.end }})
             </div>
 
             <!-- Handle สำหรับ Resize ด้านขวา -->
-            <div class="resize-handle right" 
-                 @mousedown="onResizeStart($event, order, 'right')">
-            </div>
+            <div class="resize-handle right" @mousedown="onResizeStart($event, order, 'right')"></div>
           </div>
         </div>
       </div>
     </div>
-    <v-divider :thickness="7" ></v-divider>
+
+    <!-- Ghost Order ขณะลาก -->
+    <div v-if="ghostOrder" class="drag-ghost" :style="ghostStyle">
+      {{ ghostOrder.name }} ({{ ghostOrder.start }} - {{ ghostOrder.end }})
+    </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"
-      >
+      <button v-for="p in pages" :key="p" :class="['page-btn', { active: p === currentPage }]" @click="currentPage = p">
         {{ p }}
       </button>
       <!-- ปุ่ม + สำหรับเพิ่มหน้าใหม่ -->
@@ -95,7 +78,7 @@ export default {
         { id: 5, name: 'MC5' },
       ],
 
-      // รายการ Order (ตัวอย่าง Order 1 มีสี blue)
+      // รายการ Order
       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' },
@@ -109,8 +92,26 @@ export default {
       resizingOrder: null,
       resizeDirection: null,
 
+      // Ghost Order (ตัวอย่างที่ขยับตามเม้าส์)
+      ghostOrder: null, 
+      ghostStyle: {
+        position: 'fixed',
+        top: '0px',
+        left: '0px',
+        display: 'none',
+        width: 'auto',
+        height: '40px',
+        lineHeight: '40px',
+        padding: '0 10px',
+        borderRadius: '20px',
+        pointerEvents: 'none',
+        backgroundColor: '#4caf50',
+        color: '#fff',
+        zIndex: 9999,
+      },
+
       // จัดการ Pagination
-      pages: [1, 2], // เริ่มต้นสองหน้า
+      pages: [1, 2],
       currentPage: 1,
     }
   },
@@ -144,7 +145,7 @@ export default {
       return {
         left,
         width,
-        backgroundColor: order.color || '#4caf50' // ใช้สีใน order หรือสี default
+        backgroundColor: order.color || '#4caf50'
       };
     },
 
@@ -170,55 +171,142 @@ export default {
       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)
     onDragStart(event, order) {
       this.draggingOrder = order;
       const rect = event.target.getBoundingClientRect();
-      this.dragOffset = event.clientX - rect.left;
-      event.dataTransfer.effectAllowed = "move";
+      this.dragOffset = event.target.getBoundingClientRect().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 }; // clone ข้อมูลของ Order
+      this.ghostStyle.display = 'block';
+      this.ghostStyle.backgroundColor = order.color || '#4caf50';
+
+      // ฟัง event dragover ทั้งหน้า (หรือจะใช้ใน .rows ก็ได้)
+      document.addEventListener('dragover', this.onDragOverGlobal);
+      document.addEventListener('dragend', this.onDragEndGlobal);
     },
 
-    // จบการลาก Order (DragEnd) => คำนวณตำแหน่งใหม่
-    onDragEnd(event, order) {
-      const rowTimeline = event.target.closest('.row-timeline');
-      if (!rowTimeline) return;
+    // ขณะลาก (DragOver) ที่ผูกใน template (กัน default เพื่อให้ drop ได้)
+    onDragOver(event) {
+      // ไม่ได้ทำอะไรมากในที่นี้ เพราะเราจัดการใน onDragOverGlobal
+      event.preventDefault();
+    },
 
+    // onDragOverGlobal - อัปเดตตำแหน่ง Ghost และเวลา
+    onDragOverGlobal(event) {
+      event.preventDefault();
+      const rowTimeline = document.querySelector('.row-timeline');
+      if (!rowTimeline) return;
       const timelineRect = rowTimeline.getBoundingClientRect();
-      const dropX = event.clientX - timelineRect.left - this.dragOffset;
       const timelineWidth = timelineRect.width;
+      const offsetX = event.clientX - timelineRect.left;
 
-      let ratioStart = dropX / timelineWidth;
-      ratioStart = Math.min(Math.max(ratioStart, 0), 1);
-
+      // 1️⃣ คำนวณ Snap ตามแนวนอน (เวลา)
+      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 + ratioStart * (timelineEnd - timelineStart);
+      let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart);
 
-      // ปัดค่าเป็นช่วงครึ่งชั่วโมง (0.5)
+      // ปัดเป็นช่วงครึ่งชั่วโมง (30 นาที)
       newStartDecimal = Math.round(newStartDecimal * 2) / 2;
 
-      const duration = this.timeToDecimal(order.end) - this.timeToDecimal(order.start);
+      // คำนวณช่วงเวลาของ Order
+      const duration = this.timeToDecimal(this.draggingOrder.end) - this.timeToDecimal(this.draggingOrder.start);
       let newEndDecimal = newStartDecimal + duration;
-
-      // ถ้าเกิน endTime ก็ปรับให้พอดี
       if (newEndDecimal > timelineEnd) {
         newEndDecimal = timelineEnd;
         newStartDecimal = newEndDecimal - duration;
       }
 
-      order.start = this.decimalToTime(newStartDecimal);
-      order.end = this.decimalToTime(newEndDecimal);
-      this.draggingOrder = null;
+      // อัปเดตเวลาใน ghostOrder
+      this.ghostOrder.start = this.decimalToTime(newStartDecimal);
+      this.ghostOrder.end = this.decimalToTime(newEndDecimal);
+
+      // คำนวณตำแหน่ง snapped left ตาม grid
+      const snappedRatioStart = (newStartDecimal - timelineStart) / (timelineEnd - timelineStart);
+      const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth;
+
+      // คำนวณความกว้างของ Ghost Order
+      const snappedRatioEnd = (newEndDecimal - timelineStart) / (timelineEnd - timelineStart);
+      const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth;
+
+      // 2️⃣ คำนวณ Snap ตามแนวตั้ง (เครื่องจักร)
+      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.ghostOrder.machine = closestRow.querySelector('.machine-label').textContent.trim(); // อัปเดตเครื่องจักรที่ snap
+        this.ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; // Snap ไปที่ตำแหน่งของ row นั้น
+      }
+      
+      // 3️⃣ อัปเดตตำแหน่งและขนาดของ Ghost Order
+      this.ghostStyle.left = snappedLeft + 'px';
+      this.ghostStyle.width = snappedWidth + 'px';
     },
 
+
+    // จบการลาก Order (DragEnd) => คำนวณตำแหน่งใหม่
+    onDragEnd(event, order) {
+  if (!this.ghostOrder) return;
+
+  // ใช้ค่าที่ snap แล้วจาก Ghost Order
+  order.start = this.ghostOrder.start;
+  order.end = this.ghostOrder.end;
+  order.machine = this.ghostOrder.machine;
+
+  // ลบ Ghost Order
+  document.removeEventListener('dragover', this.onDragOverGlobal);
+  this.ghostOrder = null;
+  this.ghostStyle.display = 'none';
+  this.draggingOrder = null;
+}
+,
+
     // เมื่อปล่อย Drag บนเครื่องจักรใหม่ => เปลี่ยน machine ของ Order
     onDrop(event, newMachine) {
-      if (this.draggingOrder) {
-        this.draggingOrder.machine = newMachine;
-        this.draggingOrder = null;
-      }
-    },
+  event.preventDefault();
+  
+  if (this.draggingOrder) {
+    // ใช้ค่าที่ snap แล้วจาก Ghost Order
+    this.draggingOrder.start = this.ghostOrder.start;
+    this.draggingOrder.end = this.ghostOrder.end;
+    this.draggingOrder.machine = newMachine; // ใช้ค่าของเครื่องจักรที่ปล่อยลงไป
+  }
+
+  // ลบ Ghost Order
+  this.ghostOrder = null;
+  this.ghostStyle.display = 'none';
+  this.draggingOrder = null;
+}
+,
 
     // เริ่ม Resize (mousedown ที่ handle)
     onResizeStart(event, order, direction) {
@@ -277,6 +365,7 @@ export default {
 }
 </script>
 
+
 <style scoped>
 .gantt-chart {
   width: 100%;
@@ -284,10 +373,12 @@ export default {
   display: flex;
   flex-direction: column;
 }
+
 .header {
   display: flex;
   background: #fff;
 }
+
 .machine-label {
   width: 80px;
   text-align: center;
@@ -296,10 +387,12 @@ export default {
   line-height: 40px;
   border-bottom: 1px solid #ffffff;
 }
+
 .time-scale {
   display: flex;
   flex: 1;
 }
+
 .time-cell {
   flex: 1;
   text-align: center;
@@ -308,20 +401,25 @@ export default {
   line-height: 40px;
   font-weight: bold;
 }
+
 .rows {
   flex: 1;
   overflow-y: auto;
 }
+
 .row {
   display: flex;
   min-height: 60px;
-  border-bottom: 1px solid #ddd; /* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */
+  border-bottom: 1px solid #ddd;
+  /* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */
 }
+
 .row-timeline {
   position: relative;
   flex: 1;
   overflow: hidden;
 }
+
 .vertical-line {
   position: absolute;
   top: 0;
@@ -329,6 +427,7 @@ export default {
   width: 1px;
   background-color: #ddd;
 }
+
 .order {
   position: absolute;
   top: 10px;
@@ -340,6 +439,7 @@ export default {
   align-items: center;
   z-index: 1;
 }
+
 .order-content {
   flex: 1;
   text-align: center;
@@ -347,31 +447,52 @@ export default {
   cursor: grab;
   padding: 0 10px;
 }
+
 .resize-handle {
   width: 5px;
   height: 100%;
-  background-color: #fff;
   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 ตัวจริงให้จางลง */
+.order.faded {
+  opacity: 0.3;
+}
+
+/* Ghost Order */
+.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%; /* ทำให้ `.pagination` ขยายเต็มพื้นที่ */
+  justify-content: flex-start;
+  /* จัดเรียงชิดซ้าย */
+  align-items: center;
+  /* จัดกึ่งกลางแนวตั้ง */
+  gap: 5px;
+  /* เว้นระยะระหว่างปุ่ม */
+  width: 100%;
+  /* ทำให้ `.pagination` ขยายเต็มพื้นที่ */
   margin-top: 10px;
 }
+
 .page-btn {
   background: #ffffff;
   border: 1px solid #ffffff;
@@ -379,10 +500,12 @@ export default {
   cursor: pointer;
   font-size: 14px;
 }
+
 .page-btn.active {
   background: #007bff;
   color: #ffffff;
 }
+
 .page-btn.add-page {
   font-weight: bold;
   background: #ffffff;
-- 
GitLab