diff --git a/src/components/GanttChart/AddQueueDialog.vue b/src/components/GanttChart/AddQueueDialog.vue index 1646b565a79b1014d8f870954049d36418f70555..428f821482693684242fae3602f84a7946cc324b 100644 --- a/src/components/GanttChart/AddQueueDialog.vue +++ b/src/components/GanttChart/AddQueueDialog.vue @@ -1,79 +1,108 @@ <template> - <transition name="fade"> <div class="add-queue-dialog-overlay" v-if="visible"> - <div class="add-queue-dialog"> - <h3 class="white-text">{{ isEditing ? "Edit Queue" : "Add New Queue" }}</h3> - <form @submit.prevent="handleSubmit"> - <div class="form-group"> - <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> - </div> - - <div class="form-group"> - <label class="white-text">Order ID:</label> - <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> - - <div class="form-group"> - <label class="white-text">Start Time:</label> - <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 v-model="form.finishTime" class="border-input" type="datetime-local" required /> - </div> - - <div class="form-group"> - <label class="white-text">Status:</label> - <select v-model="form.status" class="border-input" required> - <option value="Pending">Pending</option> - <option value="Process">Process</option> - <option value="Done">Done</option> - </select> - </div> - - <div class="form-group"> - <label class="white-text">Bottle Size:</label> - <select v-model="form.bottleSize" class="border-input" required> - <option value="600 ml">600 ml</option> - <option value="1500 ml">1500 ml</option> - <option value="350 ml">350 ml</option> - </select> - </div> - - <div class="form-group"> - <label class="white-text">Produced Quantity:</label> - <input v-model.number="form.producedQuantity" class="border-input" type="number" required /> - </div> - - <div class="dialog-buttons"> - <button type="submit" class="primary-btn"> - {{ isEditing ? "Save Changes" : "Add" }} - </button> - <button type="button" @click="closeDialog" class="secondary-btn">Cancel</button> - </div> - </form> + <div class="add-queue-dialog"> + <h3 class="white-text">{{ isEditing ? "Edit Queue" : "Add New Queue" }}</h3> + <form @submit.prevent="handleSubmit"> + <div class="form-group"> + <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> + </div> + + <div class="form-group"> + <label class="white-text">Order ID:</label> + <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> + + <!-- เพิ่มช่อง Item Type --> + <div class="form-group"> + <label class="white-text">Item Type:</label> + <select v-model="form.itemType" class="border-input" required> + <option v-for="type in itemTypeOptions" :key="type.value" :value="type.value"> + {{ type.label }} + </option> + </select> + </div> + + <!-- เพิ่มช่อง Item --> + <div class="form-group"> + <label class="white-text">Item:</label> + <select v-model.number="form.itemID" class="border-input" required> + <option value="" disabled> + เลือก {{ form.itemType === 'PRODUCT' ? 'Product' : 'Material' }} + </option> + <option + v-for="item in form.itemType === 'PRODUCT' ? products : materials" + :key="item.id || item.ProductID || item.MaterialID" + :value="item.id || item.ProductID || item.MaterialID" + > + <!-- ถ้าเป็น PRODUCT ให้แสดง brand + size, ถ้า MATERIAL แสดง name --> + {{ form.itemType === 'PRODUCT' ? (item.brand + ' ' + item.size) : ((item.name + ' ' + item.size + ' ' + item.size)) }} + </option> + </select> + </div> + + <div class="form-group"> + <label class="white-text">Start Time:</label> + <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 v-model="form.finishTime" class="border-input" type="datetime-local" required /> + </div> + + <div class="form-group"> + <label class="white-text">Status:</label> + <select v-model="form.status" class="border-input" required> + <option value="Pending">Pending</option> + <option value="Process">Process</option> + <option value="Done">Done</option> + </select> + </div> + + <div class="form-group"> + <label class="white-text">Bottle Size:</label> + <select v-model="form.bottleSize" class="border-input" required> + <option value="600 ml">600 ml</option> + <option value="1500 ml">1500 ml</option> + <option value="350 ml">350 ml</option> + </select> + </div> + + <div class="form-group"> + <label class="white-text">Produced Quantity:</label> + <input v-model.number="form.producedQuantity" class="border-input" type="number" min="0" required /> + </div> + + <div class="dialog-buttons"> + <button type="submit" class="primary-btn"> + {{ isEditing ? "Save Changes" : "Add" }} + </button> + <button type="button" @click="closeDialog" class="secondary-btn">Cancel</button> + </div> + </form> + </div> </div> - </div> - </transition> </template> <script setup lang="ts"> import { reactive, ref, watch, onMounted, computed } from 'vue'; +import { storeToRefs } from 'pinia'; import { useMachineStore } from '@/stores/machine'; import { useOrderStore } from '@/stores/order'; import { useQueueStore } from '@/stores/queue'; import { usePageContextStore } from '@/stores/pageContext'; +import { useProductStore } from '@/stores/product'; +import { useMaterialStore } from '@/stores/material'; import type { Queue } from '@/types/Queue'; const props = defineProps<{ @@ -90,13 +119,24 @@ const pageContext = usePageContextStore(); const machineStore = useMachineStore(); const orderStore = useOrderStore(); const queueStore = useQueueStore(); +const productStore = useProductStore(); +const materialStore = useMaterialStore(); const machines = computed(() => machineStore.machines); const orders = computed(() => orderStore.orders); +const products = computed(() => productStore.products); +const materials = computed(() => materialStore.materials); + +const itemTypeOptions = [ + { label: 'Product', value: 'PRODUCT' }, + { label: 'Material', value: 'MATERIAL' }, +]; onMounted(async () => { await machineStore.fetchMachines(); await orderStore.fetchOrders(); + await productStore.fetchProducts(); + await materialStore.fetchMaterials(); }); const isEditing = ref(false); @@ -104,6 +144,8 @@ const isEditing = ref(false); const form = reactive({ machineID: null as number | null, orderID: null as number | null, + itemType: 'PRODUCT' as 'PRODUCT' | 'MATERIAL', + itemID: null as number | null, startTime: new Date().toISOString().slice(0, 16), finishTime: new Date().toISOString().slice(0, 16), status: "Pending", @@ -134,6 +176,8 @@ watch( isEditing.value = true; form.machineID = newQueueItem.machineID ?? null; form.orderID = newQueueItem.orderID ?? null; + form.itemType = newQueueItem.itemType; + form.itemID = newQueueItem.itemID ?? null; form.startTime = formatDateTime(newQueueItem.startTime); form.finishTime = formatDateTime(newQueueItem.finishTime); form.status = newQueueItem.status; @@ -156,6 +200,8 @@ function closeDialog() { function resetForm() { form.machineID = null; form.orderID = null; + form.itemType = 'PRODUCT'; + form.itemID = null; form.startTime = new Date().toISOString().slice(0, 16); form.finishTime = new Date().toISOString().slice(0, 16); form.status = "Pending"; @@ -166,7 +212,17 @@ function resetForm() { } function handleSubmit() { - if (!form.machineID || !form.orderID || !form.startTime || !form.finishTime || !form.status || !form.bottleSize || !form.producedQuantity) { + if ( + !form.machineID || + !form.orderID || + !form.pageID || + !form.itemID || + !form.startTime || + !form.finishTime || + !form.status || + !form.bottleSize || + form.producedQuantity === null + ) { console.warn("⚠️ กรุณากรอกข้อมูลให้ครบทุกช่อง!"); return; } @@ -175,6 +231,8 @@ function handleSubmit() { MachineID: form.machineID, OrderID: form.orderID, PageID: form.pageID, + itemType: form.itemType, + itemID: form.itemID, startTime: new Date(form.startTime).toISOString(), finishTime: new Date(form.finishTime).toISOString(), status: form.status, @@ -194,14 +252,14 @@ function handleSubmit() { queueStore .addQueue(payload) .then(async () => { - await queueStore.fetchQueues(); // ✅ ดึงข้อมูลใหม่แทน push เพื่อความครบ + await queueStore.fetchQueues(); // ดึงข้อมูลใหม่ closeDialog(); }) .catch((error) => console.error("❌ Error adding queue:", error)); } } - </script> + <style scoped> .add-queue-dialog-overlay { position: fixed; @@ -287,12 +345,4 @@ function handleSubmit() { box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); } -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.3s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} </style> diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index fb9a3229e99bc8b1116a1233e9c369d134fe3ee7..b9db961b7b9d338000e809558ef518f827ccc3fb 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -74,7 +74,7 @@ <!-- Ghost Queue (ขณะลาก/resize) --> <div v-if="ghostQueue" class="drag-ghost" :style="ghostStyle"> - {{ ghostQueue.orderID }} ({{ getTimeString(ghostQueue.startTime) }} - {{ getTimeString(ghostQueue.finishTime) }}) + {{ ghostQueue.label }} ({{ getTimeString(ghostQueue.startTime) }} - {{ getTimeString(ghostQueue.finishTime) }}) </div> <v-divider :thickness="7"></v-divider> @@ -116,8 +116,8 @@ </div> <OrderDialog v-if="selectedQueueItem && !showAddDialog" - :queueItem="selectedQueueItem" - :color="getQueueColor(selectedQueueItem.orderID)" + :queueId="selectedQueueItem.QueueID" + :color="getQueueColor(selectedQueueItem.QueueID)" @close="closeQueueDialog" @edit="openAddQueueDialogForEdit" @delete="handleDelete" @@ -172,6 +172,7 @@ const draggingQueue = ref<any | null>(null); const dragOffset = ref(0); const resizingQueue = ref<any | null>(null); const resizeDirection = ref<string | null>(null); +const localQueueMap = ref<Record<number, any>>({}); const ghostQueue = ref<any | null>(null); const ghostStyle = reactive({ @@ -196,23 +197,30 @@ const originalColorMap: Record<number, string> = {}; // แปลงข้อมูล Queue จาก backend ให้เข้ากับรูปแบบที่ Gantt Chart ใช้ const formattedQueues = computed(() => { return queueStore.queues.map(q => { + const isResizing = resizingQueue.value && resizingQueue.value.QueueID === q.QueueID; + const local = localQueueMap.value[q.QueueID]; + const label = originalLabelMap[q.QueueID] ?? (q.QueueType?.QueueTypeID === 2 ? 'ผลิตเผื่อ' : - q.QueueType?.QueueTypeID === 3 ? 'เปลี่ยนขนาด' : - q.order?.customer?.name || 'Unknown'); + q.QueueType?.QueueTypeID === 3 ? 'เปลี่ยนขนาด' : + local?.label || q.order?.customer?.name || 'Unknown'); const color = originalColorMap[q.QueueID] ?? getQueueColor(label); + const startTime = isResizing ? resizingQueue.value.startTime : convertToLocalTime(q.startTime); + const finishTime = isResizing ? resizingQueue.value.finishTime : convertToLocalTime(q.finishTime); + + return { QueueID: q.QueueID, orderID: q.order?.OrderID || "Unknown", label, - color, // เพิ่มตรงนี้ไว้ใช้ใน getQueueStyle + color, machineID: q.machine?.MachineID || 0, machineName: q.machine?.name || "Unknown Machine", - startTime: q.startTime ? convertToLocalTime(q.startTime) : "Unknown", - finishTime: q.finishTime ? convertToLocalTime(q.finishTime) : "Unknown", + startTime, + finishTime, status: q.status || "Unknown", bottleSize: q.bottleSize || "Unknown", producedQuantity: q.producedQuantity || 0, @@ -224,6 +232,7 @@ const formattedQueues = computed(() => { + function clearOriginalMaps() { Object.keys(originalLabelMap).forEach((k) => delete originalLabelMap[+k]); Object.keys(originalColorMap).forEach((k) => delete originalColorMap[+k]); @@ -527,69 +536,82 @@ async function onDrop(event: DragEvent, newMachineID: number) { function onResizeStart(event: MouseEvent, item: any, direction: string) { event.preventDefault(); - ghostQueue.value = { ...item, label: item.label }; // 🟢 คง label เดิมไว้ resizingQueue.value = item; resizeDirection.value = direction; + + // 🧠 เก็บสำเนาไว้ก่อนถูก fetch เคลียร์ + localQueueMap.value[item.QueueID] = { ...item }; + document.addEventListener('mousemove', onResizing); document.addEventListener('mouseup', onResizeEnd); } + + function onResizing(event: MouseEvent) { if (!resizingQueue.value) return; + const rowTimeline = document.querySelector('.row-timeline') as HTMLElement; if (!rowTimeline) return; + const timelineRect = rowTimeline.getBoundingClientRect(); const timelineWidth = timelineRect.width; const offsetX = event.clientX - timelineRect.left; + let ratio = offsetX / timelineWidth; ratio = Math.min(Math.max(ratio, 0), 1); + const timelineStartDec = timeToDecimal(startTime.value); const timelineEndDec = timeToDecimal(endTime.value); - const startDec = timeToDecimal(getTimeString(resizingQueue.value.startTime)); - const endDec = timeToDecimal(getTimeString(resizingQueue.value.finishTime)); + let newTimeDecimal = timelineStartDec + ratio * (timelineEndDec - timelineStartDec); - newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; + newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; // snap 30 นาที + const datePart = getDateString(resizingQueue.value.startTime); + const startDec = timeToDecimal(getTimeString(resizingQueue.value.startTime)); + const endDec = timeToDecimal(getTimeString(resizingQueue.value.finishTime)); + if (resizeDirection.value === 'left') { if (newTimeDecimal >= endDec) return; - const newStartStr = datePart + ' ' + decimalToTime(newTimeDecimal); - ghostQueue.value.startTime = newStartStr; + resizingQueue.value.startTime = datePart + ' ' + decimalToTime(newTimeDecimal); } else if (resizeDirection.value === 'right') { if (newTimeDecimal <= startDec) return; - const newEndStr = datePart + ' ' + decimalToTime(newTimeDecimal); - ghostQueue.value.finishTime = newEndStr; + resizingQueue.value.finishTime = datePart + ' ' + decimalToTime(newTimeDecimal); } } -async function onResizeEnd() { - if (!resizingQueue.value || !ghostQueue.value) return; - const queueID = resizingQueue.value.QueueID; - - // 🟡 Save original label & color - originalLabelMap[queueID] = resizingQueue.value.label; - originalColorMap[queueID] = getQueueColor(resizingQueue.value.label); - await queueStore.updateQueue(queueID, { - startTime: ghostQueue.value.startTime, - finishTime: ghostQueue.value.finishTime, - }); +async function onResizeEnd() { + if (!resizingQueue.value) return; - await queueStore.fetchQueues(); - clearOriginalMaps(); + const item = resizingQueue.value; - resizingQueue.value = null; - resizeDirection.value = null; - ghostQueue.value = null; - ghostStyle.display = 'none'; + try { + await queueStore.updateQueue(item.QueueID, { + startTime: item.startTime, + finishTime: item.finishTime, + }); - document.removeEventListener('mousemove', onResizing); - document.removeEventListener('mouseup', onResizeEnd); + // ✅ ค่อย fetch หลัง update เสร็จ + await queueStore.fetchQueues(); + } catch (error) { + console.error('❌ Resize update failed:', error); + } finally { + // ✅ ย้ายมาปิดตรงนี้ เพื่อให้ formattedQueues ยังแสดง item เดิมระหว่างรอ fetch + resizingQueue.value = null; + resizeDirection.value = null; + + document.removeEventListener('mousemove', onResizing); + document.removeEventListener('mouseup', onResizeEnd); + } } + + // Pagination Functions async function addPage() { if (pageStore.pages.length < 10) { diff --git a/src/components/GanttChart/OrderDialog.vue b/src/components/GanttChart/OrderDialog.vue index f3e6846122444a4dd6d1d923c1acd1a917e9ec00..cecca831f55bc81d0824d973b3769bdffcbe0874 100644 --- a/src/components/GanttChart/OrderDialog.vue +++ b/src/components/GanttChart/OrderDialog.vue @@ -1,148 +1,172 @@ <template> <div class="order-dialog-overlay"> - <div class="order-dialog" :style="{ backgroundColor: color }"> - <div class="dialog-header"> - <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> - </button> - - <button @click="$emit('delete', queueItem)" class="icon-button"> - <span class="mdi mdi-delete"></span> - </button> - - <button @click="$emit('close')" class="icon-button"> - <span class="mdi mdi-close"></span> - </button> - </div> - </div> - <div class="order-details"> - <p><strong>ผลิตจำนวน</strong> {{ queueItem.producedQuantity }} ขวด/แพ็ค</p> - <p><strong>ขนาดขวด</strong> {{ queueItem.bottleSize }}</p> - <p><strong>ตั้งแต่เวลา</strong> {{ queueItem.startTime }} - {{ queueItem.finishTime }}</p> - </div> - <div class="order-assignments"> - <div class="machine"> - <span>🛠</span> {{ getMachineName(queueItem.machineID) }} - </div> - <!-- พนักงาน --> - <div class="employees" v-for="(employee, index) in queueItem.employees" :key="index"> - <span >👤</span> {{ employee }} - </div> + <div class="order-dialog" :style="{ backgroundColor: color }"> + <div class="dialog-header"> + <h3 class="order-title">QueueID: {{ queueItem?.QueueID || 'N/A' }}</h3> + <div class="dialog-buttons"> + <button @click="$emit('edit', queueItem)" class="icon-button"> + <v-icon>mdi-pencil</v-icon> + </button> + <button @click="$emit('delete', queueItem)" class="icon-button"> + <v-icon>mdi-delete</v-icon> + </button> + <button @click="$emit('close')" class="icon-button"> + <v-icon>mdi-close</v-icon> + </button> + </div> + </div> + + <div class="order-details"> + <p><strong>ผลิตจำนวน</strong> {{ queueItem?.producedQuantity || 'N/A' }} ขวด/แพ็ค</p> + <p><strong>ขนาดขวด</strong> {{ queueItem?.bottleSize || 'N/A' }}</p> + <p><strong>ตั้งแต่เวลา</strong> {{ formattedStartTime }} - {{ formattedFinishTime }}</p> + </div> + + <div class="order-details"> + <p><strong>Product </strong> + <!-- เช็ค itemType และแสดงข้อมูลตามประเภท --> + <span v-if="queueItem?.itemType === 'MATERIAL'"> + {{ queueItem?.item.name + ' ' + queueItem?.item.size + ' ' + queueItem?.item.brand || 'N/A' }} + </span> + <span v-else-if="queueItem?.itemType === 'PRODUCT'"> + {{ 'น้ำดื่ม '+queueItem?.item.brand + ' ' + queueItem?.item.size || 'N/A' }} + </span> + </p> + </div> + + <div class="order-assignments"> + <div class="machine"> + <v-icon>mdi-cog</v-icon> + {{ queueItem?.machine.name || 'N/A' }} + </div> + <div v-if="queueItem?.employees && queueItem.employees.length"> + <div class="employees" v-for="(employee, index) in queueItem.employees" :key="index"> + <v-icon>mdi-account</v-icon> {{ employee.name }} </div> + </div> + <div v-else class="employees no-employee"> + <v-icon>mdi-account-off</v-icon> ไม่มีพนักงาน + </div> </div> + </div> </div> </template> - -<script> -export default { + + <script> + import { useQueueStore } from '@/stores/queue'; + import { computed, watch } from 'vue'; + + export default { name: 'OrderDialog', props: { - queueItem: { - type: Object, - required: true, - }, - color: { - type: String, - default: '#F28B82', // สีโทนแดงอ่อน - }, + queueId: { type: Number, required: true }, + color: { type: String, default: '#F28B82' }, }, - data() { - return { - machines: [ - { machineID: 'MC1', name: 'เครื่องเป่าขวด' }, - { machineID: 'MC2', name: 'เครื่องเป่าขวด2' }, - { machineID: 'MC3', name: 'เครื่องสวมฉลาก' }, - { machineID: 'MC4', name: 'เครื่องสวมฉลาก2' }, - { machineID: 'MC5', name: 'เครื่องบรรจุ+แพ็ค' }, - ], - }; - }, - methods: { - getMachineName(id) { - const machine = this.machines.find(m => m.machineID === id); - return machine ? machine.name : id; + setup(props) { + const queueStore = useQueueStore(); + + // โหลดข้อมูล queueItem จาก store ตาม queueId + const queueItem = computed(() => queueStore.queues.find(q => q.QueueID === props.queueId) || null); + + // โหลดข้อมูลถ้า queueId เปลี่ยน + watch(() => props.queueId, () => { + console.log("🔄 queueId changed, fetching data..."); + queueStore.fetchQueues(); // ดึงข้อมูลใหม่ (ถ้ายังไม่ได้โหลด) + }, { immediate: true }); + + // ฟังก์ชันแปลงวันที่ให้เป็นฟอร์แมต YYYY-MM-DD + const formatTime = (dateString) => { + const date = new Date(dateString); + const hours = date.getHours().toString().padStart(2, '0'); // แปลงเป็น 2 หลัก + const minutes = date.getMinutes().toString().padStart(2, '0'); // แปลงเป็น 2 หลัก + return `${hours}:${minutes}`; // คืนค่าเวลาในฟอร์แมต HH:mm + }; + // สร้าง computed property สำหรับ startTime และ finishTime + const formattedStartTime = computed(() => formatTime(queueItem.value?.startTime)); + const formattedFinishTime = computed(() => formatTime(queueItem.value?.finishTime)); + + return { queueItem, formattedStartTime, formattedFinishTime}; }, - }, -}; -</script> - -<style scoped> -.order-dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; -} - -.order-dialog { - background: #F28B82; - padding: 15px; - border-radius: 12px; - width: 280px; - text-align: left; - position: relative; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.dialog-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.order-title { - font-size: 1.2rem; - font-weight: bold; - text-align: left; - color: white; /* เปลี่ยนเป็นสีขาว */ -} - -.dialog-buttons { - display: flex; - gap: 5px; -} - -.icon-button { - background: none; - border: none; - cursor: pointer; - font-size: 18px; - color: white; /* เปลี่ยนเป็นสีขาว */ -} - -.order-details { - background: white; - padding: 10px; - border-radius: 8px; - margin-top: 8px; - font-size: 0.9rem; -} - -.order-assignments { - margin-top: 10px; -} - -.machine, -.employees { - background: white; - padding: 5px; - border-radius: 5px; - margin-top: 5px; - display: flex; - align-items: center; - gap: 5px; -} - -.icon { - font-size: 1rem; - color: white; /* เปลี่ยนไอคอนเป็นสีขาว */ -} -</style> + }; + </script> + + <style scoped> + .order-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + } + + .order-dialog { + background: #F28B82; + padding: 15px; + border-radius: 12px; + width: 280px; + text-align: left; + position: relative; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + .dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .order-title { + font-size: 1.2rem; + font-weight: bold; + text-align: left; + color: white; + } + + .dialog-buttons { + display: flex; + gap: 5px; + } + + .icon-button { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + color: white; + } + + .order-details { + background: white; + padding: 10px; + border-radius: 8px; + margin-top: 8px; + font-size: 0.9rem; + } + + .order-assignments { + margin-top: 10px; + } + + .machine, + .employees { + background: white; + padding: 5px; + border-radius: 5px; + margin-top: 5px; + display: flex; + align-items: center; + gap: 5px; + } + + .icon { + font-size: 1rem; + color: white; + } + + </style> + \ No newline at end of file diff --git a/src/components/ProductionTargetTable.vue b/src/components/ProductionTargetTable.vue index 0ea5b0f56d13a99b22512e407f86df9a3546a6d0..8e74bc54b332946a774078d99347b8b117ac3dc6 100644 --- a/src/components/ProductionTargetTable.vue +++ b/src/components/ProductionTargetTable.vue @@ -42,25 +42,33 @@ const productionData = computed(() => { target.Date?.slice(0, 10) === selectedDate.value ) .map((target) => { - const customerName = target.order?.customer?.name || 'ไม่ระบุลูกค้า' - return { - id: target.ProductionTargetID, - // เปลี่ยน field จาก target.item?.ItemID เป็น target.item?.id - // ให้แน่ใจว่าฟิลด์นี้ตรงกับ structure ที่ backend ส่งมา - itemID: target.item?.id, - itemType: target.itemType, - name: target.item?.name || target.item?.brand || 'ไม่พบข้อมูล', - size: target.item?.size || '-', - brand: target.item?.brand || '-', - type: target.itemType === 'PRODUCT' ? 'Product' : 'Material', - target: target.TargetProduced, - produced: target.ActualProduced, - unit: target.item?.unit || '-', - remaining: target.Status, - order: target.order?.OrderID || 'ไม่ระบุ', - customer: customerName, - } - }) + const customerName = target.order?.customer?.name || 'ไม่ระบุลูกค้า' + + const item = target.item + const itemType = target.itemType + + const name = + itemType === 'PRODUCT' + ? `${'น้ำดื่ม '+item?.brand || ''} `.trim() + : `${item?.name || 'ไม่พบข้อมูล'}`.trim() + + return { + id: target.ProductionTargetID, + itemID: item?.id, + itemType: itemType, + name, + size: item?.size || '-', + brand: item?.brand || '-', + type: itemType === 'PRODUCT' ? 'Product' : 'Material', + target: target.TargetProduced, + produced: target.ActualProduced, + unit: item?.unit || '-', + remaining: target.Status, + order: target.order?.OrderID || 'ไม่ระบุ', + customer: customerName, + } +}) + console.log('✅ rawData:', rawData) diff --git a/src/services/material.ts b/src/services/material.ts new file mode 100644 index 0000000000000000000000000000000000000000..14e37a861d64f9139a241358dec1954c36c69eb3 --- /dev/null +++ b/src/services/material.ts @@ -0,0 +1,41 @@ +import http from './http'; +import type { Material } from '@/types/Material'; + +/** + * ดึงรายการ Material ทั้งหมดจาก backend + */ +export async function listMaterials(): Promise<Material[]> { + const { data } = await http.get('/materials'); + return data; +} + +/** + * ดึงข้อมูล Material รายการเดียวโดยใช้ MaterialID + */ +export async function getMaterial(id: number): Promise<Material> { + const { data } = await http.get(`/materials/${id}`); + return data; +} + +/** + * สร้าง Material ใหม่ + */ +export async function createMaterial(payload: Partial<Material>): Promise<Material> { + const { data } = await http.post('/materials', payload); + return data; +} + +/** + * อัปเดต Material ที่มีอยู่ + */ +export async function updateMaterial(id: number, payload: Partial<Material>): Promise<Material> { + const { data } = await http.patch(`/materials/${id}`, payload); + return data; +} + +/** + * ลบ Material ตาม MaterialID + */ +export async function deleteMaterial(id: number): Promise<void> { + await http.delete(`/materials/${id}`); +} diff --git a/src/services/product.ts b/src/services/product.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5198be6c34d188303eee321b3f87df60ab8f3c6 --- /dev/null +++ b/src/services/product.ts @@ -0,0 +1,41 @@ +import http from './http'; +import type { Product } from '@/types/Product'; + +/** + * ดึงรายการ Product ทั้งหมดจาก backend + */ +export async function listProducts(): Promise<Product[]> { + const { data } = await http.get('/products'); + return data; +} + +/** + * ดึงข้อมูล Product รายการเดียวโดยใช้ ProductID + */ +export async function getProduct(id: number): Promise<Product> { + const { data } = await http.get(`/products/${id}`); + return data; +} + +/** + * สร้าง Product ใหม่ + */ +export async function createProduct(payload: Partial<Product>): Promise<Product> { + const { data } = await http.post('/products', payload); + return data; +} + +/** + * อัปเดต Product ที่มีอยู่ + */ +export async function updateProduct(id: number, payload: Partial<Product>): Promise<Product> { + const { data } = await http.patch(`/products/${id}`, payload); + return data; +} + +/** + * ลบ Product ตาม ProductID + */ +export async function deleteProduct(id: number): Promise<void> { + await http.delete(`/products/${id}`); +} diff --git a/src/stores/material.ts b/src/stores/material.ts new file mode 100644 index 0000000000000000000000000000000000000000..dab9049895a1bfd71cb7f86987ade5831b6b9b57 --- /dev/null +++ b/src/stores/material.ts @@ -0,0 +1,95 @@ +// src/stores/material.store.ts + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Material } from '@/types/Material'; +import * as materialService from '@/services/material'; + +export const useMaterialStore = defineStore('material', () => { + const materials = ref<Material[]>([]); + const loading = ref<boolean>(false); + const error = ref<string | null>(null); + + // ดึงรายการ Material ทั้งหมดจาก backend + async function fetchMaterials() { + loading.value = true; + error.value = null; + try { + materials.value = await materialService.listMaterials(); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ดึงข้อมูล Material รายการเดียวโดยใช้ MaterialID + async function fetchMaterial(id: number): Promise<Material | null> { + loading.value = true; + error.value = null; + try { + return await materialService.getMaterial(id); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + // สร้าง Material ใหม่แล้วเพิ่มเข้า state + async function addMaterial(payload: Partial<Material>) { + loading.value = true; + error.value = null; + try { + const newMaterial = await materialService.createMaterial(payload); + materials.value.push(newMaterial); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // อัปเดต Material ที่มีอยู่ใน state + async function editMaterial(id: number, payload: Partial<Material>) { + loading.value = true; + error.value = null; + try { + const updatedMaterial = await materialService.updateMaterial(id, payload); + const index = materials.value.findIndex(m => m.MaterialID === id); + if (index !== -1) { + materials.value[index] = updatedMaterial; + } + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ลบ Material จาก backend และอัปเดต state + async function removeMaterial(id: number) { + loading.value = true; + error.value = null; + try { + await materialService.deleteMaterial(id); + materials.value = materials.value.filter(m => m.MaterialID !== id); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + materials, + loading, + error, + fetchMaterials, + fetchMaterial, + addMaterial, + editMaterial, + removeMaterial, + }; +}); diff --git a/src/stores/product.ts b/src/stores/product.ts new file mode 100644 index 0000000000000000000000000000000000000000..8fc68c1745f6b803e11157159ba5f77934f2048b --- /dev/null +++ b/src/stores/product.ts @@ -0,0 +1,95 @@ +// src/stores/product.store.ts + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Product } from '@/types/Product'; +import * as productService from '@/services/product'; + +export const useProductStore = defineStore('product', () => { + const products = ref<Product[]>([]); + const loading = ref<boolean>(false); + const error = ref<string | null>(null); + + // ดึงรายการ Product ทั้งหมดจาก backend + async function fetchProducts() { + loading.value = true; + error.value = null; + try { + products.value = await productService.listProducts(); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ดึงข้อมูล Product รายการเดียวโดยใช้ ProductID + async function fetchProduct(id: number): Promise<Product | null> { + loading.value = true; + error.value = null; + try { + return await productService.getProduct(id); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + // สร้าง Product ใหม่และเพิ่มลงใน state + async function addProduct(payload: Partial<Product>) { + loading.value = true; + error.value = null; + try { + const newProduct = await productService.createProduct(payload); + products.value.push(newProduct); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // อัปเดต Product ที่มีอยู่ใน state + async function editProduct(id: number, payload: Partial<Product>) { + loading.value = true; + error.value = null; + try { + const updatedProduct = await productService.updateProduct(id, payload); + const index = products.value.findIndex(p => p.ProductID === id); + if (index !== -1) { + products.value[index] = updatedProduct; + } + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + // ลบ Product จาก backend และอัปเดต state + async function removeProduct(id: number) { + loading.value = true; + error.value = null; + try { + await productService.deleteProduct(id); + products.value = products.value.filter(p => p.ProductID !== id); + } catch (err: any) { + error.value = err.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + products, + loading, + error, + fetchProducts, + fetchProduct, + addProduct, + editProduct, + removeProduct, + }; +}); diff --git a/src/types/Machine.ts b/src/types/Machine.ts index 7cde028c659d6796e9666824c338fe11c0deabe7..034971c69a580f0c08785b5546711feaedb4b08d 100644 --- a/src/types/Machine.ts +++ b/src/types/Machine.ts @@ -5,8 +5,6 @@ export interface Machine { 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/Material.ts b/src/types/Material.ts new file mode 100644 index 0000000000000000000000000000000000000000..865cc52796bd2e10d66a0b01e896f3270268c828 --- /dev/null +++ b/src/types/Material.ts @@ -0,0 +1,15 @@ +export interface Material { + MaterialID: number; + name: string; + size: string; + brand: string; + quantityInStock: number; + lowStockLevel: number; + status: string; + ReorderLevel: number; + unit: string; + pricePerUnit: number; + LastUpdate: string; // ISO string ของวันที่อัปเดตล่าสุด + // สามารถเพิ่ม field สำหรับ materialStocks หรือ recipeIngredients ได้ถ้าจำเป็น + } + \ No newline at end of file diff --git a/src/types/Product.ts b/src/types/Product.ts new file mode 100644 index 0000000000000000000000000000000000000000..00b0813caba848b7b4eb5209342f38f060c34c57 --- /dev/null +++ b/src/types/Product.ts @@ -0,0 +1,14 @@ +export interface Product { + ProductID: number; + brand: string; + size: string; + unit: string; + status: string; + lowStockLevel: number; + quantityInStock: number; + // เนื่องจากในฐานข้อมูลใช้ decimal precision, + // บางทีมันอาจจะส่งมาเป็น string จาก API + pricePerUnit: string | number; + // หากต้องการข้อมูลเพิ่มเติม เช่น orderDetails หรือ productStocks สามารถเพิ่มได้ + } + \ No newline at end of file diff --git a/src/types/Queue.ts b/src/types/Queue.ts index edfa6e631a262737f22581b66d3032b11eeed5e7..2c0627149b58c67ba42822e692aca9b3f85ceace 100644 --- a/src/types/Queue.ts +++ b/src/types/Queue.ts @@ -13,12 +13,14 @@ export interface Queue { order?: { OrderID: number; }; - employees: Employee[]; - startTime: string | Date; - finishTime: string | Date; + employees: Employee[]; + startTime: string | Date; + finishTime: string | Date; status: string; bottleSize: string; producedQuantity: number; + itemID: number; + itemType: 'PRODUCT' | 'MATERIAL'; } export interface Employee { @@ -36,4 +38,6 @@ export interface QueuePostData { PageID: number; OrderID: number; EmployeeIds: number[]; + itemID: number; + itemType: 'PRODUCT' | 'MATERIAL'; }