Gitlab@Informatics

Skip to content
Snippets Groups Projects
Commit 06c1cbba authored by Kritkhanin Anantakul's avatar Kritkhanin Anantakul
Browse files

update gantt

parent ad4e7fed
No related branches found
No related tags found
No related merge requests found
...@@ -12,63 +12,46 @@ ...@@ -12,63 +12,46 @@
<!-- Rows: เครื่องจักรแต่ละตัว --> <!-- Rows: เครื่องจักรแต่ละตัว -->
<div class="rows"> <div class="rows">
<div <div v-for="machine in machines" :key="machine.id" class="row" @dragover.prevent="onDragOver($event)"
v-for="machine in machines" @drop="onDrop($event, machine.name)">
:key="machine.id"
class="row"
@dragover.prevent
@drop="onDrop($event, machine.name)"
>
<div class="machine-label"> <div class="machine-label">
{{ machine.name }} {{ machine.name }}
</div> </div>
<div class="row-timeline"> <div class="row-timeline">
<!-- เส้นแนวตั้ง (Grid Lines) --> <!-- เส้นแนวตั้ง (Grid Lines) -->
<div <div v-for="hour in hours" :key="'line-' + hour" class="vertical-line" :style="getLineStyle(hour)"></div>
v-for="hour in hours"
:key="'line-' + hour"
class="vertical-line"
:style="getLineStyle(hour)"
></div>
<!-- แสดง Order เฉพาะหน้าปัจจุบันและเครื่องจักรตรงกัน --> <!-- แสดง Order -->
<div <div
v-for="order in orders.filter(o => pages.includes(o.page) && o.page === currentPage && o.machine === machine.name)" v-for="order in orders.filter(o => pages.includes(o.page) && o.page === currentPage && o.machine === machine.name)"
:key="order.id" :key="order.id" class="order" :class="{ 'faded': draggingOrder && draggingOrder.id === order.id }"
class="order" :style="getOrderStyle(order)">
:style="getOrderStyle(order)"
>
<!-- Handle สำหรับ Resize ด้านซ้าย --> <!-- Handle สำหรับ Resize ด้านซ้าย -->
<div class="resize-handle left" <div class="resize-handle left" @mousedown="onResizeStart($event, order, 'left')"></div>
@mousedown="onResizeStart($event, order, 'left')">
</div>
<!-- ส่วนกลางของ Order ใช้สำหรับลาก --> <!-- ส่วนกลางของ Order ใช้สำหรับลาก -->
<div class="order-content" <div class="order-content" draggable="true" @dragstart="onDragStart($event, order)"
draggable="true" @dragend="onDragEnd($event, order)">
@dragstart="onDragStart($event, order)"
@dragend="onDragEnd($event, order)"
>
{{ order.name }} ({{ order.start }} - {{ order.end }}) {{ order.name }} ({{ order.start }} - {{ order.end }})
</div> </div>
<!-- Handle สำหรับ Resize ด้านขวา --> <!-- Handle สำหรับ Resize ด้านขวา -->
<div class="resize-handle right" <div class="resize-handle right" @mousedown="onResizeStart($event, order, 'right')"></div>
@mousedown="onResizeStart($event, order, 'right')">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Ghost Order ขณะลาก -->
<div v-if="ghostOrder" class="drag-ghost" :style="ghostStyle">
{{ ghostOrder.name }} ({{ ghostOrder.start }} - {{ ghostOrder.end }})
</div> </div>
<v-divider :thickness="7"></v-divider> <v-divider :thickness="7"></v-divider>
<!-- Pagination --> <!-- Pagination -->
<div class="pagination"> <div class="pagination">
<button <button v-for="p in pages" :key="p" :class="['page-btn', { active: p === currentPage }]" @click="currentPage = p">
v-for="p in pages"
:key="p"
:class="['page-btn', { active: p === currentPage }]"
@click="currentPage = p"
>
{{ p }} {{ p }}
</button> </button>
<!-- ปุ่ม + สำหรับเพิ่มหน้าใหม่ --> <!-- ปุ่ม + สำหรับเพิ่มหน้าใหม่ -->
...@@ -95,7 +78,7 @@ export default { ...@@ -95,7 +78,7 @@ export default {
{ id: 5, name: 'MC5' }, { id: 5, name: 'MC5' },
], ],
// รายการ Order (ตัวอย่าง Order 1 มีสี blue) // รายการ Order
orders: [ orders: [
{ page: 1, id: 1, name: 'Order 1', start: '13:00', end: '15:00', machine: 'MC1', color: 'blue' }, { 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: 1, id: 2, name: 'Order 2', start: '09:00', end: '11:00', machine: 'MC2' },
...@@ -109,8 +92,26 @@ export default { ...@@ -109,8 +92,26 @@ export default {
resizingOrder: null, resizingOrder: null,
resizeDirection: 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 // จัดการ Pagination
pages: [1, 2], // เริ่มต้นสองหน้า pages: [1, 2],
currentPage: 1, currentPage: 1,
} }
}, },
...@@ -144,7 +145,7 @@ export default { ...@@ -144,7 +145,7 @@ export default {
return { return {
left, left,
width, width,
backgroundColor: order.color || '#4caf50' // ใช้สีใน order หรือสี default backgroundColor: order.color || '#4caf50'
}; };
}, },
...@@ -170,55 +171,142 @@ export default { ...@@ -170,55 +171,142 @@ export default {
const minutes = Math.round((decimal - hours) * 60); const minutes = Math.round((decimal - hours) * 60);
return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); 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) // เริ่มลาก Order (DragStart)
onDragStart(event, order) { onDragStart(event, order) {
this.draggingOrder = order; this.draggingOrder = order;
const rect = event.target.getBoundingClientRect(); const rect = event.target.getBoundingClientRect();
this.dragOffset = event.clientX - rect.left; this.dragOffset = event.target.getBoundingClientRect().width / 2;
event.dataTransfer.effectAllowed = "move";
// ปิด 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) => คำนวณตำแหน่งใหม่ // ขณะลาก (DragOver) ที่ผูกใน template (กัน default เพื่อให้ drop ได้)
onDragEnd(event, order) { onDragOver(event) {
const rowTimeline = event.target.closest('.row-timeline'); // ไม่ได้ทำอะไรมากในที่นี้ เพราะเราจัดการใน onDragOverGlobal
if (!rowTimeline) return; event.preventDefault();
},
// onDragOverGlobal - อัปเดตตำแหน่ง Ghost และเวลา
onDragOverGlobal(event) {
event.preventDefault();
const rowTimeline = document.querySelector('.row-timeline');
if (!rowTimeline) return;
const timelineRect = rowTimeline.getBoundingClientRect(); const timelineRect = rowTimeline.getBoundingClientRect();
const dropX = event.clientX - timelineRect.left - this.dragOffset;
const timelineWidth = timelineRect.width; const timelineWidth = timelineRect.width;
const offsetX = event.clientX - timelineRect.left;
let ratioStart = dropX / timelineWidth; // 1️⃣ คำนวณ Snap ตามแนวนอน (เวลา)
ratioStart = Math.min(Math.max(ratioStart, 0), 1); let ratio = offsetX / timelineWidth;
ratio = Math.min(Math.max(ratio, 0), 1);
const timelineStart = this.timeToDecimal(this.startTime); const timelineStart = this.timeToDecimal(this.startTime);
const timelineEnd = this.timeToDecimal(this.endTime); 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; 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; let newEndDecimal = newStartDecimal + duration;
// ถ้าเกิน endTime ก็ปรับให้พอดี
if (newEndDecimal > timelineEnd) { if (newEndDecimal > timelineEnd) {
newEndDecimal = timelineEnd; newEndDecimal = timelineEnd;
newStartDecimal = newEndDecimal - duration; newStartDecimal = newEndDecimal - duration;
} }
order.start = this.decimalToTime(newStartDecimal); // อัปเดตเวลาใน ghostOrder
order.end = this.decimalToTime(newEndDecimal); this.ghostOrder.start = this.decimalToTime(newStartDecimal);
this.draggingOrder = null; 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 // เมื่อปล่อย Drag บนเครื่องจักรใหม่ => เปลี่ยน machine ของ Order
onDrop(event, newMachine) { onDrop(event, newMachine) {
event.preventDefault();
if (this.draggingOrder) { if (this.draggingOrder) {
this.draggingOrder.machine = newMachine; // ใช้ค่าที่ 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; this.draggingOrder = null;
} }
}, ,
// เริ่ม Resize (mousedown ที่ handle) // เริ่ม Resize (mousedown ที่ handle)
onResizeStart(event, order, direction) { onResizeStart(event, order, direction) {
...@@ -277,6 +365,7 @@ export default { ...@@ -277,6 +365,7 @@ export default {
} }
</script> </script>
<style scoped> <style scoped>
.gantt-chart { .gantt-chart {
width: 100%; width: 100%;
...@@ -284,10 +373,12 @@ export default { ...@@ -284,10 +373,12 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header { .header {
display: flex; display: flex;
background: #fff; background: #fff;
} }
.machine-label { .machine-label {
width: 80px; width: 80px;
text-align: center; text-align: center;
...@@ -296,10 +387,12 @@ export default { ...@@ -296,10 +387,12 @@ export default {
line-height: 40px; line-height: 40px;
border-bottom: 1px solid #ffffff; border-bottom: 1px solid #ffffff;
} }
.time-scale { .time-scale {
display: flex; display: flex;
flex: 1; flex: 1;
} }
.time-cell { .time-cell {
flex: 1; flex: 1;
text-align: center; text-align: center;
...@@ -308,20 +401,25 @@ export default { ...@@ -308,20 +401,25 @@ export default {
line-height: 40px; line-height: 40px;
font-weight: bold; font-weight: bold;
} }
.rows { .rows {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.row { .row {
display: flex; display: flex;
min-height: 60px; min-height: 60px;
border-bottom: 1px solid #ddd; /* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */ border-bottom: 1px solid #ddd;
/* เส้นแบ่งระหว่างเครื่องจักร (แนวนอน) */
} }
.row-timeline { .row-timeline {
position: relative; position: relative;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.vertical-line { .vertical-line {
position: absolute; position: absolute;
top: 0; top: 0;
...@@ -329,6 +427,7 @@ export default { ...@@ -329,6 +427,7 @@ export default {
width: 1px; width: 1px;
background-color: #ddd; background-color: #ddd;
} }
.order { .order {
position: absolute; position: absolute;
top: 10px; top: 10px;
...@@ -340,6 +439,7 @@ export default { ...@@ -340,6 +439,7 @@ export default {
align-items: center; align-items: center;
z-index: 1; z-index: 1;
} }
.order-content { .order-content {
flex: 1; flex: 1;
text-align: center; text-align: center;
...@@ -347,31 +447,52 @@ export default { ...@@ -347,31 +447,52 @@ export default {
cursor: grab; cursor: grab;
padding: 0 10px; padding: 0 10px;
} }
.resize-handle { .resize-handle {
width: 5px; width: 5px;
height: 100%; height: 100%;
background-color: #fff;
cursor: ew-resize; cursor: ew-resize;
position: absolute; position: absolute;
} }
.resize-handle.left { .resize-handle.left {
left: 0; left: 0;
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-bottom-left-radius: 10px; border-bottom-left-radius: 10px;
} }
.resize-handle.right { .resize-handle.right {
right: 0; right: 0;
border-top-right-radius: 10px; border-top-right-radius: 10px;
border-bottom-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 { .pagination {
display: flex; display: flex;
justify-content: flex-start; /* จัดเรียงชิดซ้าย */ justify-content: flex-start;
align-items: center; /* จัดกึ่งกลางแนวตั้ง */ /* จัดเรียงชิดซ้าย */
gap: 5px; /* เว้นระยะระหว่างปุ่ม */ align-items: center;
width: 100%; /* ทำให้ `.pagination` ขยายเต็มพื้นที่ */ /* จัดกึ่งกลางแนวตั้ง */
gap: 5px;
/* เว้นระยะระหว่างปุ่ม */
width: 100%;
/* ทำให้ `.pagination` ขยายเต็มพื้นที่ */
margin-top: 10px; margin-top: 10px;
} }
.page-btn { .page-btn {
background: #ffffff; background: #ffffff;
border: 1px solid #ffffff; border: 1px solid #ffffff;
...@@ -379,10 +500,12 @@ export default { ...@@ -379,10 +500,12 @@ export default {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
} }
.page-btn.active { .page-btn.active {
background: #007bff; background: #007bff;
color: #ffffff; color: #ffffff;
} }
.page-btn.add-page { .page-btn.add-page {
font-weight: bold; font-weight: bold;
background: #ffffff; background: #ffffff;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment