From 21969c3ebb86d45c082d316aebb33b32ebafbc97 Mon Sep 17 00:00:00 2001 From: Kritkhanin Anantakul <65160144@go.buu.ac.th> Date: Sun, 9 Mar 2025 16:41:19 +0700 Subject: [PATCH] make queue bnt and algorithm cal --- src/components/GanttChart/GanttCalendar.vue | 2 +- src/components/GanttChart/GanttChart.vue | 83 ++--- src/components/GanttChart/MakequeueBtn.vue | 28 ++ src/components/GanttChart/ganttChart.css | 6 + .../GanttChart/scheduleCalculator.js | 306 ++++++++++++++++++ src/views/ProductQueueView.vue | 1 - 6 files changed, 371 insertions(+), 55 deletions(-) create mode 100644 src/components/GanttChart/MakequeueBtn.vue create mode 100644 src/components/GanttChart/scheduleCalculator.js diff --git a/src/components/GanttChart/GanttCalendar.vue b/src/components/GanttChart/GanttCalendar.vue index 541850f..b3ad73a 100644 --- a/src/components/GanttChart/GanttCalendar.vue +++ b/src/components/GanttChart/GanttCalendar.vue @@ -6,7 +6,7 @@ import CalendarPicker from "@/components/GanttChart/CalendarPicker.vue"; const props = defineProps({ modelValue: { type: String, - default: '1/1/2024', // เปลี่ยนเป็น format `d/M/yyyy` + default: '1/1/2025', // เปลี่ยนเป็น format `d/M/yyyy` }, }); diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index 41ae98d..a32b2ac 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -1,8 +1,12 @@ <template> <div class="gantt-container"> - <!-- Calendar: เชื่อมกับ selectedDate --> - <GanttCalendar v-model:modelValue="selectedDate" /> - + <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 --> @@ -121,11 +125,13 @@ 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, @@ -134,7 +140,7 @@ export default { return { showAddDialog: false, // ค่าจาก Calendar - selectedDate: '1/1/2024', + selectedDate: '1/1/2025', // กำหนดช่วงเวลาใน Gantt startTime: '08:00', @@ -142,22 +148,21 @@ export default { // รายการเครื่องจักร machines: [ - { machineID: 'MC1' ,name: 'เครื่องเป่าขวด'}, - { machineID: 'MC2' ,name: 'เครื่องเป่าขวด2'}, - { machineID: 'MC3' ,name: 'เครื่องสวมฉลาก'}, - { machineID: 'MC4' ,name: 'เครื่องสวมฉลาก2'}, - { machineID: 'MC5' ,name: 'เครื่องบรรจุ+แพ็ค'}, + { machineID: 'MC1' , name: 'เครื่องเป่าขวด' }, + { machineID: 'MC2' , name: 'เครื่องเป่าขวด2' }, + { machineID: 'MC3' , name: 'เครื่องสวมฉลาก' }, + { machineID: 'MC4' , name: 'เครื่องบรรจุ+แพ็ค' }, ], - // เปลี่ยนจาก orders เป็น Queue + // รายการ Queue เริ่มต้น (สามารถเปลี่ยนแปลงได้) Queue: [ { queueID: 1, machineID: 'MC1', orderID: 5, pageNumber: 1, - startTime: '1/1/2024 09:00', - finishTime: '1/1/2024 11:00', + startTime: '1/1/2025 09:00', + finishTime: '1/1/2025 11:00', status: 'Process', bottleSize: '600ml', producedQuantity: 400, @@ -167,45 +172,12 @@ export default { machineID: 'MC2', orderID: 1, pageNumber: 1, - startTime: '1/1/2024 13:00', - finishTime: '1/1/2024 15:00', + startTime: '1/1/2025 13:00', + finishTime: '1/1/2025 15:00', status: 'Waiting', bottleSize: '500ml', producedQuantity: 200, }, - { - queueID: 3, - machineID: 'MC1', - orderID: 2, - 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: 2, - 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 8:00', - finishTime: '2/1/2024 12:00', - status: 'Process', - bottleSize: '600ml', - producedQuantity: 500, - }, ], // Drag/Drop และ Resize @@ -250,6 +222,14 @@ export default { }, }, methods: { + // รับข้อมูล Queue ที่ส่งมาจาก MakequeueBtn + handleUpdateQueue(newQueue) { + console.log("Received new queue:", newQueue); + // ในที่นี้เราสามารถแทนที่ Queue เดิมหรือ merge กับข้อมูลที่มีอยู่ + // ตัวอย่างนี้ใช้แทนที่ Queue ทั้งหมด + this.Queue = newQueue; + }, + // -------------------- Dialog & Edit -------------------- openQueueDialog(item) { this.selectedQueueItem = { ...item }; @@ -262,11 +242,10 @@ export default { this.showAddDialog = true; }, deleteQueueItem(item) { - this.Queue = this.Queue.filter(q => q.queueID !== item.queueID); - this.closeQueueDialog(); + this.Queue = this.Queue.filter(q => q.queueID !== item.queueID); + this.closeQueueDialog(); }, openAddQueueDialogForEdit(queueItem) { - // เมื่อกด Edit ใน OrderDialog ให้เปิด AddQueueDialog ในโหมดแก้ไข this.selectedQueueItem = { ...queueItem }; this.showAddDialog = true; }, @@ -275,15 +254,13 @@ export default { this.selectedQueueItem = null; }, handleAddQueue(newQueueItem) { - // เพิ่ม newQueueItem ลงใน Queue this.Queue.push({ - queueID: this.Queue.length + 1, // กำหนด queueID ใหม่ + queueID: this.Queue.length + 1, ...newQueueItem, }); this.showAddDialog = false; }, handleEditQueue(updatedQueueItem) { - // หา index ของ queue ที่ต้องแก้ไข const index = this.Queue.findIndex(q => q.queueID === updatedQueueItem.queueID); if (index !== -1) { this.Queue.splice(index, 1, { ...updatedQueueItem }); diff --git a/src/components/GanttChart/MakequeueBtn.vue b/src/components/GanttChart/MakequeueBtn.vue new file mode 100644 index 0000000..bff4e23 --- /dev/null +++ b/src/components/GanttChart/MakequeueBtn.vue @@ -0,0 +1,28 @@ +<template> + <div> + <button @click="makeQueue">Make Queue</button> + </div> + </template> + + <script> + import { scheduleAllOrders } from './scheduleCalculator.js'; + + export default { + name: 'MakequeueBtn', + methods: { + makeQueue() { + const queueItems = scheduleAllOrders(); + // ส่งข้อมูล queue ไปยังคอมโพเนนต์พาเรนต์ + this.$emit('updateQueue', queueItems); + } + } + } + </script> + + <style scoped> + button { + padding: 8px 16px; + font-size: 16px; + } + </style> + \ No newline at end of file diff --git a/src/components/GanttChart/ganttChart.css b/src/components/GanttChart/ganttChart.css index 2903ba1..ba67c4b 100644 --- a/src/components/GanttChart/ganttChart.css +++ b/src/components/GanttChart/ganttChart.css @@ -9,6 +9,12 @@ display: flex; background: #fff; } + .gantt-header { + display: flex; + align-items: center; + justify-content: space-between; /* จัดให้ Calendar อยู่ซ้าย, ปุ่มอยู่ขวา */ + margin-bottom: 16px; + } .machine-label { width: 150px; diff --git a/src/components/GanttChart/scheduleCalculator.js b/src/components/GanttChart/scheduleCalculator.js new file mode 100644 index 0000000..4adf107 --- /dev/null +++ b/src/components/GanttChart/scheduleCalculator.js @@ -0,0 +1,306 @@ +// src/utils/schedule.js + +// ฟังก์ชันช่วยจัดการวันที่และเวลา +function roundDownToHalfHour(dt) { + const newDate = new Date(dt); + const minutes = newDate.getMinutes(); + if (minutes < 30) { + newDate.setMinutes(0, 0, 0); + } else { + newDate.setMinutes(30, 0, 0); + } + return newDate; + } + + // แบ่งช่วงเวลางานตามเวลาทำงาน (08:00–12:00, 13:00–17:00) + function scheduleTaskSegments(startTime, durationHours) { + const segments = []; + let current = new Date(startTime); + let remaining = durationHours; + while (remaining > 0) { + const day = new Date(current.getFullYear(), current.getMonth(), current.getDate()); + const morningStart = new Date(day); + morningStart.setHours(8, 0, 0, 0); + const morningEnd = new Date(day); + morningEnd.setHours(12, 0, 0, 0); + const afternoonStart = new Date(day); + afternoonStart.setHours(13, 0, 0, 0); + const afternoonEnd = new Date(day); + afternoonEnd.setHours(17, 0, 0, 0); + + if (current < morningStart) { + current = new Date(morningStart); + } else if (current >= morningEnd && current < afternoonStart) { + current = new Date(afternoonStart); + } + + const segLimit = (current < morningEnd) ? morningEnd : afternoonEnd; + const available = (segLimit - current) / (3600 * 1000); + if (available >= remaining) { + let segEnd = new Date(current.getTime() + remaining * 3600 * 1000); + segEnd = roundDownToHalfHour(segEnd); + segments.push({ start: new Date(current), end: segEnd }); + current = segEnd; + remaining = 0; + } else { + let segEnd = roundDownToHalfHour(segLimit); + segments.push({ start: new Date(current), end: segEnd }); + remaining -= available; + const nextDay = new Date(current); + nextDay.setDate(current.getDate() + 1); + nextDay.setHours(8, 0, 0, 0); + current = nextDay; + } + } + return { segments, endTime: current }; + } + + function formatDate(dt) { + const month = dt.getMonth() + 1; + const day = dt.getDate(); + const year = dt.getFullYear(); + const hours = String(dt.getHours()).padStart(2, '0'); + const minutes = String(dt.getMinutes()).padStart(2, '0'); + return `${month}/${day}/${year} ${hours}:${minutes}`; + } + + // ============================== + // ข้อมูลจำลอง (Data) + // ============================== + + // ข้อมูลเครื่องจักร + const machines_data = { + "MC1": { name: "เครื่องเป่าขวด", speed: { 600: 2000 } }, + "MC3": { name: "เครื่องสวมฉลาก", speed: { 600: 4000 } }, + "MC4": { name: "เครื่องบรรจุ + แพ็ค", speed: { 600: 200 } } + }; + + // รายการออเดอร์ (เพิ่ม orderID)q + const orders = [ + { orderID: 1, customer: "จอห์น", brand: "น้ำดื่มยี่ห้อ A", size: 600, packs: 300 }, + { orderID: 2, customer: "เจน", brand: "น้ำดื่มยี่ห้อ A", size: 600, packs: 300 } + ]; + + // จำนวนขวดต่อแพ็ค (สำหรับ 600 ml = 12 ขวด/แพ็ค) + const bottles_per_pack_map = { 600: 12 }; + + // สต๊อกสินค้าสำเร็จรูป + const product_stock = { + "น้ำดื่มยี่ห้อ A": { 600: 0 } + }; + + // วัตถุดิบ + const raw_materials = { + "MT001": { name: "ฉลาก ยี่ห้อ A", stock: 200000, usage_per_bottle: 1 }, + "MT005": { name: "Preform (ขวด)", stock: 200000, usage_per_bottle: 1 }, + "MT007": { name: "ขวดเปล่า", stock: 0, usage_per_bottle: 1 }, + "MT008": { name: "ขวดเปล่าพร้อมฉลาก ยี่ห้อ A", stock: 0, usage_per_bottle: 1 } + }; + + // ตารางเวลาของเครื่องจักร (เก็บ task ในแต่ละเครื่อง) + const schedule = { + "MC1": [], + "MC3": [], + "MC4": [] + }; + + // เวลาที่เครื่องจักรว่าง (เริ่มต้นที่ 1 มีนาคม 2025 เวลา 08:00) + // หมายเหตุ: เดือนใน JS เริ่มที่ 0 => 2 หมายถึงมีนาคม + const machine_available = { + "MC1": new Date(2025, 0, 1, 8, 0, 0), + "MC3": new Date(2025, 0, 1, 8, 0, 0), + "MC4": new Date(2025, 0, 1, 8, 0, 0) + }; + + // ฟังก์ชันช่วยปัดจำนวนผลิต + function roundBottlesUp(bottles_needed, step = 1000) { + return Math.ceil(bottles_needed / step) * step; + } + + function roundPacksUp(packs_needed, step = 100) { + return Math.ceil(packs_needed / step) * step; + } + + // ============================== + // ฟังก์ชันจัดตารางงานสำหรับแต่ละออเดอร์ + // ============================== + function schedule_order(order) { + const { orderID, size, packs, customer, brand } = order; + const required_bottles = packs * bottles_per_pack_map[size]; + + // --- เช็คสินค้าสำเร็จรูป --- + if (product_stock[brand][size] >= packs) { + product_stock[brand][size] -= packs; + console.log(`Order ${customer}: ใช้สินค้าสำเร็จรูป (ไม่ต้องผลิต)`); + return; + } + + // --- เช็ควัตถุดิบ: ถ้ามี ขวดเปล่าพร้อมฉลาก (MT008) พอ --- + if (raw_materials["MT008"].stock >= required_bottles) { + raw_materials["MT008"].stock -= required_bottles; + const packs_MC4 = roundPacksUp(packs, 100); + const time_MC4 = packs_MC4 / machines_data["MC4"].speed[size]; + let start_MC4 = new Date(machine_available["MC4"]); + const day = new Date(start_MC4.getFullYear(), start_MC4.getMonth(), start_MC4.getDate()); + const afternoonStart = new Date(day); + afternoonStart.setHours(13, 0, 0, 0); + if (start_MC4 < afternoonStart) { + start_MC4 = new Date(afternoonStart); + } + const result_MC4 = scheduleTaskSegments(start_MC4, time_MC4); + result_MC4.segments.forEach(seg => { + schedule["MC4"].push({ + start: seg.start, + end: seg.end, + task: `บรรจุ+แพ็ค ~ ${packs_MC4} แพ็ค (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC4"] = new Date(result_MC4.endTime); + return; + } + + // --- เช็ควัตถุดิบ: ถ้ามี ขวดเปล่า (MT007) พอ --- + if (raw_materials["MT007"].stock >= required_bottles) { + raw_materials["MT007"].stock -= required_bottles; + const bottles_MC3 = roundBottlesUp(required_bottles, 1000); + const time_MC3 = bottles_MC3 / machines_data["MC3"].speed[size]; + let start_MC3 = new Date(machine_available["MC1"]); // ใช้เวลาที่ MC1 ว่าง + const result_MC3 = scheduleTaskSegments(start_MC3, time_MC3); + result_MC3.segments.forEach(seg => { + schedule["MC3"].push({ + start: seg.start, + end: seg.end, + task: `สวมฉลาก ~ ${bottles_MC3} ขวด (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC3"] = new Date(result_MC3.endTime); + const dependency_time = new Date(result_MC3.endTime); + + const packs_MC4 = roundPacksUp(packs, 100); + const time_MC4 = packs_MC4 / machines_data["MC4"].speed[size]; + let start_MC4 = new Date(Math.max(machine_available["MC4"], dependency_time)); + const day2 = new Date(start_MC4.getFullYear(), start_MC4.getMonth(), start_MC4.getDate()); + const afternoonStart2 = new Date(day2); + afternoonStart2.setHours(13, 0, 0, 0); + if (start_MC4 < afternoonStart2) { + start_MC4 = new Date(afternoonStart2); + } + const result_MC4 = scheduleTaskSegments(start_MC4, time_MC4); + result_MC4.segments.forEach(seg => { + schedule["MC4"].push({ + start: seg.start, + end: seg.end, + task: `บรรจุ+แพ็ค ~ ${packs_MC4} แพ็ค (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC4"] = new Date(result_MC4.endTime); + return; + } + + // --- กรณีไม่ครบวัตถุดิบ: ผลิตครบทุกขั้นตอน --- + // 1. MC1: เป่าขวด + const bottles_MC1 = roundBottlesUp(required_bottles, 1000); + const time_MC1 = bottles_MC1 / machines_data["MC1"].speed[size]; + let start_MC1 = new Date(machine_available["MC1"]); + const result_MC1 = scheduleTaskSegments(start_MC1, time_MC1); + result_MC1.segments.forEach(seg => { + schedule["MC1"].push({ + start: seg.start, + end: seg.end, + task: `เป่าขวด ${bottles_MC1} ขวด (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC1"] = new Date(result_MC1.endTime); + let dependency_time = new Date(result_MC1.endTime); + + // 2. MC3: สวมฉลาก (ใช้จำนวนเดียวกับ MC1) + const bottles_MC3_full = bottles_MC1; + const time_MC3_full = bottles_MC3_full / machines_data["MC3"].speed[size]; + let start_MC3_full = new Date(Math.max(machine_available["MC3"], dependency_time)); + const result_MC3_full = scheduleTaskSegments(start_MC3_full, time_MC3_full); + result_MC3_full.segments.forEach(seg => { + schedule["MC3"].push({ + start: seg.start, + end: seg.end, + task: `สวมฉลาก ~ ${bottles_MC3_full} ขวด (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC3"] = new Date(result_MC3_full.endTime); + dependency_time = new Date(result_MC3_full.endTime); + + // 3. MC4: บรรจุ+แพ็ค + const packs_MC4_full = roundPacksUp(packs, 100); + const time_MC4_full = packs_MC4_full / machines_data["MC4"].speed[size]; + let start_MC4_full = new Date(Math.max(machine_available["MC4"], dependency_time)); + const day3 = new Date(start_MC4_full.getFullYear(), start_MC4_full.getMonth(), start_MC4_full.getDate()); + const afternoonStart3 = new Date(day3); + afternoonStart3.setHours(13, 0, 0, 0); + if (start_MC4_full < afternoonStart3) { + start_MC4_full = new Date(afternoonStart3); + } + const result_MC4_full = scheduleTaskSegments(start_MC4_full, time_MC4_full); + result_MC4_full.segments.forEach(seg => { + schedule["MC4"].push({ + start: seg.start, + end: seg.end, + task: `บรรจุ+แพ็ค ~ ${packs_MC4_full} แพ็ค (Order ${orderID})`, + orderID, + producedQuantity: required_bottles + }); + }); + machine_available["MC4"] = new Date(result_MC4_full.endTime); + } + // ============================== + // ฟังก์ชันจัดตารางงานสำหรับออเดอร์ทั้งหมด (FIFO) + // ============================== + function scheduleAllOrders() { + schedule["MC001"] = []; + schedule["MC003"] = []; + schedule["MC004"] = []; + machine_available["MC001"] = new Date(2025, 0, 1, 8, 0, 0); + machine_available["MC003"] = new Date(2025, 0, 1, 8, 0, 0); + machine_available["MC004"] = new Date(2025, 0, 1, 8, 0, 0); + + for (const order of orders) { + if (product_stock[order.brand][order.size] >= order.packs) { + product_stock[order.brand][order.size] -= order.packs; + console.log(`Order ${order.customer}: ใช้สินค้าสำเร็จรูป`); + } else { + schedule_order(order); + } + } + + // รวม queue จาก schedule + const queueItems = []; + let idCounter = 1; + for (const machineKey of Object.keys(schedule)) { + for (const task of schedule[machineKey]) { + queueItems.push({ + queueID: idCounter++, + machineID: machineKey, + orderID: task.orderID, + pageNumber: 1, + startTime: formatDate(task.start), // ✅ เปลี่ยนรูปแบบเป็น "1/1/2025 HH:mm" + finishTime: formatDate(task.end), + status: "Process", + bottleSize: "600ml", + producedQuantity: task.producedQuantity, + taskDescription: task.task + }); + } + } + return queueItems; + } + + export { scheduleAllOrders }; + \ No newline at end of file diff --git a/src/views/ProductQueueView.vue b/src/views/ProductQueueView.vue index 7c6ec08..e24eed3 100644 --- a/src/views/ProductQueueView.vue +++ b/src/views/ProductQueueView.vue @@ -3,7 +3,6 @@ import { ref } from 'vue' import GanttChart from '@/components/GanttChart/GanttChart.vue' import EmployeeSector from '@/components/EmployeeSector.vue' -const selectedDate = ref('1/1/2024') const employees = Array.from({ length: 10 }, (_, i) => `EM${i + 1}`) const orders = ref([ { id: 1, name: 'ORDER1' }, -- GitLab