diff --git a/package-lock.json b/package-lock.json index e9d153d38faf22dba6dba12f970c5fe1a8d5eff4..e8b2aef1bfa8e0ecae5e9f93631695f53f23e98c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "vue-ganttastic": "^0.9.34", "vue-horizontal-scroll": "^0.2.4", "vue-router": "^4.2.5", + "vue-toastification": "^2.0.0-rc.5", "vuetify": "^3.5.13" }, "devDependencies": { @@ -5583,6 +5584,15 @@ "he": "^1.2.0" } }, + "node_modules/vue-toastification": { + "version": "2.0.0-rc.5", + "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz", + "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.2" + } + }, "node_modules/vue-tsc": { "version": "1.8.25", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.25.tgz", diff --git a/package.json b/package.json index 2e257eda09ee4e3dc7b354964a91d4645f7be909..8a1d100919afac627c875a9203d376d4baaec5b1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "vue-ganttastic": "^0.9.34", "vue-horizontal-scroll": "^0.2.4", "vue-router": "^4.2.5", + "vue-toastification": "^2.0.0-rc.5", "vuetify": "^3.5.13" }, "devDependencies": { diff --git a/src/components/EmployeeSector.vue b/src/components/EmployeeSector.vue index 95105d10f9b88f404dab61f304f18019ee49fa6e..120eec05ee429d22aefec2bc9b6f6b7f2ed5fb17 100644 --- a/src/components/EmployeeSector.vue +++ b/src/components/EmployeeSector.vue @@ -1,12 +1,12 @@ <template> <div class="employee-sector"> - <h3>เลือกพนักงาน</h3> + <h3 style="color: black;">เลือกพนักงาน</h3> <div class="employee-list"> <div v-for="employee in employees" :key="employee.id" class="employee-card" draggable="true" @dragstart="onDragStart($event, employee)"> <!-- ✅ ปิด Drag Default ของรูปภาพ --> <img :src="employee.image || defaultImage" alt="employee" class="employee-image" @dragstart.prevent /> - <span>{{ employee.name }}</span> + <span style="color: black;">{{ employee.name }}</span> </div> </div> </div> @@ -38,25 +38,24 @@ const onDragStart = (event: DragEvent, employee: Employee) => { <style scoped> .employee-sector { - padding: 1rem; - background: white; - border-radius: 10px; - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + max-height: 250px; + border-radius: 20px; text-align: center; } /* ✅ ปรับ Employee Card ให้เป็น Responsive */ .employee-list { display: grid; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* ปรับจำนวนคอลัมน์อัตโนมัติ */ - gap: 10px; + gap: 15px; justify-items: center; padding: 10px; } /* ✅ ปรับขนาด Employee Card ตามหน้าจอ */ .employee-card { + margin: 5px; display: flex; flex-direction: column; align-items: center; diff --git a/src/components/GanttChart/GanttCalendar.vue b/src/components/GanttChart/GanttCalendar.vue index 9fb888ecef45cd3d4c604cd2bca5ed05ca248633..22a6b3093589499220143dce765b813a014f109e 100644 --- a/src/components/GanttChart/GanttCalendar.vue +++ b/src/components/GanttChart/GanttCalendar.vue @@ -1,48 +1,40 @@ <script setup> -import { ref, watch } from "vue"; -import { format, parse } from "date-fns"; -import CalendarPicker from "@/components/GanttChart/CalendarPicker.vue"; +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: "2025-01-01", // ✅ ใช้ "YYYY-MM-DD" เป็นค่าเริ่มต้น - }, -}); +// ⬇️ ดึง store +import { useDateStore } from '@/stores/dateStore' +import { storeToRefs } from 'pinia' -const emits = defineEmits(["update:modelValue"]); -const selectedDate = ref(props.modelValue); -const showCalendar = ref(false); +const dateStore = useDateStore() +const { currentDate: selectedDate } = storeToRefs(dateStore) // reactive binding + +const showCalendar = ref(false) // ✅ แปลงวันที่จาก "YYYY-MM-DD" เป็น `Date Object` const parseDate = (dateStr) => { - return parse(dateStr, "yyyy-MM-dd", new Date()); -}; + return parse(dateStr, 'yyyy-MM-dd', new Date()) +} // ✅ ฟังก์ชันเพิ่ม/ลดวัน และอัปเดต `selectedDate` const changeDate = (days) => { - const currentDate = parseDate(selectedDate.value); - const newDate = new Date(currentDate); - newDate.setDate(currentDate.getDate() + days); - selectedDate.value = format(newDate, "yyyy-MM-dd"); // ✅ ใช้ "YYYY-MM-DD" -}; - -// ✅ เมื่อ `selectedDate` เปลี่ยน ให้ emit ค่าออกไป -watch(selectedDate, (newVal) => { - console.log("📅 GanttCalendar - วันที่เปลี่ยน:", newVal); - emits("update:modelValue", newVal); -}); + const current = parseDate(selectedDate.value) + const newDate = new Date(current) + newDate.setDate(current.getDate() + days) + selectedDate.value = format(newDate, 'yyyy-MM-dd') // store จะถูกอัปเดตโดยอัตโนมัติ +} // ✅ ฟังก์ชันเปิดปฏิทิน const openCalendar = () => { - showCalendar.value = true; - console.log("📅 ปุ่ม ... ถูกกด!"); -}; + showCalendar.value = true + console.log('📅 ปุ่ม ... ถูกกด!') +} // ✅ ปิดปฏิทินเมื่อเลือกวันที่ const closeCalendarDialog = () => { - showCalendar.value = false; -}; + showCalendar.value = false +} </script> <template> @@ -60,7 +52,6 @@ const closeCalendarDialog = () => { < Back </v-btn> - <!-- ✅ ใช้ format YYYY-MM-DD --> <v-btn style="background-color: white; color: black; min-width: 200px; border-radius: 15px" > @@ -75,7 +66,6 @@ const closeCalendarDialog = () => { Next > </v-btn> - <!-- ✅ ใช้ 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"> @@ -83,7 +73,6 @@ const closeCalendarDialog = () => { </v-btn> </template> - <!-- ✅ ใช้ CalendarPicker ที่รองรับ YYYY-MM-DD --> <v-card class="pb-12 popover-card"> <CalendarPicker v-model="selectedDate" @update:modelValue="closeCalendarDialog" /> </v-card> diff --git a/src/components/GanttChart/GanttChart.vue b/src/components/GanttChart/GanttChart.vue index 99623950fbadaa62aacc089d3068c1cd95316540..445a0ced875312ba2982f14f285ac13fa0c10daf 100644 --- a/src/components/GanttChart/GanttChart.vue +++ b/src/components/GanttChart/GanttChart.vue @@ -2,12 +2,12 @@ <div class="gantt-container"> <div class="gantt-header"> <!-- Calendar: เชื่อมกับ selectedDate --> - <GanttCalendar v-model:modelValue="selectedDate" /> + <GanttCalendar /> <!-- ปุ่มจัดเรียงและเพิ่มระยะห่าง --> <div class="gantt-buttons"> <MakequeueBtn @click="openAddQueueDialog" /> - <SettingBtn /> + <SettingBtn class="mr-4"/> </div> </div> @@ -95,6 +95,12 @@ import { ref, reactive, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { useQueueStore } from '@/stores/queue'; import { useMachineStore } from '@/stores/machine'; +import { usePageStore } from '@/stores/page'; +import { usePageContextStore } from '@/stores/pageContext'; + +import { useDateStore } from '@/stores/dateStore' +import { storeToRefs } from 'pinia' + import GanttCalendar from './GanttCalendar.vue'; import OrderDialog from './OrderDialog.vue'; import AddQueueDialog from './AddQueueDialog.vue'; @@ -105,21 +111,30 @@ import './ganttChart.css'; // ใช้ store สำหรับ Queue และ Machine const queueStore = useQueueStore(); const machineStore = useMachineStore(); +const pageStore = usePageStore(); + +const dateStore = useDateStore() +const { currentDate: selectedDate } = storeToRefs(dateStore) + +const pageContext = usePageContextStore(); +const currentPage = computed({ + get: () => pageContext.currentPage, + set: (val) => pageContext.currentPage = val, +}); // เมื่อเข้าหน้า ให้ดึงข้อมูลจาก backend onMounted(() => { machineStore.fetchMachines(); - queueStore.fetchQueues().then(() => { - console.log("📌 Queues Loaded:", queueStore.queues); - }); + queueStore.fetchQueues() + pageStore.fetchPages(); }); // State สำหรับ Gantt Chart const showAddDialog = ref(false); -const selectedDate = ref('2025-01-01'); const startTime = ref('06:00'); const endTime = ref('20:00'); + const draggingQueue = ref<any | null>(null); const dragOffset = ref(0); const resizingQueue = ref<any | null>(null); @@ -165,8 +180,7 @@ function convertToLocalTime(utcString: string): string { } // Pagination & Dialog -const pages = ref<number[]>([1, 2]); -const currentPage = ref(1); +const pages = computed(() => pageStore.pages.map(p => p.PageID)); const pageToShowDelete = ref<number | null>(null); const selectedQueueItem = ref<any | null>(null); @@ -231,13 +245,13 @@ function getTimeString(dateTimeStr: string): string { function filteredQueue(machineID: number) { return formattedQueues.value.filter(q => { - const queueDate = getDateString(q.startTime); + const queueDate = getDateString(q.startTime) return ( q.machineID === machineID && q.pageNumber === currentPage.value && - queueDate === selectedDate.value - ); - }); + queueDate === selectedDate.value // ✅ ใช้จาก store แล้ว + ) + }) } const colorMap: Record<string, string> = {}; @@ -481,28 +495,28 @@ async function onResizeEnd() { document.removeEventListener('mouseup', onResizeEnd); } // Pagination Functions -function addPage() { - if (pages.value.length < 10) { - const newPage = pages.value.length + 1; - pages.value.push(newPage); +async function addPage() { + if (pageStore.pages.length < 10) { + await pageStore.addPage(); } else { - alert("Maximum of 10 pages allowed."); + 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; - } +async function deletePage(pageID: number) { + await pageStore.removePage(pageID); + + if (currentPage.value === pageID) { + const remaining = pageStore.pages.map(p => p.PageID); + currentPage.value = remaining.length > 0 ? remaining[0] : 1; } + pageToShowDelete.value = null; } + function onDocumentClick(event: MouseEvent) { const target = event.target as HTMLElement; if (!target.closest('.page-btn')) { diff --git a/src/components/GanttChart/OrderDialog.vue b/src/components/GanttChart/OrderDialog.vue index 8f2dacfea39a2241739a8f110d3dc3f989a75820..f3e6846122444a4dd6d1d923c1acd1a917e9ec00 100644 --- a/src/components/GanttChart/OrderDialog.vue +++ b/src/components/GanttChart/OrderDialog.vue @@ -18,7 +18,7 @@ </div> </div> <div class="order-details"> - <p><strong>เป่าขวดจำนวน</strong> {{ queueItem.producedQuantity }} ขวด</p> + <p><strong>ผลิตจำนวน</strong> {{ queueItem.producedQuantity }} ขวด/แพ็ค</p> <p><strong>ขนาดขวด</strong> {{ queueItem.bottleSize }}</p> <p><strong>ตั้งแต่เวลา</strong> {{ queueItem.startTime }} - {{ queueItem.finishTime }}</p> </div> diff --git a/src/components/GanttChart/PrioritySetting.vue b/src/components/GanttChart/PrioritySetting.vue new file mode 100644 index 0000000000000000000000000000000000000000..9818e06ec52a8a935f26bd4d911fe9a9c92fa346 --- /dev/null +++ b/src/components/GanttChart/PrioritySetting.vue @@ -0,0 +1,128 @@ +<script setup lang="ts"> +import { ref, onMounted, computed, watch } from 'vue'; +import { useOrderPriorityStore } from '@/stores/orderPriority'; +import { usePageContextStore } from '@/stores/pageContext'; + +const orderPriorityStore = useOrderPriorityStore(); +const pageContextStore = usePageContextStore(); + +// current page จาก global context store +const pageID = computed(() => pageContextStore.currentPage); + +// โหลด priorities ตอน mount และเมื่อ page เปลี่ยน +onMounted(() => { + if (pageID.value) { + orderPriorityStore.fetchWithFilter(undefined, pageID.value); + } +}); + +watch(pageID, (newVal) => { + if (newVal) { + orderPriorityStore.fetchWithFilter(undefined, newVal); + } +}); + +const orders = computed(() => { + return orderPriorityStore.priorities + .filter((p) => p.page?.PageID === pageID.value) + .sort((a, b) => a.priority - b.priority) + .map((p) => ({ + id: p.order.OrderID, + name: `ORDER${p.order.OrderID}`, + orderPriorityID: p.orderPriorityID, + })); +}); + +const draggedOrder = ref<{ id: number; orderPriorityID: number } | null>(null); + +const handleDragStart = (order: any) => { + draggedOrder.value = order; +}; + +const handleDragOver = (event: DragEvent) => { + event.preventDefault(); +}; + +const handleDrop = async (targetOrder: any) => { + if (!draggedOrder.value || draggedOrder.value.id === targetOrder.id) return; + + const draggedIndex = orders.value.findIndex((o) => o.id === draggedOrder.value!.id); + const targetIndex = orders.value.findIndex((o) => o.id === targetOrder.id); + + const updatedList = [...orders.value]; + const [moved] = updatedList.splice(draggedIndex, 1); + updatedList.splice(targetIndex, 0, moved); + + const payload = { + pages: [ + { + pageID: pageID.value, + configs: updatedList.map((order, index) => ({ + id: order.orderPriorityID, + priority: index + 1, + })), + }, + ], + }; + + await orderPriorityStore.updateMany(payload); + draggedOrder.value = null; +}; +</script> + +<template> + <v-card class="pa-5" style="background-color: #333647; border-radius: 15px; min-height: 280px;"> + <p class="text-center font-weight-bold mb-2" style="color: white; font-size: 20px;"> + ลำดับความสำคัญ (Page {{ pageID }}) + </p> + + <transition-group name="list" tag="div"> + <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> + </transition-group> + </v-card> +</template> + +<style scoped> +.order-item { + background-color: white; + color: black; + padding: 10px; + margin-bottom: 8px; + text-align: center; + cursor: move; + user-select: none; + border-radius: 5px; + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.order-item:hover { + opacity: 0.9; +} + +.order-item:active { + transform: scale(1.05); +} + +.list-move { + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.list-enter-active, .list-leave-active { + transition: opacity 0.3s, transform 0.3s; +} + +.list-enter-from, .list-leave-to { + opacity: 0; + transform: translateY(-10px); +} +</style> diff --git a/src/components/GanttChart/SettingBtn.vue b/src/components/GanttChart/SettingBtn.vue index d50bcbff2b99d75a05e70618dab9440ade38386a..238cb4787cecf719e8f3f49fb3224919f0ca5b80 100644 --- a/src/components/GanttChart/SettingBtn.vue +++ b/src/components/GanttChart/SettingBtn.vue @@ -1,15 +1,59 @@ <template> - <div> - <v-btn> - <v-icon>mdi-cog</v-icon> - </v-btn> + <div class="text-center"> + <v-menu open-on-hover> + <template v-slot:activator="{ props }"> + <v-btn v-bind="props"> + <v-icon>mdi-cog</v-icon> + </v-btn> + </template> + + <v-list> + <v-list-item + v-for="(item, index) in items" + :key="index" + @click="handleMenuClick(index)" + > + <v-list-item-title>{{ item.title }}</v-list-item-title> + </v-list-item> + </v-list> + </v-menu> + + <!-- Dialog สำหรับ StockConfig --> + <v-dialog v-model="showCollectDialog" max-width="700px"> + <StockConfig /> + </v-dialog> + + <!-- Dialog สำหรับ PrioritySetting --> + <v-dialog v-model="showPriorityDialog" max-width="400px"> + <PrioritySetting :currentPage="currentPage" /> + </v-dialog> </div> </template> -<script lang="ts" setup> -import { defineEmits } from 'vue'; -import { useQueueStore } from '@/stores/queue.js'; -import LoadingDialog from '../LoadingDialog.vue'; +<script setup lang="ts"> +import { ref, computed } from 'vue'; +import StockConfig from './StockConfig.vue'; +import PrioritySetting from './PrioritySetting.vue'; +import { usePageContextStore } from '@/stores/pageContext'; + +const items = ref([ + { title: 'จัดแต่งการเก็บ' }, + { title: 'ลำดับความสำคัญ' }, +]); + +const showCollectDialog = ref(false); +const showPriorityDialog = ref(false); + +const handleMenuClick = (index: number) => { + if (index === 0) { + showCollectDialog.value = true; + } else if (index === 1) { + showPriorityDialog.value = true; + } +}; + +const pageContext = usePageContextStore(); +const currentPage = computed(() => pageContext.currentPage); </script> <style scoped> @@ -35,4 +79,4 @@ import LoadingDialog from '../LoadingDialog.vue'; .v-icon { font-size: 32px; } -</style> +</style> \ No newline at end of file diff --git a/src/components/GanttChart/StockConfig.vue b/src/components/GanttChart/StockConfig.vue new file mode 100644 index 0000000000000000000000000000000000000000..829499359e9ce2c24d85165b7ac756ee3f14d52f --- /dev/null +++ b/src/components/GanttChart/StockConfig.vue @@ -0,0 +1,182 @@ +<template> + <v-card class="pa-5 dialog-card"> + <p class="text-center font-weight-bold mb-4 text-white" style="font-size: 20px;"> + หน้าจัดแต่งการเก็บ + </p> + + <v-table dense class="product-table"> + <thead> + <tr> + <th>ลำดับความสำคัญ</th> + <th>ชื่อ</th> + <th>ชนิด</th> + <th>แบรนด์</th> + <th>จำนวนที่ต้องการเก็บ</th> + <th>หน่วย</th> + </tr> + </thead> + <transition-group tag="tbody" name="list"> + <tr + v-for="config in stockConfigStore.stockConfigs" + :key="config.StockConfigID" + draggable="true" + @dragstart="() => handleDragStart(config)" + @dragover="handleDragOver" + @drop="() => handleDrop(config)" + > + <td class="drag-handle">☰</td> + <td>{{ config.itemName }}</td> + <td>{{ config.itemType }}</td> + <td>{{ config.itemDetail?.brand ?? '-' }}</td> + <td>{{ (config.targetStockLevel ?? 0).toLocaleString() }}</td> + <td>{{ config.itemDetail?.unit ?? '-' }}</td> + </tr> + + </transition-group> + </v-table> + + <!-- Toggle & Action Buttons --> + <div class="action-section"> + <v-switch + color="primary" + v-model="isProductionSpare" + label="ไม่ทำการผลิตเผื่อ" + class="switch-toggle" + /> + </div> + </v-card> +</template> + +<script setup> +import { ref, onMounted } from 'vue'; +import { useStockConfigStore } from '@/stores/stockConfig'; +import { usePageContextStore } from '@/stores/pageContext'; + +const stockConfigStore = useStockConfigStore(); +const pageContextStore = usePageContextStore(); + +// ดึงข้อมูล StockConfig จาก backend เมื่อ component mount +onMounted(() => { + const pageId = pageContextStore.currentPage; + if (pageId != null) { + console.log('📄 Current Page ID:', pageId); + stockConfigStore.fetchStockConfigs(pageId).then(() => { + console.log('✅ ดึงได้:', stockConfigStore.stockConfigs); + }); + } +}); + +// สำหรับ drag & drop +const draggedConfig = ref(null); + +function handleDragStart(config) { + draggedConfig.value = config; +} + +function handleDragOver(event) { + event.preventDefault(); +} + +async function handleDrop(targetConfig) { + if (!draggedConfig.value || draggedConfig.value.StockConfigID === targetConfig.StockConfigID) + return; + + const configs = stockConfigStore.stockConfigs; + const draggedIndex = configs.findIndex( + (c) => c.StockConfigID === draggedConfig.value.StockConfigID + ); + const targetIndex = configs.findIndex( + (c) => c.StockConfigID === targetConfig.StockConfigID + ); + + const [removed] = configs.splice(draggedIndex, 1); + configs.splice(targetIndex, 0, removed); + + // ✅ หลัง reorder ใน frontend แล้วส่งอัปเดตไป backend + const pageID = pageContextStore.currentPage; + const payload = { + pages: [ + { + PageID: pageID, + configs: configs.map((config, index) => ({ + id: config.StockConfigID, + priorityLevel: index + 1, + targetStockLevel: config.targetStockLevel, + status: config.status, + })), + }, + ], + }; + + await stockConfigStore.batchUpdateStockConfigs(payload); + draggedConfig.value = null; +} + + + +// Toggle สำหรับ "ไม่ทำการผลิตเผื่อ" +const isProductionSpare = ref(false); +</script> + +<style scoped> +.dialog-card { + background-color: #2b2e3f; + border-radius: 15px; + color: white; +} + +.product-table { + width: 100%; + background-color: #333647; + border-radius: 10px; +} + +.product-table th, +.product-table td { + padding: 10px; + text-align: center; + color: white; +} + +.product-table tr { + background-color: #333647; +} + +.product-table tr:hover { + background-color: #3e4258; +} + +.drag-handle { + cursor: grab; + font-size: 18px; + text-align: center; +} + +/* Animation สำหรับ Transition Group */ +.list-move { + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.list-enter-active, +.list-leave-active { + transition: opacity 0.3s, transform 0.3s; +} + +.list-enter-from, +.list-leave-to { + opacity: 0; + transform: translateY(-10px); +} + +.switch-toggle { + color: white; +} + +.action-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 15px; +} + +</style> diff --git a/src/components/GanttChart/ganttChart.css b/src/components/GanttChart/ganttChart.css index fc448c50483c671872357320b5faf699fe294cf8..726fa49a92068283ac9c0b9db6966aeeaf838b1a 100644 --- a/src/components/GanttChart/ganttChart.css +++ b/src/components/GanttChart/ganttChart.css @@ -1,6 +1,6 @@ .gantt-chart { width: 100%; - height: 100%; + height: 400px; display: flex; flex-direction: column; } @@ -88,7 +88,7 @@ .gantt-buttons { display: flex; align-items: center; - gap: 12px; /* ปรับตามต้องการ */ + gap: 20px; /* ปรับตามต้องการ */ } .MakequeueBtn { background-color: #333647; @@ -184,6 +184,7 @@ .page-btn.active { background: #007bff; + border-radius: 6px; color: #ffffff; } diff --git a/src/components/Navbar/MainAppBar.vue b/src/components/Navbar/MainAppBar.vue index de3f0f3b557c1695c67fc4e9ea2535c63ed30cc5..10a338c7d58a625309c023b7cc587141e278ad9f 100644 --- a/src/components/Navbar/MainAppBar.vue +++ b/src/components/Navbar/MainAppBar.vue @@ -18,9 +18,6 @@ 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' diff --git a/src/components/Navbar/MainMenu.vue b/src/components/Navbar/MainMenu.vue index a2f1af9b5762bdd9a17ae35c15eea99ad5726299..424839a6743850d10c999722af6f426db30ea964 100644 --- a/src/components/Navbar/MainMenu.vue +++ b/src/components/Navbar/MainMenu.vue @@ -36,8 +36,7 @@ const authStore = useAuthStore() <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> - <!----> + <v-list-item prepend-icon="mdi-text-box" title="คิวการผลิต" value="pq" :to="{ name: 'pq' }"></v-list-item> <!----> <v-list-item prepend-icon="mdi-car" title="คิวการขนส่ง" value="transport-queue" :to="{ path: '/transport-queue' }"></v-list-item> diff --git a/src/components/ProductionTargetTable.vue b/src/components/ProductionTargetTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..52e1ab894a899abd4c7705d56fd338fcce59cf01 --- /dev/null +++ b/src/components/ProductionTargetTable.vue @@ -0,0 +1,163 @@ +<script setup lang="ts"> +import { ref, onMounted, computed } from 'vue' +import { useProductionTargetStore } from '@/stores/productionTarget' +import { usePageContextStore } from '@/stores/pageContext' + +// ⬇️ ใช้ dateStore แทน props +import { useDateStore } from '@/stores/dateStore' +import { storeToRefs } from 'pinia' + +const productionTargetStore = useProductionTargetStore() +const pageContext = usePageContextStore() +const { currentDate: selectedDate } = storeToRefs(useDateStore()) // ✅ ใช้จาก store + +const currentPage = computed(() => pageContext.currentPage) + +const headers = ref([ + { title: "รหัส", key: "id" }, + { title: "ชื่อ", key: "name" }, + { title: "ขนาด", key: "size" }, + { title: "แบรนด์", key: "brand" }, + { title: "ชนิด", key: "type" }, + { title: "เป้าหมายการผลิต", key: "target" }, + { title: "ผลิตได้จริง", key: "produced" }, + { title: "หน่วย", key: "unit" }, + { title: "เหลือ", key: "remaining" }, +]) + +const groupBy = [{ key: 'order', order: 'asc' }] +const savingRowIds = ref<number[]>([]) +const recentlySavedIds = ref<number[]>([]) + +onMounted(async () => { + await productionTargetStore.fetchProductionTargets() +}) + +// ✅ กรองตาม PageID + selectedDate จาก store +const productionData = computed(() => + productionTargetStore.productionTargets + .filter((target) => + target.page?.PageID === currentPage.value && + target.Date?.slice(0, 10) === selectedDate.value + ) + .map((target) => ({ + id: target.ProductionTargetID, + 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 || `Order #${target.order?.OrderID}` || 'ไม่ระบุ', + })) +) + +async function saveActualProduced(item: any) { + const id = item.id + const produced = Number(item.produced) + if (isNaN(produced) || produced < 0) return + if (savingRowIds.value.includes(id)) return + + savingRowIds.value.push(id) + + try { + await productionTargetStore.editProductionTarget(id, { + ActualProduced: produced, + }) + await productionTargetStore.fetchProductionTargets() + + recentlySavedIds.value.push(id) + setTimeout(() => { + recentlySavedIds.value = recentlySavedIds.value.filter(rowId => rowId !== id) + }, 1000) + } catch (e) { + console.error('❌ Save failed:', e) + } finally { + savingRowIds.value = savingRowIds.value.filter(rowId => rowId !== id) + } +} + +function blockInvalidKeys(event: KeyboardEvent) { + const invalidKeys = ['e', 'E', '+', '-', '.', ','] + if (invalidKeys.includes(event.key)) { + event.preventDefault() + } +} +</script> + +<template> + <v-data-table + dense + class="custom-table" + :group-by="groupBy" + :headers="headers" + :items="productionData" + item-value="name" + hide-default-footer + > + <template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }"> + <tr> + <td :colspan="columns.length"> + <div class="d-flex align-center"> + <v-btn + :icon="isGroupOpen(item) ? '$expand' : '$next'" + density="comfortable" + size="x-small" + variant="outlined" + @click="toggleGroup(item)" + ></v-btn> + <span class="ms-2 text-caption">Order {{ item.value }}</span> + </div> + </td> + </tr> + </template> + + <template v-slot:item.produced="{ item }"> + <div class="save-cell-wrapper" style="position: relative; display: inline-block;"> + <v-text-field + class="tiny-input" + v-model.number="item.produced" + type="number" + density="compact" + variant="plain" + hide-details + style="max-width: 80px" + min="0" + step="1" + :loading="savingRowIds.includes(item.id)" + @keydown="blockInvalidKeys" + @keyup.enter="saveActualProduced(item)" + /> + <v-icon + v-if="recentlySavedIds.includes(item.id)" + color="green" + class="fade-check" + style="position: absolute; right: -24px; top: 4px;" + > + mdi-check-circle + </v-icon> + </div> + </template> + </v-data-table> +</template> + +<style scoped> +.custom-table { + font-size: 16px; +} +.tiny-input input { + font-size: 12px !important; + padding: 2px 4px !important; + height: 24px !important; +} +.fade-check { + animation: fadeOut 1s ease-out forwards; +} +@keyframes fadeOut { + 0% { opacity: 1; transform: scale(1); } + 80% { opacity: 1; transform: scale(1.2); } + 100% { opacity: 0; transform: scale(1); } +} +</style> diff --git a/src/services/orderPriority.ts b/src/services/orderPriority.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef6d2117fc887a07924c54263bcbd802119cc337 --- /dev/null +++ b/src/services/orderPriority.ts @@ -0,0 +1,59 @@ +import http from './http'; +import type { OrderPriority } from '@/types/OrderPriority'; + +export interface CreateOrderPriorityPayload { + orderID: number; + pageID: number; + priority?: number; +} + +export interface UpdateOrderPriorityPayload { + priority?: number; +} + +export interface BatchUpdateManyOrderPriorityDto { + pages: { + pageID: number; + configs: { + id: number; + priority: number; + }[]; + }[]; +} + +export async function listOrderPriorities(): Promise<OrderPriority[]> { + const { data } = await http.get('/orderpiorities'); + return data; +} + +export async function findOrderPrioritiesWithFilter( + orderId?: number, + pageId?: number +): Promise<OrderPriority[]> { + const params = new URLSearchParams(); + if (orderId !== undefined) params.append('orderId', orderId.toString()); + if (pageId !== undefined) params.append('pageId', pageId.toString()); + + const query = params.toString(); + const { data } = await http.get(`/orderpiorities${query ? '?' + query : ''}`); + return data; +} + + +export async function createOrUpdateOrderPriority( + payload: CreateOrderPriorityPayload +): Promise<{ message: string; orderPriority: OrderPriority }> { + const { data } = await http.post('/orderpiorities', payload); + return data; +} + +export async function updateManyOrderPriorities( + payload: BatchUpdateManyOrderPriorityDto +): Promise<any> { + const { data } = await http.patch('/orderpiorities/batch', payload); + return data; +} + +export async function deleteOrderPriority(id: number): Promise<void> { + await http.delete(`/orderpiorities/${id}`); +} diff --git a/src/services/page.ts b/src/services/page.ts index 82266566bb9a2afccce489900a2034c8f49ba39a..bfd1d48e4ac4d833163f1f405dd84f14ebdb2210 100644 --- a/src/services/page.ts +++ b/src/services/page.ts @@ -1,33 +1,41 @@ -import type { Page } from "../types/Page"; +import http from './http'; +import type { Page } from '@/types/Page'; -export class PageService { - private pages: Page[] = []; - - // ดึงข้อมูลหน้าทั้งหมด - getPages(): Page[] { - return this.pages; - } +/** + * ดึงรายการ Page ทั้งหมด + */ +export async function listPages(): Promise<Page[]> { + const { data } = await http.get('/pages'); + return data; +} - // ค้นหาหน้าโดยใช้ pageID - getPageById(pageID: number): Page | undefined { - return this.pages.find(page => page.pageID === pageID); - } +/** + * ดึง Page รายการเดียว + */ +export async function getPage(id: number): Promise<Page> { + const { data } = await http.get(`/pages/${id}`); + return data; +} - // เพิ่มหน้าใหม่ - addPage(page: Page): void { - this.pages.push(page); - } +/** + * สร้าง Page ใหม่ (ไม่ต้องส่ง payload) + */ +export async function createPage(): Promise<Page> { + const { data } = await http.post('/pages'); + return data; +} - // อัปเดตข้อมูลของหน้า - updatePage(updatedPage: Page): void { - const index = this.pages.findIndex(page => page.pageID === updatedPage.pageID); - if (index !== -1) { - this.pages[index] = updatedPage; - } - } +/** + * อัปเดต Page + */ +export async function updatePage(id: number, payload: Partial<Page>): Promise<Page> { + const { data } = await http.put(`/pages/${id}`, payload); + return data; +} - // ลบหน้าตาม pageID - removePage(pageID: number): void { - this.pages = this.pages.filter(page => page.pageID !== pageID); - } +/** + * ลบ Page + */ +export async function deletePage(id: number): Promise<void> { + await http.delete(`/pages/${id}`); } diff --git a/src/services/productionTarget.ts b/src/services/productionTarget.ts new file mode 100644 index 0000000000000000000000000000000000000000..777201288ab236ec67031a43c5c251e78adb6587 --- /dev/null +++ b/src/services/productionTarget.ts @@ -0,0 +1,31 @@ +import http from './http'; +import type { ProductionTarget } from '@/types/ProductionTarget'; + +export async function listProductionTargets(): Promise<ProductionTarget[]> { + const { data } = await http.get('/production-targets'); + return data; +} + +export async function getProductionTarget(id: number): Promise<ProductionTarget> { + const { data } = await http.get(`/production-targets/${id}`); + return data; +} + +export async function createProductionTarget( + payload: Partial<ProductionTarget> +): Promise<ProductionTarget> { + const { data } = await http.post('/production-targets', payload); + return data; +} + +export async function updateProductionTarget( + id: number, + payload: Partial<ProductionTarget> +): Promise<ProductionTarget> { + const { data } = await http.put(`/production-targets/${id}`, payload); + return data; +} + +export async function deleteProductionTarget(id: number): Promise<void> { + await http.delete(`/production-targets/${id}`); +} diff --git a/src/services/stockConfig.ts b/src/services/stockConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..7510c6168359ccd7c291b55f6e2c901a8fac6c63 --- /dev/null +++ b/src/services/stockConfig.ts @@ -0,0 +1,56 @@ +import http from './http'; +import type { StockConfig } from '@/types/StockConfig'; +import type { + CreateStockConfigDto, + UpdateStockConfigDto, + BatchUpdateManyStockConfigDto, +} from '@/types/StockConfigDto'; + +/** + * ดึงรายการ StockConfig ทั้งหมด (optional: ตาม page) + */ +export async function listStockConfigs(pageId?: number): Promise<StockConfig[]> { + const { data } = await http.get('/stock-config', { + params: pageId ? { pageId } : undefined, // ✅ แนบ pageId ไป + }); + return data; + } + +/** + * ดึง StockConfig รายการเดียว + */ +export async function getStockConfig(id: number): Promise<StockConfig> { + const { data } = await http.get(`/stock-config/${id}`); + return data; +} + +/** + * สร้าง StockConfig ใหม่ + */ +export async function createStockConfig(payload: CreateStockConfigDto): Promise<any> { + const { data } = await http.post('/stock-configs', payload); + return data; +} + +/** + * อัปเดต StockConfig + */ +export async function updateStockConfig(id: number, payload: UpdateStockConfigDto): Promise<StockConfig> { + const { data } = await http.put(`/stock-config/${id}`, payload); + return data; +} + +/** + * อัปเดตแบบหลายรายการ (batch) + */ +export async function updateManyStockConfigs(payload: BatchUpdateManyStockConfigDto): Promise<any> { + const { data } = await http.put('/stock-config/batch', payload); + return data; +} + +/** + * ลบ StockConfig + */ +export async function deleteStockConfig(id: number): Promise<void> { + await http.delete(`/stock-config/${id}`); +} diff --git a/src/stores/dateStore.ts b/src/stores/dateStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc09a60348d63ec5c958ca21e375520be79eb57c --- /dev/null +++ b/src/stores/dateStore.ts @@ -0,0 +1,17 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useDateStore = defineStore('dateStore', () => { + // 🔸 ใช้ default เป็น "2025-01-01" + const currentDate = ref('2025-01-01') + + // ✅ อัปเดตวันที่ + const setDate = (newDate: string) => { + currentDate.value = newDate + } + + return { + currentDate, + setDate, + } +}) diff --git a/src/stores/orderPriority.ts b/src/stores/orderPriority.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cbcf5f2e2d61625bbf013e8a5d539f4d3a2656b --- /dev/null +++ b/src/stores/orderPriority.ts @@ -0,0 +1,93 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { OrderPriority } from '@/types/OrderPriority'; +import * as service from '@/services/orderPriority'; + +export const useOrderPriorityStore = defineStore('orderPriority', () => { + const priorities = ref<OrderPriority[]>([]); + const loading = ref(false); + const error = ref<string | null>(null); + + async function fetchAll() { + loading.value = true; + error.value = null; + try { + priorities.value = await service.listOrderPriorities(); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function fetchWithFilter(orderId?: number, pageId?: number) { + loading.value = true; + error.value = null; + + // ✅ แปลงและกรองค่าที่ไม่ใช่ number + const validOrderId = typeof orderId === 'number' && !isNaN(orderId) ? orderId : undefined; + const validPageId = typeof pageId === 'number' && !isNaN(pageId) ? pageId : undefined; + + try { + priorities.value = await service.findOrderPrioritiesWithFilter(validOrderId, validPageId); + console.log + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + + + async function createOrUpdate(payload: service.CreateOrderPriorityPayload) { + loading.value = true; + error.value = null; + try { + const result = await service.createOrUpdateOrderPriority(payload); + await fetchAll(); // หรืออัปเดต state แบบเฉพาะจุดก็ได้ + return result; + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function updateMany(payload: service.BatchUpdateManyOrderPriorityDto) { + loading.value = true; + error.value = null; + try { + await service.updateManyOrderPriorities(payload); + await fetchAll(); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function remove(id: number) { + loading.value = true; + error.value = null; + try { + await service.deleteOrderPriority(id); + priorities.value = priorities.value.filter(p => p.orderPriorityID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + priorities, + loading, + error, + fetchAll, + fetchWithFilter, + createOrUpdate, + updateMany, + remove, + }; +}); diff --git a/src/stores/page.ts b/src/stores/page.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce84526f808fead9a46830824ec55f3507d40efd --- /dev/null +++ b/src/stores/page.ts @@ -0,0 +1,86 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { Page } from '@/types/Page'; +import * as pageService from '@/services/page'; + +export const usePageStore = defineStore('page', () => { + const pages = ref<Page[]>([]); + const loading = ref(false); + const error = ref<string | null>(null); + + async function fetchPages() { + loading.value = true; + error.value = null; + try { + pages.value = await pageService.listPages(); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function fetchPage(id: number): Promise<Page | null> { + loading.value = true; + error.value = null; + try { + return await pageService.getPage(id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + async function addPage() { + loading.value = true; + error.value = null; + try { + const newPage = await pageService.createPage(); + pages.value.push(newPage); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function editPage(id: number, payload: Partial<Page>) { + loading.value = true; + error.value = null; + try { + const updated = await pageService.updatePage(id, payload); + const index = pages.value.findIndex(p => p.PageID === id); + if (index !== -1) pages.value[index] = updated; + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function removePage(id: number) { + loading.value = true; + error.value = null; + try { + await pageService.deletePage(id); + pages.value = pages.value.filter(p => p.PageID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + pages, + loading, + error, + fetchPages, + fetchPage, + addPage, + editPage, + removePage, + }; +}); diff --git a/src/stores/pageContext.ts b/src/stores/pageContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..8022a213d0565e4d1374a92f5dcfe96251b99d8a --- /dev/null +++ b/src/stores/pageContext.ts @@ -0,0 +1,8 @@ +// src/stores/pageContext.ts +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const usePageContextStore = defineStore('pageContext', () => { + const currentPage = ref(1); + return { currentPage }; +}); diff --git a/src/stores/productionTarget.ts b/src/stores/productionTarget.ts new file mode 100644 index 0000000000000000000000000000000000000000..2246d0b1b37efe1ae07d9b9ead0eb8eecf730beb --- /dev/null +++ b/src/stores/productionTarget.ts @@ -0,0 +1,88 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { ProductionTarget } from '@/types/ProductionTarget'; +import * as productionTargetService from '@/services/productionTarget'; + +export const useProductionTargetStore = defineStore('productionTarget', () => { + const productionTargets = ref<ProductionTarget[]>([]); + const loading = ref(false); + const error = ref<string | null>(null); + + async function fetchProductionTargets() { + loading.value = true; + error.value = null; + try { + productionTargets.value = await productionTargetService.listProductionTargets(); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function fetchProductionTarget(id: number): Promise<ProductionTarget | null> { + loading.value = true; + error.value = null; + try { + return await productionTargetService.getProductionTarget(id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + async function addProductionTarget(payload: Partial<ProductionTarget>) { + loading.value = true; + error.value = null; + try { + const newTarget = await productionTargetService.createProductionTarget(payload); + productionTargets.value.push(newTarget); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function editProductionTarget(id: number, payload: Partial<ProductionTarget>) { + loading.value = true; + error.value = null; + try { + const updated = await productionTargetService.updateProductionTarget(id, payload); + const index = productionTargets.value.findIndex(p => p.ProductionTargetID === id); + if (index !== -1) { + productionTargets.value[index] = updated; + } + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function removeProductionTarget(id: number) { + loading.value = true; + error.value = null; + try { + await productionTargetService.deleteProductionTarget(id); + productionTargets.value = productionTargets.value.filter(p => p.ProductionTargetID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + productionTargets, + loading, + error, + fetchProductionTargets, + fetchProductionTarget, + addProductionTarget, + editProductionTarget, + removeProductionTarget, + }; +}); diff --git a/src/stores/stockConfig.ts b/src/stores/stockConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b545d12dd6398f54a1384785db779d588990efa --- /dev/null +++ b/src/stores/stockConfig.ts @@ -0,0 +1,111 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import type { StockConfig } from '@/types/StockConfig'; +import * as stockConfigService from '@/services/stockConfig'; +import type { + CreateStockConfigDto, + UpdateStockConfigDto, + BatchUpdateManyStockConfigDto, +} from '@/types/StockConfigDto'; + +export const useStockConfigStore = defineStore('stockConfig', () => { + const stockConfigs = ref<StockConfig[]>([]); + const loading = ref(false); + const error = ref<string | null>(null); + + async function fetchStockConfigs(pageId?: number) { + loading.value = true; + error.value = null; + try { + const result = await stockConfigService.listStockConfigs(pageId); + console.log('📥 stockConfigService.listStockConfigs response:', result); + stockConfigs.value = result; + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + console.error('❌ fetchStockConfigs error:', e); + } finally { + loading.value = false; + } + } + + + async function fetchStockConfig(id: number): Promise<StockConfig | null> { + loading.value = true; + error.value = null; + try { + return await stockConfigService.getStockConfig(id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + return null; + } finally { + loading.value = false; + } + } + + async function addStockConfig(payload: CreateStockConfigDto) { + loading.value = true; + error.value = null; + try { + const res = await stockConfigService.createStockConfig(payload); + stockConfigs.value.push(res.stockConfig); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function editStockConfig(id: number, payload: UpdateStockConfigDto) { + loading.value = true; + error.value = null; + try { + const updated = await stockConfigService.updateStockConfig(id, payload); + const index = stockConfigs.value.findIndex((c) => c.StockConfigID === id); + if (index !== -1) { + stockConfigs.value[index] = updated; + } + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function batchUpdateStockConfigs(payload: BatchUpdateManyStockConfigDto) { + loading.value = true; + error.value = null; + try { + await stockConfigService.updateManyStockConfigs(payload); + await fetchStockConfigs(); // รีโหลดทั้งหมดเพื่อความชัวร์ + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + async function removeStockConfig(id: number) { + loading.value = true; + error.value = null; + try { + await stockConfigService.deleteStockConfig(id); + stockConfigs.value = stockConfigs.value.filter((c) => c.StockConfigID !== id); + } catch (e: any) { + error.value = e.message || 'Unknown Error'; + } finally { + loading.value = false; + } + } + + return { + stockConfigs, + loading, + error, + fetchStockConfigs, + fetchStockConfig, + addStockConfig, + editStockConfig, + batchUpdateStockConfigs, + removeStockConfig, + }; +}); diff --git a/src/types/OrderPriority.ts b/src/types/OrderPriority.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ed8e0e85be3b57a4f7a95b8809b245f98379eb0 --- /dev/null +++ b/src/types/OrderPriority.ts @@ -0,0 +1,11 @@ +export interface OrderPriority { + orderPriorityID: number; + priority: number; + order: { + OrderID: number; + }; + page: { + PageID: number; + }; + } + \ No newline at end of file diff --git a/src/types/Page.ts b/src/types/Page.ts index 7925fb12295e16b258ac43c0a2f7f969a5500955..b03066705d31eefd4cdde13afe0a134765d5deba 100644 --- a/src/types/Page.ts +++ b/src/types/Page.ts @@ -1,5 +1,5 @@ -export type Page = { - pageID: number; // PK - รหัสหน้าที่ใช้ใน Gantt Chart - priorityID: number; // FK - รหัสความสำคัญ (เชื่อมกับ Priority) - }; - \ No newline at end of file +export interface Page { + PageID: number; + createdAt: string; // ISO string + updatedAt?: string; // ISO string (nullable) +} \ No newline at end of file diff --git a/src/types/ProductionTarget.ts b/src/types/ProductionTarget.ts new file mode 100644 index 0000000000000000000000000000000000000000..421757204ef61477f4c850d35e89a718c1f70861 --- /dev/null +++ b/src/types/ProductionTarget.ts @@ -0,0 +1,32 @@ +export interface ProductionTarget { + ProductionTargetID: number; + itemID: number; + itemType: 'PRODUCT' | 'MATERIAL'; + TargetProduced: number; + ActualProduced: number; + Status: string; + startTime: string | null; + endTime: string | null; + totalProductionHours: number | null; + Date: string; + updatedAt: string; + order?: { + OrderID: number; + } | null; + page?: { + PageID: number; + } | null; + item?: { + id: number; + type: 'PRODUCT' | 'MATERIAL'; + brand?: string; + size?: string; + unit?: string; + quantityInStock?: number; + lowStockLevel?: number; + pricePerUnit?: number; + status?: string; + name?: string; + lastUpdate?: string; + } | null; +} diff --git a/src/types/StockConfig.ts b/src/types/StockConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..c33688712c8ee117e43d35b8ee24001e1b0c3e19 --- /dev/null +++ b/src/types/StockConfig.ts @@ -0,0 +1,17 @@ +export interface StockConfig { + StockConfigID: number; + itemID: number; + itemType: 'PRODUCT' | 'MATERIAL'; + priorityLevel: number; + targetStockLevel: number; + status: string; + lastUpdated: string; + page: { + PageID: number; + createdAt: string; + updatedAt: string; + }; + itemName?: string; + itemDetail?: any; + } + \ No newline at end of file diff --git a/src/types/StockConfigDto.ts b/src/types/StockConfigDto.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e5d0cbba194da9ef04794a179636c7d79a88f16 --- /dev/null +++ b/src/types/StockConfigDto.ts @@ -0,0 +1,27 @@ +export interface CreateStockConfigDto { + PageID: number; + itemID: number; + itemType: 'PRODUCT' | 'MATERIAL'; + priorityLevel?: number; + targetStockLevel?: number; + status?: string; + } + + export interface UpdateStockConfigDto { + priorityLevel?: number; + targetStockLevel?: number; + status?: string; + } + + export interface BatchUpdateManyStockConfigDto { + pages: { + PageID: number; + configs: { + id: number; + priorityLevel?: number; + targetStockLevel?: number; + status?: string; + }[]; + }[]; + } + \ No newline at end of file diff --git a/src/views/ProductQueueView.vue b/src/views/ProductQueueView.vue index ab16a733fe149579d10d643054408ebd57a05d0b..e066efeb2e87279c89e036411c5e1e9b29107b2f 100644 --- a/src/views/ProductQueueView.vue +++ b/src/views/ProductQueueView.vue @@ -1,90 +1,60 @@ <script setup> -import { ref } from 'vue' -import GanttChart from '@/components/GanttChart/GanttChart.vue' -import EmployeeSector from '@/components/EmployeeSector.vue' +import { ref } from 'vue'; +import GanttChart from '@/components/GanttChart/GanttChart.vue'; +import EmployeeSector from '@/components/EmployeeSector.vue'; +import ProductionTargetTable from '@/components/ProductionTargetTable.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 -} +const length = ref(2); +const onboarding = ref(1); </script> <template> - <div class="gantt-title">ตารางคิวการผลิต</div> <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 --> + <v-sheet + class="pa-1 mb-2" + style="border-radius: 10px; max-width: 100%; min-height: 300px; " + > <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-sheet + class="pa-1 mb-2" + style="border-radius: 10px; max-width: 100%; min-height: 250px;" + > + <v-card flat> + <v-window v-model="onboarding"> + <!-- Employee Section --> + <v-window-item :value="1"> + <v-card style="height: 290px; overflow-y: auto;"> + <EmployeeSector :employees="employees" /> + </v-card> + </v-window-item> + + <!-- Product Data Table --> + <v-window-item :value="2"> + <v-card style="height: 290px; overflow-y: auto;"> + <ProductionTargetTable/> + </v-card> + </v-window-item> + </v-window> + </v-card> + + <!-- Pagination --> + <v-item-group v-model="onboarding" class="text-center mt-1" mandatory> + <v-item v-for="n in length" :key="`btn-${n}`" v-slot="{ isSelected, toggle }" :value="n"> + <v-btn + :variant="isSelected ? 'outlined' : 'text'" + icon="mdi-record" + density="compact" + @click="toggle" + ></v-btn> + </v-item> + </v-item-group> + </v-sheet> </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>