diff --git a/src/components/GanttChart/AddQueueDialog.vue b/src/components/GanttChart/AddQueueDialog.vue index 92a78eced3c5a90f2c4d9f229eb4a75912b01b18..f6f4b28f52424eab94c25fd14b0b9ce592ce2f07 100644 --- a/src/components/GanttChart/AddQueueDialog.vue +++ b/src/components/GanttChart/AddQueueDialog.vue @@ -40,7 +40,6 @@ <label>Produced Quantity:</label> <input v-model.number="form.producedQuantity" type="number" required /> </div> - <!-- ปุ่ม Add หรือ Save Changes --> <div class="dialog-buttons"> <button type="submit" class="primary-btn"> @@ -53,78 +52,106 @@ </div> </template> +<script setup lang="ts"> +import { reactive, ref, watch } from 'vue' -<script> -export default { - name: 'AddQueueDialog', - props: { - visible: Boolean, - queueItem: Object, // รับ queueItem ถ้ามี - }, - data() { - return { - form: { - machineID: '', - orderID: null, - pageNumber: null, - startTime: '', - finishTime: '', - status: '', - bottleSize: '', - producedQuantity: null, - }, - machines: [ - { machineID: 'MC1', name: 'เครื่องเป่าขวด' }, - { machineID: 'MC2', name: 'เครื่องเป่าขวด2' }, - { machineID: 'MC3', name: 'เครื่องสวมฉลาก' }, - { machineID: 'MC4', name: 'เครื่องสวมฉลาก2' }, - { machineID: 'MC5', name: 'เครื่องบรรจุ+แพ็ค' }, - ], - isEditing: false, // เช็คว่าเป็นโหมด Edit หรือไม่ - }; - }, - watch: { - queueItem(newItem) { - if (newItem) { - this.form = { ...newItem }; // ถ้าเป็น Edit โยนค่ามาใส่ฟอร์ม - this.isEditing = true; - } else { - this.resetForm(); - this.isEditing = false; - } - }, - }, - methods: { - closeDialog() { - console.log("Closing dialog"); // Debugging - this.$emit("close"); - this.resetForm(); - }, - handleSubmit() { - if (this.isEditing) { - this.$emit("edit", { ...this.form }); // ส่งข้อมูลไปอัปเดต Queue - } else { - this.$emit("add", { ...this.form, queueID: Date.now() }); // กรณีเพิ่มใหม่ต้องให้ queueID ไม่ซ้ำ - } - this.closeDialog(); - }, - resetForm() { - this.form = { - machineID: '', - orderID: null, - pageNumber: null, - startTime: '', - finishTime: '', - status: '', - bottleSize: '', - producedQuantity: null, - }; - }, +interface QueueItem { + machineID: string + orderID: number | null + pageNumber: number | null + startTime: string + finishTime: string + status: string + bottleSize: string + producedQuantity: number | null + queueID?: number +} + +interface Machine { + machineID: string + name: string +} + +// Props +const props = defineProps<{ + visible: boolean + queueItem?: QueueItem +}>() + +// 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, + pageNumber: null, + startTime: '', + finishTime: '', + status: '', + bottleSize: '', + 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.pageNumber = null + form.startTime = '' + form.finishTime = '' + form.status = '' + form.bottleSize = '' + form.producedQuantity = null +} + +function closeDialog() { + console.log("Closing dialog") + emit("close") + resetForm() +} + +function handleSubmit() { + if (isEditing.value) { + emit("edit", { ...form }) + } else { + emit("add", { ...form, queueID: Date.now() }) + } + closeDialog() +} + +// Watch for changes on queueItem prop to toggle edit mode +watch( + () => props.queueItem, + (newItem) => { + if (newItem) { + Object.assign(form, newItem) + isEditing.value = true + } else { + resetForm() + isEditing.value = false + } }, -}; + { immediate: true } +) </script> - <style scoped> .add-queue-dialog-overlay { position: fixed; @@ -201,4 +228,4 @@ export default { font-size: 24px; color: #2B2E3F; } -</style> \ No newline at end of file +</style> diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index a32b2ac9fc8ee92d28346fc6aa9803cce91b4715..6583f9536449852901649751328a88702123032f 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -3,10 +3,11 @@ <div class="gantt-header"> <!-- Calendar: เชื่อมกับ selectedDate --> <GanttCalendar v-model:modelValue="selectedDate" /> - + <!-- ปุ่ม Make a queue พร้อม event listener --> - <MakequeueBtn @updateQueue="handleUpdateQueue" /> + <MakequeueBtn @click="queueStore.makeQueue()" /> </div> + <!-- Gantt Chart UI --> <div class="gantt-chart"> <!-- Header: Time Scale --> @@ -21,41 +22,26 @@ <!-- Rows: เครื่องจักรแต่ละตัว --> <div class="rows"> - <div - v-for="machine in machines" - :key="machine.id" - class="row" - @dragover.prevent="onDragOver($event)" - @drop="onDrop($event, machine.machineID)" - > + <div v-for="machine in machines" :key="machine.machineID" class="row" @dragover.prevent="onDragOver($event)" + @drop="onDrop($event, machine.machineID)"> <div class="machine-label"> - {{ machine.name }} + {{ machine.machineID }} </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" + <div v-for="item in filteredQueue(machine.machineID)" :key="item.queueID" class="order" :class="{ 'faded': draggingQueue && draggingQueue.queueID === item.queueID }" - :style="getQueueStyle(item)" - > + :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> @@ -68,7 +54,8 @@ <!-- Ghost Queue (ขณะลาก) --> <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> @@ -99,419 +86,468 @@ </div> <!-- AddQueueDialog (สำหรับเพิ่ม/แก้ไข) --> - <AddQueueDialog - :visible="showAddDialog" - :queueItem="selectedQueueItem" - @close="closeAddQueueDialog" - @add="handleAddQueue" - @edit="handleEditQueue" - /> + <AddQueueDialog :visible="showAddDialog" :queueItem="selectedQueueItem" @close="closeAddQueueDialog" + @add="handleAddQueue" @edit="handleEditQueue" /> </div> <!-- Order Dialog (สำหรับดูรายละเอียดเพิ่มเติม) --> - <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="deleteQueueItem" /> </div> </div> </template> -<script> +<script lang="ts" setup> +import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import GanttCalendar from './GanttCalendar.vue'; import OrderDialog from './OrderDialog.vue'; import AddQueueDialog from './AddQueueDialog.vue'; import MakequeueBtn from './MakequeueBtn.vue'; import './ganttChart.css'; - -export default { - name: 'GanttChart', - components: { - MakequeueBtn, - GanttCalendar, - OrderDialog, - AddQueueDialog, - }, - data() { - return { - showAddDialog: false, - // ค่าจาก Calendar - selectedDate: '1/1/2025', - - // กำหนดช่วงเวลาใน Gantt - startTime: '08:00', - endTime: '17:00', - - // รายการเครื่องจักร - machines: [ - { machineID: 'MC1' , name: 'เครื่องเป่าขวด' }, - { machineID: 'MC2' , name: 'เครื่องเป่าขวด2' }, - { machineID: 'MC3' , name: 'เครื่องสวมฉลาก' }, - { machineID: 'MC4' , name: 'เครื่องบรรจุ+แพ็ค' }, - ], - - // รายการ Queue เริ่มต้น (สามารถเปลี่ยนแปลงได้) - Queue: [ - { - queueID: 1, - machineID: 'MC1', - orderID: 5, - pageNumber: 1, - startTime: '1/1/2025 09:00', - finishTime: '1/1/2025 11:00', - status: 'Process', - bottleSize: '600ml', - producedQuantity: 400, - }, - { - queueID: 2, - machineID: 'MC2', - orderID: 1, - pageNumber: 1, - startTime: '1/1/2025 13:00', - finishTime: '1/1/2025 15:00', - status: 'Waiting', - bottleSize: '500ml', - producedQuantity: 200, - }, - ], - - // Drag/Drop และ Resize - draggingQueue: null, - dragOffset: 0, - resizingQueue: null, - resizeDirection: null, - - // Ghost Queue (ขณะลาก) - ghostQueue: null, - ghostStyle: { - position: 'fixed', - top: '0px', - left: '0px', - display: 'none', - width: 'auto', - height: '40px', - lineHeight: '40px', - padding: '0 10px', - borderRadius: '10px', - pointerEvents: 'none', - backgroundColor: '#4caf50', - color: '#fff', - zIndex: 9999, - }, - - // Pagination - pages: [1, 2], - currentPage: 1, - pageToShowDelete: null, - - // Dialog - selectedQueueItem: null, - }; - }, - computed: { - // สร้าง list ของชั่วโมงทั้งหมดจาก startTime ถึง endTime - hours() { - const startHour = parseInt(this.startTime.split(':')[0]); - const endHour = parseInt(this.endTime.split(':')[0]); - return Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i); - }, - }, - methods: { - // รับข้อมูล Queue ที่ส่งมาจาก MakequeueBtn - handleUpdateQueue(newQueue) { - console.log("Received new queue:", newQueue); - // ในที่นี้เราสามารถแทนที่ Queue เดิมหรือ merge กับข้อมูลที่มีอยู่ - // ตัวอย่างนี้ใช้แทนที่ Queue ทั้งหมด - this.Queue = newQueue; - }, - - // -------------------- Dialog & Edit -------------------- - openQueueDialog(item) { - this.selectedQueueItem = { ...item }; - }, - closeQueueDialog() { - this.selectedQueueItem = null; - }, - openAddQueueDialog() { - this.selectedQueueItem = null; - this.showAddDialog = true; - }, - deleteQueueItem(item) { - this.Queue = this.Queue.filter(q => q.queueID !== item.queueID); - this.closeQueueDialog(); - }, - openAddQueueDialogForEdit(queueItem) { - this.selectedQueueItem = { ...queueItem }; - this.showAddDialog = true; - }, - closeAddQueueDialog() { - this.showAddDialog = false; - this.selectedQueueItem = null; - }, - handleAddQueue(newQueueItem) { - this.Queue.push({ - queueID: this.Queue.length + 1, - ...newQueueItem, - }); - this.showAddDialog = false; - }, - handleEditQueue(updatedQueueItem) { - const index = this.Queue.findIndex(q => q.queueID === updatedQueueItem.queueID); - if (index !== -1) { - this.Queue.splice(index, 1, { ...updatedQueueItem }); - } - this.selectedQueueItem = null; - this.showAddDialog = false; - }, - - // -------------------- Utility Functions -------------------- - formatHour(hour) { - return (hour < 10 ? '0' + hour : hour) + ':00'; - }, - getDateString(dateTimeStr) { - return dateTimeStr.split(' ')[0]; - }, - getTimeString(dateTimeStr) { - return dateTimeStr.split(' ')[1]; - }, - filteredQueue(machineID) { - return this.Queue.filter((q) => { - const queueDate = this.getDateString(q.startTime); - return ( - q.machineID === machineID && - q.pageNumber === this.currentPage && - queueDate === this.selectedDate - ); - }); - }, - getQueueColor(orderID) { - const colors = ['#F58181', '#FDDC5C', '#C5A1BC', '#49E060']; - return colors[(orderID - 1) % colors.length]; - }, - getQueueStyle(item) { - const start = this.getTimeString(item.startTime); - const end = this.getTimeString(item.finishTime); - const startDecimal = this.timeToDecimal(start); - const endDecimal = this.timeToDecimal(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: this.getQueueColor(item.orderID), - color: '#fff', - borderRadius: '10px', - textAlign: 'center', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - padding: '5px', - fontSize: '14px', - }; - }, - getLineStyle(hour) { - const timelineStart = this.timeToDecimal(this.startTime); - const timelineEnd = this.timeToDecimal(this.endTime); - const ratio = (hour - timelineStart) / (timelineEnd - timelineStart); - return { left: ratio * 100 + '%' }; - }, - timeToDecimal(timeStr) { - const [hours, minutes] = timeStr.split(':').map(Number); - return hours + minutes / 60; - }, - decimalToTime(decimal) { - const hours = Math.floor(decimal); - const minutes = Math.round((decimal - hours) * 60); - return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); - }, - - // -------------------- Drag/Drop & Resize -------------------- - onDragStart(event, item) { - this.draggingQueue = item; - const rect = event.target.getBoundingClientRect(); - this.dragOffset = rect.width / 2; - const emptyImg = document.createElement('img'); - emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; - event.dataTransfer.setDragImage(emptyImg, 0, 0); - - this.ghostQueue = { ...item }; - this.ghostStyle.display = 'block'; - this.ghostStyle.backgroundColor = this.getQueueColor(item.orderID); - - document.addEventListener('dragover', this.onDragOverGlobal); - document.addEventListener('dragend', this.onDragEndGlobal); - }, - onDragOver(event) { - event.preventDefault(); - }, - onDragOverGlobal(event) { - event.preventDefault(); - const rowTimeline = document.querySelector('.row-timeline'); - 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 timelineStart = this.timeToDecimal(this.startTime); - const timelineEnd = this.timeToDecimal(this.endTime); - const startDecimal = this.timeToDecimal(this.getTimeString(this.draggingQueue.startTime)); - const endDecimal = this.timeToDecimal(this.getTimeString(this.draggingQueue.finishTime)); - const duration = endDecimal - startDecimal; - - let newStartDecimal = timelineStart + ratio * (timelineEnd - timelineStart); - newStartDecimal = Math.round(newStartDecimal * 2) / 2; - let newEndDecimal = newStartDecimal + duration; - if (newEndDecimal > timelineEnd) { - newEndDecimal = timelineEnd; - newStartDecimal = newEndDecimal - duration; - } - - const datePart = this.getDateString(this.draggingQueue.startTime); - const newStartStr = datePart + ' ' + this.decimalToTime(newStartDecimal); - const newEndStr = datePart + ' ' + this.decimalToTime(newEndDecimal); - - this.ghostQueue.startTime = newStartStr; - this.ghostQueue.finishTime = newEndStr; - - const snappedRatioStart = (newStartDecimal - timelineStart) / (timelineEnd - timelineStart); - const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth; - const snappedRatioEnd = (newEndDecimal - timelineStart) / (timelineEnd - timelineStart); - const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth; - - const rows = document.querySelectorAll('.row'); - let closestRow = null; - let minDistance = Infinity; - rows.forEach((row) => { - const rect = row.getBoundingClientRect(); - const distance = Math.abs(event.clientY - rect.top); - if (distance < minDistance) { - minDistance = distance; - closestRow = row; - } - }); - - if (closestRow) { - this.ghostQueue.machineID = closestRow.querySelector('.machine-label').textContent.trim(); - this.ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; - } - - this.ghostStyle.left = snappedLeft + 'px'; - this.ghostStyle.width = snappedWidth + 'px'; - }, - onDragEndGlobal() { - this.ghostQueue = null; - this.ghostStyle.display = 'none'; - this.draggingQueue = null; - document.removeEventListener('dragend', this.onDragEndGlobal); - }, - onDragEnd(event, item) { - if (!this.ghostQueue) return; - item.startTime = this.ghostQueue.startTime; - item.finishTime = this.ghostQueue.finishTime; - item.machineID = this.ghostQueue.machineID; - document.removeEventListener('dragover', this.onDragOverGlobal); - this.ghostQueue = null; - this.ghostStyle.display = 'none'; - this.draggingQueue = null; - }, - onDrop(event, newMachine) { - event.preventDefault(); - if (this.draggingQueue && this.ghostQueue) { - this.draggingQueue.startTime = this.ghostQueue.startTime; - this.draggingQueue.finishTime = this.ghostQueue.finishTime; - this.draggingQueue.machineID = newMachine; - } - this.ghostQueue = null; - this.ghostStyle.display = 'none'; - this.draggingQueue = null; - }, - onResizeStart(event, item, direction) { - event.preventDefault(); - this.resizingQueue = item; - this.resizeDirection = direction; - document.addEventListener('mousemove', this.onResizing); - document.addEventListener('mouseup', this.onResizeEnd); - }, - onResizing(event) { - if (!this.resizingQueue) return; - const rowTimeline = document.querySelector('.row-timeline'); - 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 timelineStart = this.timeToDecimal(this.startTime); - const timelineEnd = this.timeToDecimal(this.endTime); - const startDec = this.timeToDecimal(this.getTimeString(this.resizingQueue.startTime)); - const endDec = this.timeToDecimal(this.getTimeString(this.resizingQueue.finishTime)); - let newTimeDecimal = timelineStart + ratio * (timelineEnd - timelineStart); - newTimeDecimal = Math.round(newTimeDecimal * 2) / 2; - const datePart = this.getDateString(this.resizingQueue.startTime); - - if (this.resizeDirection === 'left') { - if (newTimeDecimal >= endDec) return; - const newStartStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); - this.resizingQueue.startTime = newStartStr; - } else if (this.resizeDirection === 'right') { - if (newTimeDecimal <= startDec) return; - const newEndStr = datePart + ' ' + this.decimalToTime(newTimeDecimal); - this.resizingQueue.finishTime = newEndStr; - } - }, - onResizeEnd() { - this.resizingQueue = null; - this.resizeDirection = null; - document.removeEventListener('mousemove', this.onResizing); - document.removeEventListener('mouseup', this.onResizeEnd); - }, - addPage() { - if (this.pages.length < 10) { - const newPage = this.pages.length + 1; - this.pages.push(newPage); - } else { - alert("Maximum of 10 pages allowed."); - } - }, - onPageRightClick(page, event) { - event.preventDefault(); - this.pageToShowDelete = page; - }, - deletePage(page) { - const index = this.pages.indexOf(page); - if (index !== -1) { - this.pages.splice(index, 1); - if (this.currentPage === page) { - this.currentPage = this.pages.length > 0 ? this.pages[0] : 1; - } - } - this.pageToShowDelete = null; - }, - onDocumentClick(event) { - if (!event.target.closest('.page-btn')) { - this.pageToShowDelete = null; - } - }, - }, - mounted() { - document.addEventListener('click', this.onDocumentClick); - }, - beforeUnmount() { - document.removeEventListener('click', this.onDocumentClick); - }, -}; +import { useQueueStore } from '@/stores/queue'; +import type { QueueItem } from '@/types/QueueItem'; + +interface Machine { + machineID: string; + name: string; +} + + + +const queueStore = useQueueStore(); + +// NOTE: ถ้าต้องการโหลดข้อมูลทันทีที่เข้าหน้านี้ ให้ un-comment ด้านล่าง +onMounted(() => { + +}); + +// State +const showAddDialog = ref(false); +const selectedDate = ref('1/1/2025'); +const startTime = ref('08:00'); +const endTime = ref('17:00'); + +const machines = ref<Machine[]>([ + { machineID: 'MC1', name: 'เครื่องเป่าขวด' }, + { machineID: 'MC2', name: 'เครื่องเป่าขวด2' }, + { machineID: 'MC3', name: 'เครื่องสวมฉลาก' }, + { machineID: 'MC4', name: 'เครื่องบรรจุ+แพ็ค' }, +]); + +// ดึง ref ของ QueueItem[] จาก Store +const Queues = computed(() => queueStore.Queues.value); + +// Drag/Drop & Resize +const draggingQueue = ref<QueueItem | null>(null); +const dragOffset = ref(0); +const resizingQueue = ref<QueueItem | null>(null); +const resizeDirection = ref<string | null>(null); + +const ghostQueue = ref<QueueItem | null>(null); +const ghostStyle = reactive({ + position: 'fixed', + top: '0px', + left: '0px', + display: 'none', + width: 'auto', + height: '40px', + lineHeight: '40px', + padding: '0 10px', + borderRadius: '10px', + pointerEvents: 'none', + backgroundColor: '#4caf50', + color: '#fff', + zIndex: 9999, +}); + +// Pagination & Dialog +const pages = ref<number[]>([1, 2]); +const currentPage = ref(1); +const pageToShowDelete = ref<number | null>(null); +const selectedQueueItem = ref<QueueItem | null>(null); + + +watch(() => queueStore.Queues.value, (newVal) => { + console.log("🔄 Queues updated in GanttChart.vue:", newVal); +}, { deep: true }); + + +// Computed +const hours = computed(() => { + const startHour = parseInt(startTime.value.split(':')[0]); + const endHour = parseInt(endTime.value.split(':')[0]); + return Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i); +}); + +// Methods +function handleUpdateQueue(newQueue: QueueItem[]) { + console.log("Received new queue:", newQueue); + // อัปเดตลงใน store + Queues.value = newQueue; +} + +function openQueueDialog(item: QueueItem) { + selectedQueueItem.value = { ...item }; +} + +function closeQueueDialog() { + selectedQueueItem.value = null; +} + +function openAddQueueDialog() { + selectedQueueItem.value = null; + showAddDialog.value = true; +} + +function deleteQueueItem(item: QueueItem) { + // ลบคิวใน store + Queues.value = Queues.value.filter(q => q.queueID !== item.queueID); + closeQueueDialog(); +} + +function openAddQueueDialogForEdit(queueItem: QueueItem) { + selectedQueueItem.value = { ...queueItem }; + showAddDialog.value = true; +} + +function closeAddQueueDialog() { + showAddDialog.value = false; + selectedQueueItem.value = null; +} + +function handleAddQueue(newQueueItem: Omit<QueueItem, 'queueID'>) { + const newQueueID = Queues.value.length + 1; + Queues.value.push({ queueID: newQueueID, ...newQueueItem }); + showAddDialog.value = false; +} + +function handleEditQueue(updatedQueueItem: QueueItem) { + const index = Queues.value.findIndex(q => q.queueID === updatedQueueItem.queueID); + if (index !== -1) { + Queues.value.splice(index, 1, { ...updatedQueueItem }); + } + selectedQueueItem.value = null; + showAddDialog.value = false; +} + +// Utility Functions +function formatHour(hour: number): string { + return (hour < 10 ? '0' + hour : hour) + ':00'; +} + +function getDateString(dateTimeStr: string): string { + return dateTimeStr.split(' ')[0]; +} + +function getTimeString(dateTimeStr: string): string { + return dateTimeStr.split(' ')[1]; +} + +function filteredQueue(machineID: string) { + if (!queueStore.Queues?.values || typeof queueStore.Queues.values !== "function") { + console.warn("queueStore.Queues.values is not a function or is undefined"); + return []; + } + + return queueStore.Queues.filter(q => { + const queueDate = getDateString(q.startTime); + console.log(queueDate) + return ( + q.machineID === machineID && + q.pageNumber === currentPage.value && + queueDate === selectedDate.value + ); + }); +} + + + + +const colorMap: Record<string, string> = {}; // Map เก็บสีของแต่ละ orderID + +function getQueueColor(orderID: string | number): string { + const key = String(orderID); + + // 🔴 ถ้าเป็น "ผลิตเผื่อ" → ใช้สีแดงพาสเทล + if (key === "ผลิตเผื่อ") { + return "#FFC1C1"; // Light Pastel Red + } + + // 💜 ถ้าเป็น "เปลี่ยนขนาด" → ใช้สีม่วงชมพูพาสเทล + if (key === "เปลี่ยนขนาด") { + return "#E1BEE7"; // Light Pastel Pink-Purple (Lavender) + } + + // ✅ ถ้ามีสีอยู่แล้ว ให้ใช้สีเดิม + if (colorMap[key]) return colorMap[key]; + + // 🌊 ถ้าเป็นคิวปกติ → ใช้สีธีมน้ำ + const newColor = generateWaterPastel(); + colorMap[key] = newColor; + return newColor; +} + +function generateWaterPastel(): string { + // สุ่มเฉพาะสีโทนน้ำทะเล (ฟ้า, น้ำเงิน, เขียวมิ้นต์) + const waterHues = [180, 190, 200, 210, 220, 230, 240]; + const hue = waterHues[Math.floor(Math.random() * waterHues.length)]; + return `hsl(${hue}, 60%, 80%)`; +} + +function getQueueStyle(item: QueueItem) { + const start = getTimeString(item.startTime); + const end = getTimeString(item.finishTime); + const startDecimal = timeToDecimal(start); + const endDecimal = timeToDecimal(end); + const timelineStart = timeToDecimal(startTime.value); + const timelineEnd = timeToDecimal(endTime.value); + + const ratioStart = Math.max(0, Math.min(1, (startDecimal - timelineStart) / (timelineEnd - timelineStart))); + const ratioEnd = Math.max(0, Math.min(1, (endDecimal - timelineStart) / (timelineEnd - timelineStart))); + + const backgroundColor = getQueueColor(item.orderID); // ใช้ฟังก์ชันที่แก้ไขแล้ว + + console.log(`Queue ${item.queueID}: orderID=${item.orderID}, color=${backgroundColor}`); + + return { + left: ratioStart * 100 + '%', + width: (ratioEnd - ratioStart) * 100 + '%', + backgroundColor: backgroundColor, // ใช้สีแดงพาสเทลหรือสีธีมน้ำ + color: '#333', + borderRadius: '10px', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '5px', + fontSize: '14px', + fontWeight: 'bold', + boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', + }; +} + + + +function getLineStyle(hour: number) { + const timelineStart = timeToDecimal(startTime.value); + const timelineEnd = timeToDecimal(endTime.value); + const ratio = (hour - timelineStart) / (timelineEnd - timelineStart); + return { left: ratio * 100 + '%' }; +} + +function timeToDecimal(timeStr: string): number { + const [hours, minutes] = timeStr.split(':').map(Number); + return hours + minutes / 60; +} + +function decimalToTime(decimal: number): string { + const hours = Math.floor(decimal); + const minutes = Math.round((decimal - hours) * 60); + return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); +} + +// Drag/Drop & Resize Functions +function onDragStart(event: DragEvent, item: QueueItem) { + draggingQueue.value = item; + const target = event.target as HTMLElement; + const rect = target.getBoundingClientRect(); + dragOffset.value = rect.width / 2; + + const emptyImg = new Image(); + emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + event.dataTransfer?.setDragImage(emptyImg, 0, 0); + + ghostQueue.value = { ...item }; + ghostStyle.display = 'block'; + ghostStyle.backgroundColor = getQueueColor(item.orderID); + + document.addEventListener('dragover', onDragOverGlobal); + document.addEventListener('dragend', onDragEndGlobal); +} + +function onDragOver(event: DragEvent) { + event.preventDefault(); +} + +function onDragOverGlobal(event: DragEvent) { + event.preventDefault(); + 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); + if (!draggingQueue.value) return; + const startDecimal = timeToDecimal(getTimeString(draggingQueue.value.startTime)); + const endDecimal = timeToDecimal(getTimeString(draggingQueue.value.finishTime)); + const duration = endDecimal - startDecimal; + + let newStartDecimal = timelineStartDec + ratio * (timelineEndDec - timelineStartDec); + newStartDecimal = Math.round(newStartDecimal * 2) / 2; + let newEndDecimal = newStartDecimal + duration; + if (newEndDecimal > timelineEndDec) { + newEndDecimal = timelineEndDec; + newStartDecimal = newEndDecimal - duration; + } + + const datePart = getDateString(draggingQueue.value.startTime); + const newStartStr = datePart + ' ' + decimalToTime(newStartDecimal); + const newEndStr = datePart + ' ' + decimalToTime(newEndDecimal); + + if (ghostQueue.value) { + ghostQueue.value.startTime = newStartStr; + ghostQueue.value.finishTime = newEndStr; + } + + const snappedRatioStart = (newStartDecimal - timelineStartDec) / (timelineEndDec - timelineStartDec); + const snappedLeft = timelineRect.left + snappedRatioStart * timelineWidth; + const snappedRatioEnd = (newEndDecimal - timelineStartDec) / (timelineEndDec - timelineStartDec); + const snappedWidth = (snappedRatioEnd - snappedRatioStart) * timelineWidth; + + const rows = document.querySelectorAll('.row'); + let closestRow: HTMLElement | null = null; + let minDistance = Infinity; + rows.forEach((row) => { + const rect = (row as HTMLElement).getBoundingClientRect(); + const distance = Math.abs(event.clientY - rect.top); + if (distance < minDistance) { + minDistance = distance; + closestRow = row as HTMLElement; + } + }); + + if (closestRow) { + const labelEl = closestRow.querySelector('.machine-label') as HTMLElement; + if (labelEl) { + ghostQueue.value!.machineID = labelEl.textContent?.trim() || ghostQueue.value!.machineID; + } + ghostStyle.top = closestRow.getBoundingClientRect().top + 'px'; + } + + ghostStyle.left = snappedLeft + 'px'; + ghostStyle.width = snappedWidth + 'px'; +} + +function onDragEndGlobal() { + ghostQueue.value = null; + ghostStyle.display = 'none'; + draggingQueue.value = null; + document.removeEventListener('dragend', onDragEndGlobal); +} + +function onDragEnd(event: DragEvent, item: QueueItem) { + if (!ghostQueue.value) return; + item.startTime = ghostQueue.value.startTime; + item.finishTime = ghostQueue.value.finishTime; + item.machineID = ghostQueue.value.machineID; + document.removeEventListener('dragover', onDragOverGlobal); + ghostQueue.value = null; + ghostStyle.display = 'none'; + draggingQueue.value = null; +} + +function onDrop(event: DragEvent, newMachine: string) { + event.preventDefault(); + if (draggingQueue.value && ghostQueue.value) { + draggingQueue.value.startTime = ghostQueue.value.startTime; + draggingQueue.value.finishTime = ghostQueue.value.finishTime; + draggingQueue.value.machineID = newMachine; + } + ghostQueue.value = null; + ghostStyle.display = 'none'; + draggingQueue.value = null; +} + +function onResizeStart(event: MouseEvent, item: QueueItem, direction: string) { + event.preventDefault(); + resizingQueue.value = item; + resizeDirection.value = direction; + 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; + const datePart = getDateString(resizingQueue.value.startTime); + + if (resizeDirection.value === 'left') { + if (newTimeDecimal >= endDec) return; + const newStartStr = datePart + ' ' + decimalToTime(newTimeDecimal); + resizingQueue.value.startTime = newStartStr; + } else if (resizeDirection.value === 'right') { + if (newTimeDecimal <= startDec) return; + const newEndStr = datePart + ' ' + decimalToTime(newTimeDecimal); + resizingQueue.value.finishTime = newEndStr; + } +} + +function onResizeEnd() { + resizingQueue.value = null; + resizeDirection.value = null; + document.removeEventListener('mousemove', onResizing); + document.removeEventListener('mouseup', onResizeEnd); +} + +// Pagination Functions +function addPage() { + if (pages.value.length < 10) { + const newPage = pages.value.length + 1; + pages.value.push(newPage); + } else { + alert("Maximum of 10 pages allowed."); + } +} + +function onPageRightClick(page: number, event: MouseEvent) { + event.preventDefault(); + pageToShowDelete.value = page; +} + +function deletePage(page: number) { + const index = pages.value.indexOf(page); + if (index !== -1) { + pages.value.splice(index, 1); + if (currentPage.value === page) { + currentPage.value = pages.value.length > 0 ? pages.value[0] : 1; + } + } + pageToShowDelete.value = null; +} + +function onDocumentClick(event: MouseEvent) { + const target = event.target as HTMLElement; + if (!target.closest('.page-btn')) { + pageToShowDelete.value = null; + } +} + +// Lifecycle Hooks +onMounted(() => { + document.addEventListener('click', onDocumentClick); +}); + +onBeforeUnmount(() => { + document.removeEventListener('click', onDocumentClick); +}); </script> diff --git a/src/components/GanttChart/MakequeueBtn.vue b/src/components/GanttChart/MakequeueBtn.vue index 33f5ad09d2955498684a3117c636e65afe081e52..36474d62b6b8f794809f05dc059e1640bf85c96e 100644 --- a/src/components/GanttChart/MakequeueBtn.vue +++ b/src/components/GanttChart/MakequeueBtn.vue @@ -1,50 +1,34 @@ <template> <div> - <button @click="makeQueue">Make a queue</button> + <v-btn>Make a queue</v-btn> </div> </template> -<script> -import { scheduleAllOrders } from './scheduleCalculator.js'; +<script lang="ts" setup> +import { defineEmits } from 'vue'; +import { useQueueStore } from '@/stores/queue.js'; +import LoadingDialog from '../LoadingDialog.vue'; + -export default { - name: 'MakequeueBtn', - methods: { - makeQueue() { - const queueItems = scheduleAllOrders(); - this.$emit('updateQueue', queueItems); - } - } -} </script> <style scoped> -button { - /* พื้นหลังสีน้ำเงิน */ - background-color: #2196f3; - /* สีตัวอักษรเป็นสีขาว */ - color: #fff; - /* ไม่มีขอบ */ - border: none; - /* มุมโค้ง */ +.v-btn { + background-color: #333647; + color: #ffffff; border-radius: 8px; - /* ขนาดตัวอักษรใหญ่ขึ้น */ font-size: 18px; font-weight: bold; - /* ปรับขนาดปุ่มให้กว้างขึ้น */ width: 200px; height: 60px; - /* กึ่งกลางตัวอักษร */ display: flex; align-items: center; justify-content: center; - /* เปลี่ยนเคอร์เซอร์เป็นแบบคลิกได้ */ cursor: pointer; transition: background-color 0.2s ease; } -/* เมื่อเอาเมาส์ไปชี้ */ -button:hover { - background-color: #1976d2; +.v-btn:hover { + background-color: #7e9cd3; } -</style> +</style> \ No newline at end of file diff --git a/src/components/LoadingDialog.vue b/src/components/LoadingDialog.vue index 81427c150c04e8e128a69c4118e835c0cdfc60c8..9ca9a362e3a9c846c2068e9d9b51857d90764938 100644 --- a/src/components/LoadingDialog.vue +++ b/src/components/LoadingDialog.vue @@ -16,31 +16,38 @@ const loadingStore = useLoadingStore() width: 40px; aspect-ratio: 1; --c: linear-gradient(#ffffff 0 0); - --r1: radial-gradient(farthest-side at bottom,#ffffff 93%,#ffffff); - --r2: radial-gradient(farthest-side at top ,#ffffff 93%,#ffffff); - background: - var(--c) ,var(--r1),var(--r2), - var(--c) ,var(--r1),var(--r2), - var(--c) ,var(--r1),var(--r2); + --r1: radial-gradient(farthest-side at bottom, #ffffff 93%, #ffffff); + --r2: radial-gradient(farthest-side at top, #ffffff 93%, #ffffff); + background: + var(--c), var(--r1), var(--r2), + var(--c), var(--r1), var(--r2), + var(--c), var(--r1), var(--r2); background-repeat: no-repeat; animation: l2 1s infinite alternate; } + @keyframes l2 { - 0%,25% { - background-size: 8px 0,8px 4px,8px 4px,8px 0,8px 4px,8px 4px,8px 0,8px 4px,8px 4px; - background-position: 0 50%,0 calc(50% - 2px),0 calc(50% + 2px),50% 50%,50% calc(50% - 2px),50% calc(50% + 2px),100% 50%,100% calc(50% - 2px),100% calc(50% + 2px); - } - 50% { - background-size: 8px 100%,8px 4px,8px 4px,8px 0,8px 4px,8px 4px,8px 0,8px 4px,8px 4px; - background-position: 0 50%,0 calc(0% - 2px),0 calc(100% + 2px),50% 50%,50% calc(50% - 2px),50% calc(50% + 2px),100% 50%,100% calc(50% - 2px),100% calc(50% + 2px); - } - 75% { - background-size: 8px 100%,8px 4px,8px 4px,8px 100%,8px 4px,8px 4px,8px 0,8px 4px,8px 4px; - background-position: 0 50%,0 calc(0% - 2px),0 calc(100% + 2px),50% 50%,50% calc(0% - 2px),50% calc(100% + 2px),100% 50%,100% calc(50% - 2px),100% calc(50% + 2px); - } - 95%,100% { - background-size: 8px 100%,8px 4px, 8px 4px,8px 100%,8px 4px,8px 4px,8px 100%,8px 4px,8px 4px; - background-position: 0 50%,0 calc(0% - 2px),0 calc(100% + 2px),50% 50%,50% calc(0% - 2px),50% calc(100% + 2px),100% 50%,100% calc(0% - 2px),100% calc(100% + 2px); - } + + 0%, + 25% { + background-size: 8px 0, 8px 4px, 8px 4px, 8px 0, 8px 4px, 8px 4px, 8px 0, 8px 4px, 8px 4px; + background-position: 0 50%, 0 calc(50% - 2px), 0 calc(50% + 2px), 50% 50%, 50% calc(50% - 2px), 50% calc(50% + 2px), 100% 50%, 100% calc(50% - 2px), 100% calc(50% + 2px); + } + + 50% { + background-size: 8px 100%, 8px 4px, 8px 4px, 8px 0, 8px 4px, 8px 4px, 8px 0, 8px 4px, 8px 4px; + background-position: 0 50%, 0 calc(0% - 2px), 0 calc(100% + 2px), 50% 50%, 50% calc(50% - 2px), 50% calc(50% + 2px), 100% 50%, 100% calc(50% - 2px), 100% calc(50% + 2px); + } + + 75% { + background-size: 8px 100%, 8px 4px, 8px 4px, 8px 100%, 8px 4px, 8px 4px, 8px 0, 8px 4px, 8px 4px; + background-position: 0 50%, 0 calc(0% - 2px), 0 calc(100% + 2px), 50% 50%, 50% calc(0% - 2px), 50% calc(100% + 2px), 100% 50%, 100% calc(50% - 2px), 100% calc(50% + 2px); + } + + 95%, + 100% { + background-size: 8px 100%, 8px 4px, 8px 4px, 8px 100%, 8px 4px, 8px 4px, 8px 100%, 8px 4px, 8px 4px; + background-position: 0 50%, 0 calc(0% - 2px), 0 calc(100% + 2px), 50% 50%, 50% calc(0% - 2px), 50% calc(100% + 2px), 100% 50%, 100% calc(0% - 2px), 100% calc(100% + 2px); + } } </style> diff --git a/src/components/Navbar/MainAppBar.vue b/src/components/Navbar/MainAppBar.vue index a1138f1d65b8c962640387687634cc17cca8d53c..de3f0f3b557c1695c67fc4e9ea2535c63ed30cc5 100644 --- a/src/components/Navbar/MainAppBar.vue +++ b/src/components/Navbar/MainAppBar.vue @@ -18,6 +18,9 @@ watchEffect(() => { if (route.name === 'pq') { icon.value = 'mdi-text-box' title.value = ' คิวการผลิต' + } else if (route.name === 'pq-aof') { + icon.value = 'mdi-package-variant' + title.value = 'คิวการผลิตออฟ' } else if (route.name === 'stocks') { icon.value = 'mdi-package-variant' title.value = 'Stock Management' @@ -96,22 +99,22 @@ watchEffect(() => { } else if (route.name === 'receipt') { icon.value = 'mdi-receipt' title.value = 'Receipt Management' - } else if (route.name === 'se') { + } else if (route.name === 'se') { icon.value = 'mdi-cash-plus' title.value = 'Store Expenses Bill' - } else if (route.name === 'sereport') { + } else if (route.name === 'sereport') { icon.value = 'mdi-file-chart' title.value = 'Store Expenses Report' } else if (route.name === 'sechart') { icon.value = 'mdi-file-chart' title.value = 'Store Expenses Chart' - } else if (route.name === 'branch') { + } else if (route.name === 'branch') { icon.value = 'mdi-store' title.value = 'Branch Management' } else if (route.name === 'product') { icon.value = 'mdi-food-fork-drink' title.value = 'Product Management' - }else { + } else { icon.value = '' title.value = '' } diff --git a/src/components/Navbar/MainMenu.vue b/src/components/Navbar/MainMenu.vue index 3fec98d961ad205eccc9842b4f85fb3161403ec7..a2f1af9b5762bdd9a17ae35c15eea99ad5726299 100644 --- a/src/components/Navbar/MainMenu.vue +++ b/src/components/Navbar/MainMenu.vue @@ -9,73 +9,73 @@ const authStore = useAuthStore() </script> <template> - <v-navigation-drawer expand-on-hover - rail style="background-color: #2B2E3F; color:white;"> + <v-navigation-drawer expand-on-hover rail style="background-color: #2B2E3F; color:white;"> <!-- profile --> <v-list> <v-list-item - :prepend-avatar="`http://localhost:4000/images/users/${authStore.getCurrentUser()?.image || 'default.jpg'}`"> - <template #title> - <span style="font-size: 20px; font-weight: bold"> - {{ authStore.getCurrentUser()?.name || 'Unknown User' }} - </span> - </template> - <template #subtitle> - <span style="font-size: 17px"> - {{ authStore.getCurrentUser()?.role?.name || 'No Role' }} - </span> - </template> - </v-list-item> + :prepend-avatar="`http://localhost:4000/images/users/${authStore.getCurrentUser()?.image || 'default.jpg'}`"> + <template #title> + <span style="font-size: 20px; font-weight: bold"> + {{ authStore.getCurrentUser()?.name || 'Unknown User' }} + </span> + </template> + <template #subtitle> + <span style="font-size: 17px"> + {{ authStore.getCurrentUser()?.role?.name || 'No Role' }} + </span> + </template> + </v-list-item> </v-list> <!-- nav --> <v-list density="compact" nav> <!----> <v-list-item prepend-icon="mdi-chart-areaspline" title="แดชบอร์ด" value="dashboard" - :to="{ path: '/dashboard' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/dashboard' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-cart-outline" title="จัดการคำสั่งซื้อ" value="management" - :to="{ path: '/management' }"></v-list-item> - <!----> - <v-list-item prepend-icon="mdi-text-box" title="คิวการผลิต" value="pos" :to="{ name: 'pq' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/management' }"></v-list-item> + <!----> + <v-list-item prepend-icon="mdi-text-box" title="คิวการผลิต" value="pos" :to="{ name: 'pq' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-car" title="คิวการขนส่ง" value="transport-queue" - :to="{ path: '/transport-queue' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/transport-queue' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-cube-outline" title="จัดการวัตถุดิบ" value="manage-materials" - :to="{ path: '/manage-materials' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/manage-materials' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-archive" title="จัดการสินค้า" value="manage-products" - :to="{ path: '/manage-products' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/manage-products' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-account-multiple" title="จัดการลูกค้า" value="manage-customers" - :to="{ path: '/manage-customers' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/manage-customers' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-clipboard-account" title="จัดการพนักงาน" value="manage-employees" - :to="{ path: '/manage-employees' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/manage-employees' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-factory" title="จัดการเครื่องจักร" value="manage-machines" - :to="{ path: '/manage-machines' }"></v-list-item> - <!----> - <!----> + :to="{ path: '/manage-machines' }"></v-list-item> + <!----> + <!----> <v-list-item prepend-icon="mdi-truck-delivery" title="จัดการรถขนส่ง" value="manage-transportation-cars" - :to="{ path: '/manage-transportation-cars' }"></v-list-item> - <!----> - <v-list-item prepend-icon="mdi-account-tie" title="จัดการผู้ใช้งาน" value="user" - :to="{ path: '/user' }"></v-list-item> - <!----> + :to="{ path: '/manage-transportation-cars' }"></v-list-item> + <!----> + <v-list-item prepend-icon="mdi-account-tie" title="จัดการผู้ใช้งาน" value="user" + :to="{ path: '/user' }"></v-list-item> + <!----> </v-list> <v-list density="compact" nav style="position: absolute; bottom: 0; width: 100%;"> <v-divider></v-divider> - <v-list-item prepend-icon="mdi-cog" title="ตั้งค่า" value="setting" ></v-list-item> - <v-list-item prepend-icon="mdi-logout" title="ออกจากระบบ" value="logout" @click="authStore.logout()"></v-list-item> + <v-list-item prepend-icon="mdi-cog" title="ตั้งค่า" value="setting"></v-list-item> + <v-list-item prepend-icon="mdi-logout" title="ออกจากระบบ" value="logout" + @click="authStore.logout()"></v-list-item> </v-list> - + </v-navigation-drawer> </template> \ No newline at end of file diff --git a/src/services/queue.ts b/src/services/queue.ts index 93461466ea6cebac4030a99be93ed12b102e26bf..710d5cac468d1c9a2f9cec246f4bc17bd043d522 100644 --- a/src/services/queue.ts +++ b/src/services/queue.ts @@ -1,4 +1,45 @@ -import type { Queue } from '@/types/Queue' +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[] = []; @@ -47,4 +88,5 @@ export class QueueService { } return null; } + } diff --git a/src/stores/queue.ts b/src/stores/queue.ts new file mode 100644 index 0000000000000000000000000000000000000000..370d79ca1fcce60b8b3be85eef722a02a6f274ad --- /dev/null +++ b/src/stores/queue.ts @@ -0,0 +1,41 @@ +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)); +} + +export const useQueueStore = defineStore('queue', () => { + const Queues = ref<QueueItem[]>([]); + const loadingStore = useLoadingStore(); + + const makeQueue = async () => { + try { + // ✅ รอ 3 วินาทีจริง ๆ ก่อนเริ่มโหลด + loadingStore.isLoading = true; + console.log("Waiting for 3 seconds..."); + await sleep(3000); + + console.log("Fetching queue data..."); + const queueData = await QueueService.makeQueue(); // ✅ รอ API + + if (queueData && Array.isArray(queueData)) { + console.log("Appending data to Queues..."); + Queues.value = [...queueData]; // ✅ ใช้ spread แทน forEach() + } else { + console.log("No queue data received."); + } + + loadingStore.isLoading = false; // ✅ ปิด loading เมื่อโหลดเสร็จ + } catch (error) { + console.error('❌ Error fetching queue:', error); + loadingStore.isLoading = false; // ปิด loading ถ้า error + } + }; + + return { Queues, makeQueue }; +}); diff --git a/src/types/Queue.ts b/src/types/Queue.ts index d213212600126150443b8ebf03ca3cf933c5c3a5..ca35bf679eb7c8dec528d95fdeb09c73c7fed160 100644 --- a/src/types/Queue.ts +++ b/src/types/Queue.ts @@ -1,11 +1,11 @@ export type Queue = { - queueID: number; // PK - รหัส Queue - machineID: number; // FK - รหัสเครื่องจักรที่ใช้ - orderID: number; // FK - รหัสออเดอร์ที่เกี่ยวข้อง - pageID: number; // FK - รหัสหน้าที่แสดงใน Gantt Chart - startTime: Date; // เวลาเริ่มต้น - finishTime: Date; // เวลาสิ้นสุด - status: 'pending' | 'in-progress' | 'completed' | 'canceled'; // สถานะของคิว - bottleSize: string; // ขนาดขวดที่ผลิต - producedQuantity: number; // จำนวนที่ผลิตได้ -}; + queueID: number | string; + machineID: number | string; + orderID: number | string; + pageID: number; + startTime: Date; + finishTime: Date; + status: 'pending' | 'in-progress' | 'completed' | 'canceled'; + bottleSize: string; + producedQuantity: number; +}; \ No newline at end of file diff --git a/src/types/QueueItem.ts b/src/types/QueueItem.ts new file mode 100644 index 0000000000000000000000000000000000000000..b156202566bdb31c059624a31a339890de072e9d --- /dev/null +++ b/src/types/QueueItem.ts @@ -0,0 +1,11 @@ +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/ProductQueueViewAof.vue b/src/views/ProductQueueViewAof.vue new file mode 100644 index 0000000000000000000000000000000000000000..78c78c38318fb2156662bb1e642716d7be62b5af --- /dev/null +++ b/src/views/ProductQueueViewAof.vue @@ -0,0 +1,90 @@ +<script setup> +import { ref } from 'vue' +import GanttChart from '@/components/GanttChartAof/GanttChart.vue' +import EmployeeSector from '@/components/EmployeeSector.vue' + + +const employees = Array.from({ length: 10 }, (_, i) => `EM${i + 1}`) +const orders = ref([ + { id: 1, name: 'ORDER1' }, + { id: 2, name: 'ORDER2' }, + { id: 3, name: 'ORDER3' } +]) + +// Drag and drop functionality +const draggedOrder = ref(null) + +const handleDragStart = (order) => { + draggedOrder.value = order +} + +const handleDragOver = (event) => { + event.preventDefault() +} + +const handleDrop = (targetOrder) => { + if (!draggedOrder.value || draggedOrder.value === targetOrder) return + + // Find the indices of the dragged and target orders + const draggedIndex = orders.value.findIndex((o) => o.id === draggedOrder.value.id) + const targetIndex = orders.value.findIndex((o) => o.id === targetOrder.id) + + // Remove the dragged order from its original position + const [removed] = orders.value.splice(draggedIndex, 1) + + // Insert the dragged order at the target position + orders.value.splice(targetIndex, 0, removed) + + // Reset the dragged order + draggedOrder.value = null +} +</script> + +<template> + <v-container class="pa-0"> + <!-- Gantt chart --> + <v-sheet class="pa-1 mb-3" + style="border-radius: 15px; max-width: 98%; margin-left: auto; margin-right: auto; min-height: 460px;"> + <!-- Gantt Chart --> + <GanttChart /> + </v-sheet> + + <!-- Bottom Section --> + <v-row class="mt-1" style="max-width: 100%; margin-left: auto; margin-right: auto;"> + <!-- Employee Selection --> + <v-col cols="6"> + <v-card class="pa-4" style="background-color: white; border-radius: 15px; min-height: 280px;"> + <!-- เลือกพนักงานวางทับตรงนี้ --> + <EmployeeSector :employees="employees" /> + </v-card> + </v-col> + + <!-- Order Priority --> + <v-col cols="6"> + <v-card class="pa-5" style="background-color: white; border-radius: 15px; min-height: 280px"> + <p class="text-center font-weight-bold mb-4 text-black" style="font-size: 20px;">ลำดับความสำคัญ</p> + <div v-for="order in orders" :key="order.id" class="order-item" draggable="true" + @dragstart="() => handleDragStart(order)" @dragover="handleDragOver" @drop="() => handleDrop(order)"> + {{ order.name }} + </div> + </v-card> + </v-col> + </v-row> + </v-container> +</template> +<!-- ส่วน UI ของปุ่ม Order1--2 --> +<style scoped> +.order-item { + background-color: #2b2e3f; + color: white; + padding: 7px; + margin-bottom: 8px; + text-align: center; + cursor: move; + user-select: none; +} + +.order-item:hover { + opacity: 0.9; +} +</style>