From 41c6eb42171421b7e96bfedb0b6ac0bef082c52f Mon Sep 17 00:00:00 2001 From: Kritkhanin Anantakul <65160144@go.buu.ac.th> Date: Tue, 18 Mar 2025 16:42:49 +0700 Subject: [PATCH] all gantt service store / connect fontend backend / fix calendar format/ fix some ui --- src/components/GanttChart/AddQueueDialog.vue | 231 +++++++++++------- src/components/GanttChart/CalendarPicker.vue | 131 ++++------ src/components/GanttChart/GanttCalendar.vue | 64 ++--- src/components/GanttChart/GanttChart.vue | 142 +++++------ src/components/GanttChart/OrderDialog.vue | 2 +- .../GanttChart/scheduleCalculator.js | 2 +- src/components/OrderItem.vue | 75 ------ src/router/index.ts | 16 ++ src/services/http.ts | 2 +- src/services/machine.ts | 41 ++++ src/services/order.ts | 45 ++++ src/services/queue.ts | 123 +++------- src/stores/machine.ts | 95 +++++++ src/stores/order.ts | 100 ++++++++ src/stores/queue.ts | 147 +++++++++-- src/types/Machine.ts | 12 + src/types/Order.ts | 13 + src/types/Queue.ts | 44 +++- src/types/QueueItem.ts | 11 - src/views/MachineManagementView.vue | 118 +++++++++ 20 files changed, 916 insertions(+), 498 deletions(-) delete mode 100644 src/components/OrderItem.vue create mode 100644 src/services/machine.ts create mode 100644 src/services/order.ts create mode 100644 src/stores/machine.ts create mode 100644 src/stores/order.ts create mode 100644 src/types/Machine.ts create mode 100644 src/types/Order.ts delete mode 100644 src/types/QueueItem.ts create mode 100644 src/views/MachineManagementView.vue diff --git a/src/components/GanttChart/AddQueueDialog.vue b/src/components/GanttChart/AddQueueDialog.vue index 90aba53..766a2ab 100644 --- a/src/components/GanttChart/AddQueueDialog.vue +++ b/src/components/GanttChart/AddQueueDialog.vue @@ -4,9 +4,10 @@ <h3 class="white-text">{{ isEditing ? "Edit Queue" : "Add New Queue" }}</h3> <form @submit.prevent="handleSubmit"> <div class="form-group"> - <label class="white-text">Machine ID:</label> - <select v-model="form.status" class="border-input" required> - <option v-for="machine in machines" :key="machine.machineID" :value="machine.machineID"> + <label class="white-text">Machine Name:</label> + <select v-model.number="form.machineID" class="border-input" required> + <option value="" disabled>เลือกเครื่องจักร</option> + <option v-for="machine in machines" :key="machine.MachineID" :value="machine.MachineID"> {{ machine.name }} </option> </select> @@ -14,29 +15,32 @@ <div class="form-group"> <label class="white-text">Order ID:</label> - <input class="border-input" v-model.number="form.orderID" type="number" required /> + <select v-model.number="form.orderID" class="border-input" required> + <option value="" disabled>เลือกคำสั่งซื้อ</option> + <option v-for="order in orders" :key="order.OrderID" :value="order.OrderID"> + {{ order.OrderID }} + </option> + </select> </div> - <!-- 🆕 Start Time & Finish Time: ใช้ DateTime Picker --> <div class="form-group"> <label class="white-text">Start Time:</label> - <input class="border-input" type="datetime-local" v-model="form.startTime" required /> + <input v-model="form.startTime" class="border-input" type="datetime-local" required /> </div> <div class="form-group"> <label class="white-text">Finish Time:</label> - <input class="border-input" type="datetime-local" v-model="form.finishTime" required /> + <input v-model="form.finishTime" class="border-input" type="datetime-local" required /> </div> - <!-- 🆕 Status: ใช้ Dropdown --> <div class="form-group"> <label class="white-text">Status:</label> <select v-model="form.status" class="border-input" required> - <option value="ยังไม่ได้จัดส่ง">ยังไม่ได้จัดส่ง</option> - <option value="จัดส่งแล้ว">จัดส่งแล้ว</option> + <option value="Pending">Pending</option> + <option value="Process">Process</option> + <option value="Done">Done</option> </select> </div> - <!-- 🆕 Bottle Size: ใช้ Dropdown --> <div class="form-group"> <label class="white-text">Bottle Size:</label> <select v-model="form.bottleSize" class="border-input" required> @@ -48,10 +52,14 @@ <div class="form-group"> <label class="white-text">Produced Quantity:</label> - <input class="border-input" v-model.number="form.producedQuantity" type="number" required /> + <input v-model.number="form.producedQuantity" class="border-input" type="number" required /> + </div> + + <div class="form-group"> + <label class="white-text">Page ID:</label> + <input v-model.number="form.pageID" class="border-input" type="number" readonly /> </div> - <!-- ปุ่ม Add หรือ Save Changes --> <div class="dialog-buttons"> <button type="submit" class="primary-btn"> {{ isEditing ? "Save Changes" : "Add" }} @@ -64,100 +72,139 @@ </template> <script setup lang="ts"> -import { reactive, ref, watch } from 'vue' - -interface QueueItem { - machineID: string - orderID: number | null - startTime: string - finishTime: string - status: string - bottleSize: string - producedQuantity: number | null - queueID?: number -} +import { reactive, ref, watch, onMounted, computed } from 'vue'; +import { useMachineStore } from '@/stores/machine'; +import { useOrderStore } from '@/stores/order'; +import { useQueueStore } from '@/stores/queue'; +import type { Queue } from '@/types/Queue'; -interface Machine { - machineID: string - name: string -} - -// Props const props = defineProps<{ - visible: boolean - queueItem?: QueueItem -}>() + visible: boolean; + currentPage: number; + queueItem?: Queue; +}>(); -// Emits const emit = defineEmits<{ - (e: 'close'): void - (e: 'edit', payload: QueueItem): void - (e: 'add', payload: QueueItem): void -}>() - -// Reactive form state -const form = reactive<QueueItem>({ - machineID: '', - orderID: null, - startTime: '', - finishTime: '', - status: 'ยังไม่ได้จัดส่ง', - bottleSize: '600 ml', - producedQuantity: null, -}) - -// Static machines list -const machines: Machine[] = [ - { machineID: 'MC1', name: 'เครื่องเป่าขวด' }, - { machineID: 'MC2', name: 'เครื่องเป่าขวด2' }, - { machineID: 'MC3', name: 'เครื่องสวมฉลาก' }, - { machineID: 'MC4', name: 'เครื่องสวมฉลาก2' }, - { machineID: 'MC5', name: 'เครื่องบรรจุ+แพ็ค' }, -] - -// Track editing mode -const isEditing = ref(false) - -// Functions -function resetForm() { - form.machineID = '' - form.orderID = null - form.startTime = '' - form.finishTime = '' - form.status = 'ยังไม่ได้จัดส่ง' - form.bottleSize = '600 ml' - form.producedQuantity = null -} + (e: 'close'): void; +}>(); -function closeDialog() { - console.log("Closing dialog") - emit("close") - resetForm() -} +const machineStore = useMachineStore(); +const orderStore = useOrderStore(); +const queueStore = useQueueStore(); -function handleSubmit() { - if (isEditing.value) { - emit("edit", { ...form }) - } else { - emit("add", { ...form, queueID: Date.now() }) +const machines = computed(() => machineStore.machines); +const orders = computed(() => orderStore.orders); + +onMounted(async () => { + await machineStore.fetchMachines(); + await orderStore.fetchOrders(); +}); + +const isEditing = ref(false); + +const form = reactive({ + machineID: null as number | null, + orderID: null as number | null, + startTime: new Date().toISOString().slice(0, 16), + finishTime: new Date().toISOString().slice(0, 16), + status: "Pending", + bottleSize: "600 ml", + producedQuantity: null as number | null, + pageID: Number(props.currentPage), +}); + +// ฟังก์ชันแปลงเวลาให้เข้ากับ datetime-local +function formatDateTime(dateTime: string | Date): string { + if (!dateTime) return ""; + + // ถ้า dateTime เป็น Date ให้แปลงเป็น ISO String ก่อน + if (dateTime instanceof Date) { + return dateTime.toISOString().slice(0, 16); } - closeDialog() + + return dateTime.replace(" ", "T"); // แปลง "2025-01-01 09:00" เป็น "2025-01-01T09:00" } -// Watch for changes on queueItem prop to toggle edit mode +// โหลดข้อมูล queueItem เมื่อ Edit watch( () => props.queueItem, - (newItem) => { - if (newItem) { - Object.assign(form, newItem) - isEditing.value = true + (newQueueItem) => { + if (newQueueItem) { + console.log("🛠 queueItem received:", newQueueItem); + + isEditing.value = true; + form.machineID = newQueueItem.machineID ?? null; + form.orderID = newQueueItem.orderID ?? null; + form.startTime = formatDateTime(newQueueItem.startTime); + form.finishTime = formatDateTime(newQueueItem.finishTime); + form.status = newQueueItem.status; + form.bottleSize = newQueueItem.bottleSize; + form.producedQuantity = newQueueItem.producedQuantity; + form.pageID = newQueueItem.pageID ?? Number(props.currentPage); } else { - resetForm() - isEditing.value = false + isEditing.value = false; + resetForm(); } }, { immediate: true } -) +); + +function closeDialog() { + console.log("🔴 Closing Dialog... Emitting 'close' event"); + resetForm(); + emit("close"); +} + +function resetForm() { + form.machineID = null; + form.orderID = null; + form.startTime = new Date().toISOString().slice(0, 16); + form.finishTime = new Date().toISOString().slice(0, 16); + form.status = "Pending"; + form.bottleSize = "600 ml"; + form.producedQuantity = null; + form.pageID = Number(props.currentPage); + isEditing.value = false; +} + +function handleSubmit() { + console.log("🚀 Submitting Form:", form); + + if (!form.machineID || !form.orderID || !form.startTime || !form.finishTime || !form.status || !form.bottleSize || !form.producedQuantity) { + console.warn("⚠️ กรุณากรอกข้อมูลให้ครบทุกช่อง!"); + return; + } + + const payload = { + MachineID: form.machineID, + OrderID: form.orderID, + PageID: form.pageID, + startTime: new Date(form.startTime).toISOString(), + finishTime: new Date(form.finishTime).toISOString(), + status: form.status, + bottleSize: form.bottleSize, + producedQuantity: form.producedQuantity, + EmployeeIds: [], + }; + + if (isEditing.value && props.queueItem) { + queueStore + .updateQueue(props.queueItem.QueueID, payload) + .then(() => { + console.log("✅ Queue updated successfully!", payload); + closeDialog(); + }) + .catch((error) => console.error("❌ Error updating queue:", error)); + } else { + queueStore + .addQueue(payload) + .then(() => { + console.log("✅ Queue added successfully!", payload); + closeDialog(); + }) + .catch((error) => console.error("❌ Error adding queue:", error)); + } +} </script> <style scoped> diff --git a/src/components/GanttChart/CalendarPicker.vue b/src/components/GanttChart/CalendarPicker.vue index 17afce4..2f24006 100644 --- a/src/components/GanttChart/CalendarPicker.vue +++ b/src/components/GanttChart/CalendarPicker.vue @@ -1,120 +1,93 @@ <script setup> -import { ref, computed, watch } from 'vue'; -import { format, parse, parseISO, isValid } from 'date-fns'; +import { ref, computed, watch } from "vue"; +import { format, parse, parseISO, isValid } from "date-fns"; const props = defineProps({ - modelValue: String, // ค่าที่รับมาจาก GanttCalendar.vue + modelValue: String, // ค่าที่รับมาจาก GanttCalendar.vue }); const emits = defineEmits(["update:modelValue"]); -// ใช้ ref เก็บค่า selectedDate -const selectedDate = ref(props.modelValue ? parseDateDMY(props.modelValue) : new Date()); +// ✅ ตรวจสอบและแปลงวันที่ให้เป็น Date Object +function parseDate(dateString) { + if (!dateString) return new Date(); -// ฟังก์ชันแปลง `d/M/yyyy` หรือ `yyyy-MM-dd` → `Date` -function parseDateDMY(dateString) { - if (!dateString) return new Date(); + // ตรวจสอบว่าเป็นรูปแบบ "YYYY-MM-DD" + if (dateString.includes("-")) { + const parsedDate = parseISO(dateString); + if (isValid(parsedDate)) return parsedDate; + } - try { - const parts = dateString.includes('-') ? dateString.split('-') : dateString.split('/'); + // ตรวจสอบว่าเป็นรูปแบบ "d/M/yyyy" + if (dateString.includes("/")) { + const parsedDate = parse(dateString, "d/M/yyyy", new Date()); + if (isValid(parsedDate)) return parsedDate; + } - if (parts.length !== 3) throw new Error("Invalid date format"); - - const day = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10) - 1; // `month` ใน JavaScript เริ่มที่ 0 - const year = parseInt(parts[2], 10); - - // ถ้าปีเป็นสองหลัก ให้บวก 2000 เข้าไป - const fullYear = year < 100 ? year + 2000 : year; - - const parsedDate = new Date(fullYear, month, day); - - if (!isValid(parsedDate)) throw new Error("Invalid parsed date"); - - return parsedDate; - } catch (error) { - console.error("📅 parseDateDMY ERROR:", error, "ค่าที่รับมา:", dateString); - return new Date(); - } + return new Date(); // ถ้ารูปแบบไม่ถูกต้อง ให้ใช้วันที่ปัจจุบันแทน } -// ฟังก์ชันแปลง `Date` → `d/M/yyyy` -const formattedDate = computed(() => { - if (!selectedDate.value) return null; - const date = new Date(selectedDate.value); - return format(date, 'd/M/yyyy'); -}); +// ✅ ใช้ ref เก็บค่า selectedDate และแปลงจาก modelValue +const selectedDate = ref(parseDate(props.modelValue)); -// watch `formattedDate` แล้ว emit กลับไปที่ `GanttCalendar.vue` -watch(formattedDate, (newValue) => { - if (newValue !== null) { - console.log("📅 CalendarPicker - ส่งค่ากลับไปเป็น:", newValue); - emits("update:modelValue", newValue); - } +// ✅ แปลง `Date` → `YYYY-MM-DD` +const formattedDate = computed(() => format(selectedDate.value, "yyyy-MM-dd")); + +// ✅ เมื่อ `formattedDate` เปลี่ยน ให้ emit ค่าออกไปที่ `GanttCalendar.vue` +watch(formattedDate, (newVal) => { + console.log("📅 CalendarPicker - ส่งค่ากลับเป็น:", newVal); + emits("update:modelValue", newVal); }); -// ฟังก์ชันอัปเดตวันที่จาก `v-date-picker` +// ✅ ฟังก์ชันอัปเดตวันที่จาก `v-date-picker` const updateSelectedDate = (newValue) => { - selectedDate.value = newValue; -}; - -// ฟังก์ชันสลับเปิด-ปิด Date Picker -const showDatePicker = ref(false); -const toggleDatePicker = () => { - showDatePicker.value = !showDatePicker.value; + selectedDate.value = newValue; }; - </script> - - <template> - <v-container> - <v-row justify="center"> - <v-date-picker v-model="selectedDate" color="#96A9DB" class="custom-date-picker" - @update:modelValue="updateSelectedDate"> - </v-date-picker> - </v-row> - </v-container> + <v-container> + <v-row justify="center"> + <v-date-picker + v-model="selectedDate" + color="#96A9DB" + class="custom-date-picker" + @update:modelValue="updateSelectedDate" + ></v-date-picker> + </v-row> + </v-container> </template> <style scoped> /* ✅ ปรับขนาด v-date-picker */ .custom-date-picker { - width: 350px !important; - /* ✅ ลดความกว้าง */ - height: 390px !important; - max-width: 350px; - font-size: 14px !important; - /* ✅ ลดขนาดตัวอักษร */ - border-radius: 20px !important; - /* ✅ ทำให้ขอบโค้งมน */ + width: 350px !important; + height: 390px !important; + max-width: 350px; + font-size: 14px !important; + border-radius: 20px !important; } -/* ✅ ลดขนาดของตัวอักษรวันที่ (Mon, Jan 1) */ +/* ✅ ลดขนาดของตัวอักษรวันที่ */ :deep(.v-date-picker-title) { - font-size: 14px !important; + font-size: 14px !important; } /* ✅ ปรับขนาดปุ่มวันที่ */ :deep(.v-date-picker .v-btn) { - min-width: 32px !important; - /* ✅ ลดความกว้างของปุ่ม */ - height: 32px !important; - /* ✅ ลดความสูงของปุ่ม */ - font-size: 15px !important; - /* ✅ ลดขนาดตัวเลขในปฏิทิน */ - border-radius: 60px !important; - /* ✅ ทำให้ปุ่มโค้งมน */ + min-width: 32px !important; + height: 32px !important; + font-size: 15px !important; + border-radius: 60px !important; } /* ✅ ปรับขนาดชื่อเดือน-ปี */ :deep(.v-date-picker-header) { - font-size: 16px !important; + font-size: 16px !important; } -/* ✅ ปรับขนาดตัวอักษรชื่อวันในสัปดาห์ (S, M, T, W, T, F, S) */ +/* ✅ ปรับขนาดตัวอักษรชื่อวัน */ :deep(.v-date-picker .v-date-picker-weekdays) { - font-size: 12px !important; + font-size: 12px !important; } </style> diff --git a/src/components/GanttChart/GanttCalendar.vue b/src/components/GanttChart/GanttCalendar.vue index b3ad73a..9fb888e 100644 --- a/src/components/GanttChart/GanttCalendar.vue +++ b/src/components/GanttChart/GanttCalendar.vue @@ -1,76 +1,81 @@ <script setup> -import { ref, watch } from 'vue'; -import { format, parse } from 'date-fns'; +import { ref, watch } from "vue"; +import { format, parse } from "date-fns"; import CalendarPicker from "@/components/GanttChart/CalendarPicker.vue"; const props = defineProps({ modelValue: { type: String, - default: '1/1/2025', // เปลี่ยนเป็น format `d/M/yyyy` + default: "2025-01-01", // ✅ ใช้ "YYYY-MM-DD" เป็นค่าเริ่มต้น }, }); -const emits = defineEmits(['update:modelValue']); +const emits = defineEmits(["update:modelValue"]); const selectedDate = ref(props.modelValue); const showCalendar = ref(false); -// ✅ ฟังก์ชันแปลง `d/M/yyyy` → `YYYY-MM-DD` +// ✅ แปลงวันที่จาก "YYYY-MM-DD" เป็น `Date Object` const parseDate = (dateStr) => { - return format(parse(dateStr, "d/M/yyyy", new Date()), "yyyy-MM-dd"); + return parse(dateStr, "yyyy-MM-dd", new Date()); }; - - // ✅ ฟังก์ชันเพิ่ม/ลดวัน และอัปเดต `selectedDate` const changeDate = (days) => { - const currentDate = parse(selectedDate.value, "d/M/yyyy", new Date()); + const currentDate = parseDate(selectedDate.value); const newDate = new Date(currentDate); newDate.setDate(currentDate.getDate() + days); - selectedDate.value = format(newDate, 'd/M/yyyy'); + selectedDate.value = format(newDate, "yyyy-MM-dd"); // ✅ ใช้ "YYYY-MM-DD" }; - // ✅ เมื่อ `selectedDate` เปลี่ยน ให้ emit ค่าออกไป watch(selectedDate, (newVal) => { console.log("📅 GanttCalendar - วันที่เปลี่ยน:", newVal); - emits('update:modelValue', newVal); + emits("update:modelValue", newVal); }); -// เมื่อเปิดปฏิทิน ให้ `selectedDate` ซิงค์กับ `v-date-picker` - - -// ฟังก์ชันเปิดปฏิทิน +// ✅ ฟังก์ชันเปิดปฏิทิน const openCalendar = () => { showCalendar.value = true; console.log("📅 ปุ่ม ... ถูกกด!"); }; -// เมื่อเลือกวันที่ใน CalendarPicker จะปิด dialog +// ✅ ปิดปฏิทินเมื่อเลือกวันที่ const closeCalendarDialog = () => { showCalendar.value = false; }; - - </script> <template> <v-container> - <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="changeDate(-1)"> + <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="changeDate(-1)" + > < Back </v-btn> - <v-btn style="background-color: white; color: black; min-width: 200px; border-radius: 15px;"> + <!-- ✅ ใช้ format YYYY-MM-DD --> + <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="changeDate(1)"> + <v-btn + class="ml-2" + style="background-color: #2b2e3f; color: white; border-radius: 10px" + @click="changeDate(1)" + > Next > </v-btn> - <!-- ✅ เปลี่ยนจาก v-dialog เป็น v-menu (Popover) --> + <!-- ✅ ใช้ v-menu (Popover) แทน v-dialog --> <v-menu v-model="showCalendar" :close-on-content-click="false" location="bottom"> <template v-slot:activator="{ props }"> <v-btn v-bind="props" class="ml-2 popover-btn"> @@ -78,29 +83,24 @@ const closeCalendarDialog = () => { </v-btn> </template> - <!-- ✅ ปรับขนาด Popover และใช้ CalendarPicker --> + <!-- ✅ ใช้ CalendarPicker ที่รองรับ YYYY-MM-DD --> <v-card class="pb-12 popover-card"> <CalendarPicker v-model="selectedDate" @update:modelValue="closeCalendarDialog" /> </v-card> </v-menu> - </v-col> </v-container> </template> <style scoped> -/* ✅ ปรับแต่งปุ่ม "..." */ .popover-btn { background-color: white; color: black; border-radius: 10px; } -/* ✅ ปรับแต่งขนาดและขอบโค้งของ Popover */ .popover-card { border-radius: 25px !important; - /* ✅ กำหนดขอบโค้ง */ - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); background-color: white; } diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index a732f38..47c20fa 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -1,6 +1,5 @@ <template> <div class="gantt-container"> - <h2 class="gantt-title">ตารางคิวการผลิต</h2> <div class="gantt-header"> <!-- Calendar: เชื่อมกับ selectedDate --> <GanttCalendar v-model:modelValue="selectedDate" /> @@ -22,44 +21,24 @@ <!-- Rows: แสดงเครื่องจักรจาก Machine Store --> <div class="rows"> - <div - v-for="machine in machineStore.machines" - :key="machine.MachineID" - class="row" - :data-machine-id="machine.MachineID" - @dragover.prevent="onDragOver($event)" - @drop="onDrop($event, machine.MachineID)" - > + <div v-for="machine in machineStore.machines" :key="machine.MachineID" class="row" + :data-machine-id="machine.MachineID" @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> + <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)" - > + <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)" - > + <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 ด้านขวา --> @@ -71,7 +50,8 @@ <!-- Ghost Queue (ขณะลาก/resize) --> <div v-if="ghostQueue" class="drag-ghost" :style="ghostStyle"> - {{ ghostQueue.orderID }} ({{ getTimeString(ghostQueue.startTime) }} - {{ getTimeString(ghostQueue.finishTime) }}) + {{ ghostQueue.orderID }} ({{ getTimeString(ghostQueue.startTime) }} - {{ getTimeString(ghostQueue.finishTime) + }}) </div> <v-divider :thickness="7"></v-divider> @@ -79,13 +59,8 @@ <!-- 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)" - > + <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 @@ -100,22 +75,12 @@ <v-icon>mdi-plus</v-icon> </v-btn> </div> - <AddQueueDialog - :visible="showAddDialog" - :queueItem="selectedQueueItem" - @close="closeAddQueueDialog" - @add="handleAddQueue" - @edit="handleEditQueue" - /> + <AddQueueDialog :visible="showAddDialog" :queueItem="selectedQueueItem" :currentPage="currentPage" + @close="closeAddQueueDialog" /> </div> - <OrderDialog - v-if="selectedQueueItem && !showAddDialog" - :queueItem="selectedQueueItem" - :color="getQueueColor(selectedQueueItem.orderID)" - @close="closeQueueDialog" - @edit="openAddQueueDialogForEdit" - @delete="deleteQueueItem" - /> + <OrderDialog v-if="selectedQueueItem && !showAddDialog" :queueItem="selectedQueueItem" + :color="getQueueColor(selectedQueueItem.orderID)" @close="closeQueueDialog" @edit="openAddQueueDialogForEdit" + @delete="handleDelete" /> </div> </div> </template> @@ -223,28 +188,30 @@ function openAddQueueDialog() { selectedQueueItem.value = null; showAddDialog.value = true; } -function deleteQueueItem(item: any) { - queueStore.removeQueue(item.QueueID); - closeQueueDialog(); -} + function openAddQueueDialogForEdit(queueItem: any) { selectedQueueItem.value = { ...queueItem }; showAddDialog.value = true; } -function closeAddQueueDialog() { - showAddDialog.value = false; - selectedQueueItem.value = null; + +function handleDelete(queueItem: { QueueID: number; }) { + if (!queueItem) return; + + queueStore + .deleteQueue(queueItem.QueueID) + .then(() => { + console.log(`✅ Queue ID ${queueItem.QueueID} ถูกลบสำเร็จ`); + closeAddQueueDialog(); + }) + .catch((error) => console.error(`❌ Error deleting queue:`, error)); } -async function handleAddQueue(newQueueItem: any) { - await queueStore.addQueue(newQueueItem); + +function closeAddQueueDialog() { showAddDialog.value = false; -} -async function handleEditQueue(updatedQueueItem: any) { - await queueStore.editQueue(updatedQueueItem.QueueID, updatedQueueItem); selectedQueueItem.value = null; - showAddDialog.value = false; } + function formatHour(hour: number): string { return (hour < 10 ? '0' + hour : hour) + ':00'; } @@ -396,13 +363,14 @@ function onDragOverGlobal(event: DragEvent) { if (closestRow) { // ใช้ dataset เพื่อดึง machine id - const machineIDString = closestRow.dataset.machineId; + const machineIDString = (closestRow as HTMLElement).dataset.machineId; if (machineIDString) { - ghostQueue.value.machine = { MachineID: parseInt(machineIDString, 10) }; + ghostQueue.value.machineID = parseInt(machineIDString, 10); } - ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; + ghostStyle.top = (closestRow as HTMLElement).getBoundingClientRect().top + 'px'; } + ghostStyle.left = snappedLeft + 'px'; ghostStyle.width = snappedWidth + 'px'; } @@ -431,22 +399,35 @@ async function onDragEnd(event: DragEvent, item: any) { } async function onDrop(event: DragEvent, newMachineID: number) { event.preventDefault(); - if (draggingQueue.value && ghostQueue.value) { - draggingQueue.value.startTime = ghostQueue.value.startTime; - draggingQueue.value.finishTime = ghostQueue.value.finishTime; - draggingQueue.value.machine = { MachineID: newMachineID }; + + if (!draggingQueue.value || !ghostQueue.value) return; + + console.log(`🔄 Dropped Queue ${draggingQueue.value.QueueID} to Machine ${newMachineID}`); + + draggingQueue.value.startTime = ghostQueue.value.startTime; + draggingQueue.value.finishTime = ghostQueue.value.finishTime; + draggingQueue.value.machineID = newMachineID; // ✅ อัปเดต MachineID + + try { await queueStore.updateQueue(draggingQueue.value.QueueID, { startTime: draggingQueue.value.startTime, finishTime: draggingQueue.value.finishTime, - machine: { MachineID: newMachineID, name: '' } + MachineID: newMachineID, // ✅ ส่งค่าไป Backend }); + + console.log(`✅ Queue ${draggingQueue.value.QueueID} updated to Machine ${newMachineID}`); + } catch (error) { + console.error(`❌ Error updating queue:`, error); } + ghostQueue.value = null; ghostStyle.display = 'none'; draggingQueue.value = null; } + function onResizeStart(event: MouseEvent, item: any, direction: string) { event.preventDefault(); + ghostQueue.value = { ...item }; resizingQueue.value = item; resizeDirection.value = direction; document.addEventListener('mousemove', onResizing); @@ -471,23 +452,12 @@ function onResizing(event: MouseEvent) { if (resizeDirection.value === 'left') { if (newTimeDecimal >= endDec) return; const newStartStr = datePart + ' ' + decimalToTime(newTimeDecimal); - // อัปเดต ghostQueue ให้แสดงผลแบบ real-time ghostQueue.value.startTime = newStartStr; } else if (resizeDirection.value === 'right') { if (newTimeDecimal <= startDec) return; const newEndStr = datePart + ' ' + decimalToTime(newTimeDecimal); ghostQueue.value.finishTime = newEndStr; } - // อัปเดต ghostStyle ตาม ghostQueue ใหม่ - const start = getTimeString(ghostQueue.value.startTime); - const end = getTimeString(ghostQueue.value.finishTime); - const startDecimal = timeToDecimal(start); - const endDecimal = timeToDecimal(end); - const totalHours = hours.value.length; - const leftPercent = ((startDecimal - timeToDecimal(startTime.value)) / totalHours) * 100; - const widthPercent = ((endDecimal - startDecimal) / totalHours) * 100; - ghostStyle.left = `${leftPercent}%`; - ghostStyle.width = `${widthPercent}%`; } async function onResizeEnd() { if (!resizingQueue.value) return; @@ -538,4 +508,8 @@ onMounted(() => { onBeforeUnmount(() => { document.removeEventListener('click', onDocumentClick); }); + +function closeDialog() { + throw new Error('Function not implemented.'); +} </script> diff --git a/src/components/GanttChart/OrderDialog.vue b/src/components/GanttChart/OrderDialog.vue index 39e0fdd..8f2dacf 100644 --- a/src/components/GanttChart/OrderDialog.vue +++ b/src/components/GanttChart/OrderDialog.vue @@ -2,7 +2,7 @@ <div class="order-dialog-overlay"> <div class="order-dialog" :style="{ backgroundColor: color }"> <div class="dialog-header"> - <h3 class="order-title">QueueID: {{ queueItem.queueID }}</h3> + <h3 class="order-title">QueueID: {{ queueItem.QueueID }}</h3> <div class="dialog-buttons"> <button @click="$emit('edit', queueItem)" class="icon-button"> <span class="mdi mdi-pencil"></span> diff --git a/src/components/GanttChart/scheduleCalculator.js b/src/components/GanttChart/scheduleCalculator.js index 4adf107..74f0a3f 100644 --- a/src/components/GanttChart/scheduleCalculator.js +++ b/src/components/GanttChart/scheduleCalculator.js @@ -286,7 +286,7 @@ function roundDownToHalfHour(dt) { for (const machineKey of Object.keys(schedule)) { for (const task of schedule[machineKey]) { queueItems.push({ - queueID: idCounter++, + QueueID: idCounter++, machineID: machineKey, orderID: task.orderID, pageNumber: 1, diff --git a/src/components/OrderItem.vue b/src/components/OrderItem.vue deleted file mode 100644 index 763e0fa..0000000 --- a/src/components/OrderItem.vue +++ /dev/null @@ -1,75 +0,0 @@ -<template> - <div class="order" :style="getOrderStyle(order)"> - <div class="resize-handle left" @mousedown="$emit('resize-start', order, 'left')"></div> - <div class="order-content" draggable="true" @dragstart="$emit('drag-start', order)" @dragend="$emit('drag-end', order)"> - {{ order.name }} ({{ order.start }} - {{ order.end }}) - </div> - <div class="resize-handle right" @mousedown="$emit('resize-start', order, 'right')"></div> - </div> -</template> - -<script> -export default { - props: { - order: Object, - startTime: String, - endTime: String - }, - methods: { - timeToDecimal(timeStr) { - const [hours, minutes] = timeStr.split(':').map(Number); - return hours + minutes / 60; - }, - getOrderStyle(order) { - const startDecimal = this.timeToDecimal(order.start); - const endDecimal = this.timeToDecimal(order.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: order.color || '#4caf50' - }; - } - } -}; -</script> - -<style scoped> -.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; -} - -.resize-handle.right { - right: 0; -} -</style> diff --git a/src/router/index.ts b/src/router/index.ts index 5c0889f..769a12e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -46,6 +46,22 @@ const router = createRouter({ requireAuth: true } }, + { + path: '/manage-machines', + name: 'manage-machines', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + components: { + default: () => import('../views/MachineManagementView.vue'), + menu: () => import('../components/Navbar/MainMenu.vue'), + header: () => import('../components/Navbar/MainAppBar.vue') + }, + meta: { + layout: 'MainLayout', + requireAuth: true + } + }, { path: '/login', name: 'login', diff --git a/src/services/http.ts b/src/services/http.ts index b4023a3..6997076 100644 --- a/src/services/http.ts +++ b/src/services/http.ts @@ -31,7 +31,7 @@ instance.interceptors.response.use( async function (res) { console.log('response interceptor') - await delay(1) + await delay(0.1) return res }, function (error) { diff --git a/src/services/machine.ts b/src/services/machine.ts new file mode 100644 index 0000000..a4fa58a --- /dev/null +++ b/src/services/machine.ts @@ -0,0 +1,41 @@ +import http from './http'; +import type { Machine } from '@/types/Machine'; + +/** + * ดึงรายการ Machine ทั้งหมดจาก backend + */ +export async function listMachines(): Promise<Machine[]> { + const { data } = await http.get('/machines'); + return data; +} + +/** + * ดึงข้อมูล Machine รายการเดียวโดยใช้ MachineID + */ +export async function getMachine(id: number): Promise<Machine> { + const { data } = await http.get(`/machines/${id}`); + return data; +} + +/** + * สร้าง Machine ใหม่ + */ +export async function createMachine(payload: Partial<Machine>): Promise<Machine> { + const { data } = await http.post('/machines', payload); + return data; +} + +/** + * อัปเดต Machine ตาม MachineID + */ +export async function updateMachine(id: number, payload: Partial<Machine>): Promise<Machine> { + const { data } = await http.put(`/machines/${id}`, payload); + return data; +} + +/** + * ลบ Machine ตาม MachineID + */ +export async function deleteMachine(id: number): Promise<void> { + await http.delete(`/machines/${id}`); +} diff --git a/src/services/order.ts b/src/services/order.ts new file mode 100644 index 0000000..452eb65 --- /dev/null +++ b/src/services/order.ts @@ -0,0 +1,45 @@ +// /src/services/order.service.ts + +import http from './http'; +import type { Order } from '@/types/Order'; + +/** + * ดึงรายการ Order ทั้งหมดจาก backend + */ +export async function listOrders(): Promise<Order[]> { + const { data } = await http.get('/orders'); + console.log("API Response for Orders:", data); // เช็คข้อมูลที่ได้จาก API + return data; +} + + +/** + * ดึงข้อมูล Order รายการเดียวโดยใช้ OrderID + */ +export async function getOrder(id: number): Promise<Order> { + const { data } = await http.get(`/orders/${id}`); + return data; +} + +/** + * สร้าง Order ใหม่ + */ +export async function createOrder(payload: Partial<Order>): Promise<Order> { + const { data } = await http.post('/orders', payload); + return data; +} + +/** + * อัปเดต Order ตาม OrderID + */ +export async function updateOrder(id: number, payload: Partial<Order>): Promise<Order> { + const { data } = await http.put(`/orders/${id}`, payload); + return data; +} + +/** + * ลบ Order ตาม OrderID + */ +export async function deleteOrder(id: number): Promise<void> { + await http.delete(`/orders/${id}`); +} diff --git a/src/services/queue.ts b/src/services/queue.ts index 710d5ca..28cdd75 100644 --- a/src/services/queue.ts +++ b/src/services/queue.ts @@ -1,92 +1,41 @@ -import axios from 'axios'; -import type { QueueItem } from '@/types/QueueItem'; - -const makeQueue = async (): Promise<QueueItem[] | undefined> => { - try { - // เรียก API - const response = await axios.post('http://127.0.0.1:9000/run-simulation'); - console.log('📌 Response:', response); - - const queueData = response.data.production_log; - console.log('this is queueData queueservice', queueData); - - if (!Array.isArray(queueData)) { - console.error('❌ Error: Invalid queue data format'); - return undefined; - } - - // ✅ แปลงข้อมูลเป็น JSON String แล้ว Parse กลับมาเป็น Object ใหม่ - const queueItems: QueueItem[] = JSON.parse( - JSON.stringify(queueData.map(q => ({ - queueID: q.queueID, - machineID: q.machineID, - orderID: q.orderID, - pageNumber: q.pageNumber ?? 1, // ถ้าไม่มี ให้ใช้ค่า default เป็น 1 - startTime: q.startTime, - finishTime: q.finishTime, - status: q.status, - bottleSize: q.bottleSize, - producedQuantity: q.producedQuantity, - }))) - ); - - return queueItems; // ✅ คืนค่าเป็น `QueueItem[]` - } catch (error: any) { - console.error('❌ Error:', error.message || error); - return undefined; - } -}; - -export default { makeQueue }; - - - -export class QueueService { - private queue: Queue[] = []; - - // ดึงรายการคิวทั้งหมด - getQueue(): Queue[] { - return this.queue; - } - - // ค้นหารายการคิวโดยใช้ queueID - getQueueById(queueID: number): Queue | undefined { - return this.queue.find(item => item.queueID === queueID); - } - - // เพิ่มรายการใหม่เข้า queue - addQueueItem(item: Queue): void { - this.queue.push(item); - } - - // อัปเดตรายการ queue ที่มีอยู่ - updateQueueItem(updatedItem: Queue): void { - const index = this.queue.findIndex(item => item.queueID === updatedItem.queueID); - if (index !== -1) { - this.queue[index] = updatedItem; - } - } +import http from './http'; +import type { Queue, QueuePostData } from '@/types/Queue'; + +/** + * ดึงรายการ Queue ทั้งหมดจาก backend + */ +export async function listQueues(): Promise<Queue[]> { + const { data } = await http.get('/queues'); + return data; +} - // ลบรายการ queue ตาม queueID - removeQueueItem(queueID: number): void { - this.queue = this.queue.filter(item => item.queueID !== queueID); - } +/** + * ดึงข้อมูล Queue รายการเดียวโดยใช้ QueueID + */ +export async function getQueue(id: number): Promise<Queue> { + const { data } = await http.get(`/queues/${id}`); + return data; +} - // เปลี่ยนสถานะของคิว - updateQueueStatus(queueID: number, status: 'pending' | 'in-progress' | 'completed' | 'canceled'): void { - const queueItem = this.getQueueById(queueID); - if (queueItem) { - queueItem.status = status; - } - } +/** + * สร้าง Queue ใหม่ (ใช้ QueuePostData เพื่อให้โครงสร้างถูกต้อง) + */ +export async function createQueue(payload: QueuePostData): Promise<Queue> { + const { data } = await http.post('/queues', payload); + return data; +} - // คำนวณเวลาการทำงานของ queue - calculateProductionTime(queueID: number): number | null { - const queueItem = this.getQueueById(queueID); - if (queueItem && queueItem.startTime && queueItem.finishTime) { - return (queueItem.finishTime.getTime() - queueItem.startTime.getTime()) / (1000 * 60); // แปลงเป็นนาที - } - return null; - } +/** + * อัปเดต Queue ตาม QueueID (ใช้ QueuePostData) + */ +export async function updateQueue(id: number, payload: QueuePostData): Promise<Queue> { + const { data } = await http.patch(`/queues/${id}`, payload); + return data; +} +/** + * ลบ Queue ตาม QueueID + */ +export async function deleteQueue(id: number): Promise<void> { + await http.delete(`/queues/${id}`); } diff --git a/src/stores/machine.ts b/src/stores/machine.ts new file mode 100644 index 0000000..ce98548 --- /dev/null +++ b/src/stores/machine.ts @@ -0,0 +1,95 @@ +// /src/stores/machine.store.ts + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Machine } from '@/types/Machine'; +import * as machineService from '@/services/machine'; + +export const useMachineStore = defineStore('machine', () => { + const machines = ref<Machine[]>([]); + const loading = ref<boolean>(false); + const error = ref<string | null>(null); + + // ดึงรายการ Machine ทั้งหมดจาก backend + async function fetchMachines() { + loading.value = true; + error.value = null; + try { + machines.value = await machineService.listMachines(); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ดึงข้อมูล Machine รายการเดียวโดยใช้ MachineID + async function fetchMachine(id: number): Promise<Machine | null> { + loading.value = true; + error.value = null; + try { + return await machineService.getMachine(id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + // สร้าง Machine ใหม่แล้วเพิ่มเข้า state + async function addMachine(payload: Partial<Machine>) { + loading.value = true; + error.value = null; + try { + const newMachine = await machineService.createMachine(payload); + machines.value.push(newMachine); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // อัปเดต Machine ที่มีอยู่ใน state + async function editMachine(id: number, payload: Partial<Machine>) { + loading.value = true; + error.value = null; + try { + const updatedMachine = await machineService.updateMachine(id, payload); + const index = machines.value.findIndex(m => m.MachineID === id); + if (index !== -1) { + machines.value[index] = updatedMachine; + } + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ลบ Machine จาก backend และอัปเดต state + async function removeMachine(id: number) { + loading.value = true; + error.value = null; + try { + await machineService.deleteMachine(id); + machines.value = machines.value.filter(m => m.MachineID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + machines, + loading, + error, + fetchMachines, + fetchMachine, + addMachine, + editMachine, + removeMachine, + }; +}); diff --git a/src/stores/order.ts b/src/stores/order.ts new file mode 100644 index 0000000..00d00e2 --- /dev/null +++ b/src/stores/order.ts @@ -0,0 +1,100 @@ +// /src/stores/order.store.ts + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Order } from '@/types/Order'; +import * as orderService from '@/services/order'; + +export const useOrderStore = defineStore('order', () => { + const orders = ref<Order[]>([]); + const loading = ref<boolean>(false); + const error = ref<string | null>(null); + + // ดึงรายการ Order ทั้งหมดจาก backend + async function fetchOrders() { + loading.value = true; + error.value = null; + try { + const response = await orderService.listOrders(); + console.log("Fetched orders from API:", response); // ✅ ตรวจสอบ API Response + orders.value = response; + console.log("Orders in store after update:", orders.value); // ✅ ตรวจสอบว่าถูกอัปเดตหรือไม่ + } catch (e: any) { + error.value = e.message || "Unknown Error"; + } finally { + loading.value = false; + } + } + + + + // ดึงข้อมูล Order รายการเดียวโดยใช้ OrderID + async function fetchOrder(id: number): Promise<Order | null> { + loading.value = true; + error.value = null; + try { + return await orderService.getOrder(id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + // สร้าง Order ใหม่แล้วเพิ่มเข้า state + async function addOrder(payload: Partial<Order>) { + loading.value = true; + error.value = null; + try { + const newOrder = await orderService.createOrder(payload); + orders.value.push(newOrder); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // อัปเดต Order ที่มีอยู่ใน state + async function editOrder(id: number, payload: Partial<Order>) { + loading.value = true; + error.value = null; + try { + const updatedOrder = await orderService.updateOrder(id, payload); + const index = orders.value.findIndex(o => o.OrderID === id); + if (index !== -1) { + orders.value[index] = updatedOrder; + } + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ลบ Order จาก backend และอัปเดต state + async function removeOrder(id: number) { + loading.value = true; + error.value = null; + try { + await orderService.deleteOrder(id); + orders.value = orders.value.filter(o => o.OrderID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + orders, + loading, + error, + fetchOrders, + fetchOrder, + addOrder, + editOrder, + removeOrder, + }; +}); diff --git a/src/stores/queue.ts b/src/stores/queue.ts index 370d79c..f3b6e0f 100644 --- a/src/stores/queue.ts +++ b/src/stores/queue.ts @@ -1,41 +1,134 @@ import { defineStore } from 'pinia'; -import QueueService from '@/services/queue'; -import type { QueueItem } from '@/types/QueueItem' import { ref } from 'vue'; -import { useLoadingStore } from './loading'; - -// ฟังก์ชัน Sleep ให้รอจริง ๆ -function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} +import type { Queue, QueuePostData } from '@/types/Queue'; +import * as queueService from '@/services/queue'; export const useQueueStore = defineStore('queue', () => { - const Queues = ref<QueueItem[]>([]); - const loadingStore = useLoadingStore(); + const queues = ref<Queue[]>([]); + const loading = ref<boolean>(false); + const error = ref<string | null>(null); + + // **ดึงรายการ Queue ทั้งหมดจาก backend** + async function fetchQueues() { + loading.value = true; + error.value = null; + try { + console.log("📡 Fetching Queues from Backend..."); + const response = await queueService.listQueues(); + console.log("✅ Queues Response from Backend:", response); + queues.value = response; + } catch (e: any) { + console.error("❌ Error fetching queues:", e.response?.data || e.message); + error.value = e.message || "Unknown Error"; + } finally { + loading.value = false; + } + } - const makeQueue = async () => { + // **ดึง Queue รายการเดียวจาก backend** + async function getQueue(id: number): Promise<Queue> { + loading.value = true; + error.value = null; try { - // ✅ รอ 3 วินาทีจริง ๆ ก่อนเริ่มโหลด - loadingStore.isLoading = true; - console.log("Waiting for 3 seconds..."); - await sleep(3000); + console.log(`📡 Fetching Queue ID: ${id} from Backend...`); + const queue = await queueService.getQueue(id); + console.log("✅ Queue Data Received:", queue); + return queue; + } catch (e: any) { + console.error(`❌ Error fetching Queue ID ${id}:`, e.response?.data || e.message); + error.value = e.message || "Unknown Error"; + throw e; + } finally { + loading.value = false; + } + } - console.log("Fetching queue data..."); - const queueData = await QueueService.makeQueue(); // ✅ รอ API + // **เพิ่ม Queue ลง Database (ใช้ QueuePostData)** + async function addQueue(newQueue: QueuePostData) { + loading.value = true; + error.value = null; + try { + console.log("➕ Adding Queue:", newQueue); + const createdQueue = await queueService.createQueue(newQueue); + queues.value.push(createdQueue); + console.log("✅ Queue Added:", createdQueue); + } catch (e: any) { + console.error("❌ Error adding queue:", e.response?.data || e.message); + error.value = e.message || "Unknown Error"; + } finally { + loading.value = false; + } + } - if (queueData && Array.isArray(queueData)) { - console.log("Appending data to Queues..."); - Queues.value = [...queueData]; // ✅ ใช้ spread แทน forEach() + // **อัปเดต Queue (ใช้ QueuePostData)** + async function updateQueue(id: number, updatedData: QueuePostData) { + if (!id) { + console.error("❌ queueID is undefined! Cannot update."); + return; + } + + loading.value = true; + error.value = null; + try { + console.log(`🛠 Updating Queue ID: ${id}...`); + const updatedQueue = await queueService.updateQueue(id, updatedData); + const index = queues.value.findIndex(q => q.QueueID === id); + if (index !== -1) { + queues.value[index] = updatedQueue; + console.log(`✅ Queue ID: ${id} Updated Successfully`, updatedQueue); } else { - console.log("No queue data received."); + console.warn(`⚠️ Queue ID: ${id} Not Found in Store!`); } + } catch (e: any) { + console.error(`❌ Error updating Queue ID ${id}:`, e.response?.data || e.message); + error.value = e.message || "Unknown Error"; + } finally { + loading.value = false; + } + } - loadingStore.isLoading = false; // ✅ ปิด loading เมื่อโหลดเสร็จ - } catch (error) { - console.error('❌ Error fetching queue:', error); - loadingStore.isLoading = false; // ปิด loading ถ้า error + // **ลบ Queue** + async function deleteQueue(id: number) { + loading.value = true; + error.value = null; + try { + console.log(`🗑 Deleting Queue ID: ${id}...`); + await queueService.deleteQueue(id); + // ลบ Queue ออกจาก Store + queues.value = queues.value.filter(q => q.QueueID !== id); + console.log(`✅ Queue ID: ${id} Deleted Successfully`); + } catch (e: any) { + console.error(`❌ Error deleting Queue ID ${id}:`, e.response?.data || e.message); + error.value = e.message || "Unknown Error"; + } finally { + loading.value = false; } - }; + } + + function convertQueueToPostData(queue: Queue): QueuePostData { + return { + startTime: new Date(queue.startTime).toISOString(), + finishTime: new Date(queue.finishTime).toISOString(), + status: queue.status, + bottleSize: queue.bottleSize, + producedQuantity: queue.producedQuantity, + MachineID: queue.machine?.MachineID ?? 0, // ใช้ MachineID จาก object + PageID: queue.page?.PageID ?? 0, + OrderID: queue.order?.OrderID ?? 0, + EmployeeIds: queue.employees.map(emp => emp.EmployeeID), // แปลง employees เป็นแค่ array ของ ID + }; + } + - return { Queues, makeQueue }; + return { + queues, + loading, + error, + fetchQueues, + getQueue, + convertQueueToPostData, + addQueue, + updateQueue, + deleteQueue, + }; }); diff --git a/src/types/Machine.ts b/src/types/Machine.ts new file mode 100644 index 0000000..7cde028 --- /dev/null +++ b/src/types/Machine.ts @@ -0,0 +1,12 @@ +export interface Machine { + MachineID: number; // ใช้ตาม entity (ตัว Q ใหญ่) + name: string; // ชื่อเครื่องจักร + type: string; // ประเภทเครื่องจักร + lastMaintenanceDate?: string; // วันที่ซ่อมบำรุงล่าสุด (ISO string) + status: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE'; // สถานะเครื่องจักร + notes?: string; // หมายเหตุเพิ่มเติม + // หากต้องการ include ความสัมพันธ์ + // machineDetails?: MachineDetail[]; + // queues?: Queue[]; + } + \ No newline at end of file diff --git a/src/types/Order.ts b/src/types/Order.ts new file mode 100644 index 0000000..b592e38 --- /dev/null +++ b/src/types/Order.ts @@ -0,0 +1,13 @@ +export interface Order { + OrderID: number; // ใช้ Q ใหญ่ตาม entity + customer: { CustomerID: number }; // ความสัมพันธ์กับ Customer + orderPriorities?: { OrderPriorityID: number }[]; // ความสัมพันธ์กับ OrderPriority (ถ้ามี) + orderDetails?: { OrderDetailID: number }[]; // ความสัมพันธ์กับ OrderDetail (ถ้ามี) + queues?: { QueueID: number }[]; // ความสัมพันธ์กับ Queue (ถ้ามี) + OrderDate: string; // วันที่สร้างออเดอร์ (ISO string) + status: string; // สถานะออเดอร์ + totalPriceall: number; // ราคารวมทั้งหมด + quantity: number; // จำนวนสินค้าทั้งหมด + startDate?: string; // วันที่เริ่มต้น (ISO string) + finishDate?: string; // วันที่สิ้นสุด (ISO string) + } \ No newline at end of file diff --git a/src/types/Queue.ts b/src/types/Queue.ts index ca35bf6..edfa6e6 100644 --- a/src/types/Queue.ts +++ b/src/types/Queue.ts @@ -1,11 +1,39 @@ -export type Queue = { - queueID: number | string; - machineID: number | string; - orderID: number | string; +export interface Queue { pageID: number; - startTime: Date; - finishTime: Date; - status: 'pending' | 'in-progress' | 'completed' | 'canceled'; + orderID: null; + machineID: null; + QueueID: number; + machine?: { + MachineID: number; + name: string; + }; + page?: { + PageID: number; + }; + order?: { + OrderID: number; + }; + employees: Employee[]; + startTime: string | Date; + finishTime: string | Date; + status: string; bottleSize: string; producedQuantity: number; -}; \ No newline at end of file +} + +export interface Employee { + EmployeeID: number; + name: string; +} + +export interface QueuePostData { + startTime: string; + finishTime: string; + status: string; + bottleSize: string; + producedQuantity: number; + MachineID: number; + PageID: number; + OrderID: number; + EmployeeIds: number[]; +} diff --git a/src/types/QueueItem.ts b/src/types/QueueItem.ts deleted file mode 100644 index b156202..0000000 --- a/src/types/QueueItem.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type QueueItem = { - queueID: number; - machineID: string; - orderID: number; - pageNumber: number; - startTime: string; - finishTime: string; - status: string; - bottleSize: string; - producedQuantity: number; -} \ No newline at end of file diff --git a/src/views/MachineManagementView.vue b/src/views/MachineManagementView.vue new file mode 100644 index 0000000..5ff3aee --- /dev/null +++ b/src/views/MachineManagementView.vue @@ -0,0 +1,118 @@ +<script lang="ts" setup> +import { nextTick, onMounted, ref } from 'vue' +import type { VForm } from 'vuetify/components' +import type { Machine } from '@/types/Machine' +import { useMachineStore } from '@/stores/machine' + +const machineStore = useMachineStore() +const headers = [ + { title: 'ID', key: 'MachineID', value: 'MachineID' }, + { title: 'Name', key: 'name', value: 'name' }, + { title: 'Type', key: 'type', value: 'type' }, + { title: 'Status', key: 'status', value: 'status' }, + { title: 'Last Maintenance', key: 'lastMaintenanceDate', value: 'lastMaintenanceDate' }, + { title: 'Actions', key: 'actions', sortable: false } +] + +const form = ref(false) +const refForm = ref<VForm | null>(null) +const dialog = ref(false) +const dialogDelete = ref(false) +const loading = ref(false) + + +async function editItem(item: Machine) { + if (!item.MachineID) return + await machineStore.fetchMachine(item.MachineID) + dialog.value = true +} + +async function deleteItem(item: Machine) { + if (!item.MachineID) return + await machineStore.fetchMachine(item.MachineID) + dialogDelete.value = true +} + + +onMounted(async () => { + await machineStore.fetchMachines() +}) +</script> + +<template> + <div> + <v-data-table :headers="headers" :items="machineStore.machines"> + <template v-slot:top> + <v-toolbar flat style="background-color: rgb(247, 156, 82)"> + <v-toolbar-title style="color: white; font-weight: bold; font-size: 30px">Machines</v-toolbar-title> + <v-divider class="mx-4" inset vertical></v-divider> + <v-spacer></v-spacer> + + <v-dialog v-model="dialog" persistent width="800"> + <template v-slot:activator="{ props }"> + <v-btn prepend-icon="mdi-plus" v-bind="props" style="color: white; background-color: green"> + New Machine + </v-btn> + </template> + <v-card> + <v-card-title> + <span class="text-h5">Machine Details</span> + </v-card-title> + <v-card-text> + <v-form ref="refForm" v-model="form" @submit.prevent="save"> + <v-container> + <v-row> + <v-col cols="12" sm="6"> + <v-text-field v-model="machineStore.editedMachine.name" label="Machine Name" + :rules="[(v) => !!v || 'Field is required']"></v-text-field> + </v-col> + <v-col cols="12" sm="6"> + <v-text-field v-model="machineStore.editedMachine.type" label="Machine Type" + :rules="[(v) => !!v || 'Field is required']"></v-text-field> + </v-col> + <v-col cols="12" sm="6"> + <v-select v-model="machineStore.editedMachine.status" :items="['ACTIVE', 'INACTIVE', 'MAINTENANCE']" + label="Status"></v-select> + </v-col> + <v-col cols="12" sm="6"> + <v-text-field v-model="machineStore.editedMachine.lastMaintenanceDate" label="Last Maintenance Date" + type="date"></v-text-field> + </v-col> + </v-row> + </v-container> + </v-form> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn color="blue-darken-1" variant="text" @click="closeDialog">Close</v-btn> + <v-btn color="blue-darken-1" variant="text" @click="save">Save</v-btn> + </v-card-actions> + </v-card> + </v-dialog> + + <!-- Delete Confirmation Dialog --> + <v-dialog v-model="dialogDelete" max-width="500px"> + <v-card> + <v-card-title class="text-h5">Are you sure you want to delete this machine?</v-card-title> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn color="blue-darken-1" variant="text" @click="dialogDelete = false">Cancel</v-btn> + <v-btn color="blue-darken-1" variant="text" @click="deleteItemConfirm">Delete</v-btn> + <v-spacer></v-spacer> + </v-card-actions> + </v-card> + </v-dialog> + </v-toolbar> + </template> + + <template v-slot:[`item.actions`]="{ item }"> + <v-icon size="small" class="me-2" @click="editItem(item)"> mdi-pencil </v-icon> + <v-icon size="small" @click="deleteItem(item)"> mdi-delete </v-icon> + </template> + + <template v-slot:no-data> + <v-btn color="primary" @click="machineStore.fetchMachines">Reset</v-btn> + </template> + </v-data-table> + </div> +</template> -- GitLab