From c0c85113d96ae47e1acf3cccb94539bb02775268 Mon Sep 17 00:00:00 2001 From: Kritkhanin Anantakul <65160144@go.buu.ac.th> Date: Tue, 25 Mar 2025 19:43:04 +0700 Subject: [PATCH] fix target --- src/queues/queues.module.ts | 6 +- src/queues/queues.service.ts | 406 ++++++++++++++++++----------------- 2 files changed, 212 insertions(+), 200 deletions(-) diff --git a/src/queues/queues.module.ts b/src/queues/queues.module.ts index b2ab21be..0a9c943f 100644 --- a/src/queues/queues.module.ts +++ b/src/queues/queues.module.ts @@ -11,6 +11,8 @@ import { QueueType } from '@/queue-types/entities/queue-type.entity'; import { Product } from '@/products/entities/product.entity'; import { Material } from '@/materials/entities/material.entity'; import { ProductionTarget } from '@/production_targets/entities/production_target.entity'; +import { Recipe } from '@/recipes/entities/recipe.entity'; +import { RecipeIngredient } from '@/recipe_ingredients/entities/recipe_ingredient.entity'; @Module({ imports: [ @@ -23,7 +25,9 @@ import { ProductionTarget } from '@/production_targets/entities/production_targe QueueType, Product, Material, - ProductionTarget + ProductionTarget, + Recipe, + RecipeIngredient, ]), // ✅ เพิ่ม Entity ที่เกี่ยวข้อง ], controllers: [QueuesController], diff --git a/src/queues/queues.service.ts b/src/queues/queues.service.ts index 8d9b2fe8..605fbf31 100644 --- a/src/queues/queues.service.ts +++ b/src/queues/queues.service.ts @@ -17,15 +17,13 @@ import { QueueType } from '@/queue-types/entities/queue-type.entity'; import { Product } from '@/products/entities/product.entity'; import { Material } from '@/materials/entities/material.entity'; import { ProductionTarget } from '@/production_targets/entities/production_target.entity'; +import { Recipe } from '@/recipes/entities/recipe.entity'; +import { RecipeIngredient } from '@/recipe_ingredients/entities/recipe_ingredient.entity'; @Injectable() export class QueuesService { private readonly logger = new Logger(QueuesService.name); - deleteByDate( - dateStr: string, - ): { deletedCount: number } | PromiseLike<{ deletedCount: number }> { - throw new Error('Method not implemented.'); - } + constructor( @InjectRepository(Queue) private readonly queueRepository: Repository<Queue>, @@ -42,7 +40,7 @@ export class QueuesService { @InjectRepository(Employee) private readonly employeeRepository: Repository<Employee>, - @InjectRepository(QueueType) // ✅ เพิ่มบรรทัดนี้ + @InjectRepository(QueueType) private readonly queueTypeRepository: Repository<QueueType>, @InjectRepository(Product) @@ -53,27 +51,104 @@ export class QueuesService { @InjectRepository(ProductionTarget) private readonly productionTargetRepository: Repository<ProductionTarget>, + + @InjectRepository(Recipe) + private readonly recipeRepository: Repository<Recipe>, + + @InjectRepository(RecipeIngredient) + private readonly recipeIngredientRepository: Repository<RecipeIngredient>, ) {} - async getLatestQueuesFromDate(dateStr?: string): Promise<Queue[]> { - this.logger.log( - `📡 Fetching queues from ${dateStr || 'the most recent date'}`, - ); + // ============================================================================ + // ฟังก์ชันใหม่: _reverseDeduction() ที่ return ทั้ง leftover และ totalProduced + // ============================================================================ + // _reverseDeduction() แบบใหม่ ที่รองรับ grouping ตาม OrderID + private async _reverseDeduction(newlyCreatedQueues: Queue[]) { + // สร้าง map เพื่อเก็บยอดผลิตและยอดที่ถูกใช้สำหรับแต่ละ global key + // globalKey = `${orderId}|${itemType}-${itemID}` + type GroupEntry = { + orderId: number | null; + key: string; + totalProduced: number; + totalUsed: number; + }; + const groupMap = new Map<string, GroupEntry>(); + + // 1. รวมยอดผลิตจากแต่ละ Queue (direct production) + for (const q of newlyCreatedQueues) { + const orderId = q.order ? q.order.OrderID : null; + const itemKey = `${q.itemType}-${q.itemID}`; + const globalKey = `${orderId}|${itemKey}`; + const producedQty = q.producedQuantity || 0; + const current = groupMap.get(globalKey) || { + orderId, + key: itemKey, + totalProduced: 0, + totalUsed: 0, + }; + current.totalProduced += producedQty; + groupMap.set(globalKey, current); + } - let startDate: Date; + // 2. โหลด Recipe ทั้งหมด (เชื่อม ingredients) + const recipes = await this.recipeRepository.find({ + relations: ['ingredients', 'ingredients.material'], + }); + const recipeMap = new Map<string, Recipe>(); + for (const r of recipes) { + const outKey = `${r.outputItemType}-${r.outputItemID}`; + recipeMap.set(outKey, r); + } + + // 3. สำหรับแต่ละ Queue ถ้ามี Recipe → หักยอด input material (เฉพาะ Material) + for (const q of newlyCreatedQueues) { + const outKey = `${q.itemType}-${q.itemID}`; + const recipe = recipeMap.get(outKey); + if (recipe && recipe.ingredients && recipe.ingredients.length > 0) { + const producedQty = q.producedQuantity || 0; + for (const ing of recipe.ingredients) { + const ingKey = `MATERIAL-${ing.material.MaterialID}`; + const usedQty = (ing.quantityNeed || 0) * producedQty; + const orderId = q.order ? q.order.OrderID : null; + const globalKey = `${orderId}|${ingKey}`; + const current = groupMap.get(globalKey) || { + orderId, + key: ingKey, + totalProduced: 0, + totalUsed: 0, + }; + current.totalUsed += usedQty; + groupMap.set(globalKey, current); + } + } + } + + // 4. คำนวณ leftover = totalProduced - totalUsed (ถ้าติดลบ ให้เป็น 0) + const results = Array.from(groupMap.values()).map((entry) => ({ + orderId: entry.orderId, + key: entry.key, // ex: "MATERIAL-12" or "PRODUCT-2" + leftover: + entry.totalProduced - entry.totalUsed < 0 + ? 0 + : entry.totalProduced - entry.totalUsed, + totalProduced: entry.totalProduced, + })); + return results; + } + + // ============================================================================ + async getLatestQueuesFromDate(dateStr?: string): Promise<Queue[]> { + let startDate: Date; if (dateStr) { - // ✅ ถ้ามี `dateStr` ให้ใช้เป็นวันที่เริ่มต้น const parts = dateStr.includes('/') ? dateStr.trim().split('/') : dateStr.trim().split('-'); - if (parts.length !== 3) { throw new BadRequestException( 'Invalid date format. Use dd/MM/yyyy or yyyy-MM-dd', ); } - if (dateStr.includes('/')) { const [day, month, year] = parts.map(Number); startDate = new Date(year, month - 1, day); @@ -82,21 +157,17 @@ export class QueuesService { startDate = new Date(year, month - 1, day); } } else { - // ✅ ถ้าไม่ส่ง `dateStr` มา → ใช้วันที่ล่าสุดแทน const latestExistingQueue = await this.queueRepository.find({ order: { createdAt: 'DESC' }, take: 1, }); - if (!latestExistingQueue.length) { this.logger.warn(`⚠️ No queues found in the system`); return []; } - startDate = new Date(latestExistingQueue[0].createdAt); } - - startDate.setHours(0, 0, 0, 0); // ตั้งเป็น 00:00 ของวันนั้น + startDate.setHours(0, 0, 0, 0); this.logger.log( `🔍 Fetching all queues from ${startDate.toISOString()} onwards`, @@ -104,7 +175,7 @@ export class QueuesService { return await this.queueRepository.find({ where: { createdAt: MoreThanOrEqual(startDate) }, - order: { createdAt: 'ASC' }, // เรียงจากเก่าสุดไปใหม่สุด + order: { createdAt: 'ASC' }, }); } @@ -160,9 +231,8 @@ export class QueuesService { if (!queueType) throw new NotFoundException(`QueueType '${queueTypeName}' not found`); - // ✅ ตรวจสอบว่า itemID และ itemType มีอยู่จริง + // ตรวจสอบ itemID / itemType let validItem = null; - if (itemType === 'PRODUCT') { validItem = await this.productRepository.findOne({ where: { ProductID: itemID }, @@ -181,25 +251,20 @@ export class QueuesService { throw new BadRequestException(`Invalid itemType: ${itemType}`); } - // ✅ เตรียม employees + // เตรียม employees let employees = []; - if (Array.isArray(EmployeeIds) && EmployeeIds.length > 0) { const validEmployeeIds = EmployeeIds.map((id) => Number(id)).filter( (id) => !isNaN(id) && id > 0, ); - if (validEmployeeIds.length !== EmployeeIds.length) { throw new BadRequestException(`Invalid EmployeeIds provided`); } - employees = await this.employeeRepository.findByIds(validEmployeeIds); - const foundIds = employees.map((e) => e.EmployeeID); const missingIds = validEmployeeIds.filter( (id) => !foundIds.includes(id), ); - if (missingIds.length > 0) { throw new NotFoundException( `Employees not found: ${missingIds.join(', ')}`, @@ -207,7 +272,7 @@ export class QueuesService { } } - // ✅ สร้าง Queue object พร้อม itemID + itemType + // สร้าง Queue object const newQueue = this.queueRepository.create({ ...queueData, machine, @@ -227,14 +292,18 @@ export class QueuesService { }; } + // ============================================================================ + // createMultiple: สร้างหลาย queue + productionTargets + reverseDeduction + // ============================================================================ async createMultiple(createQueueDtos: CreateQueueDto[]) { - const newQueues = []; - if (!Array.isArray(createQueueDtos)) { throw new BadRequestException('Request body must be an array of queues'); } - - for (const createQueueDto of createQueueDtos) { + + const newQueues: Queue[] = []; + + // --- STEP 1: เตรียม Queues --- + for (const dto of createQueueDtos) { const { MachineID, PageID, @@ -245,177 +314,142 @@ export class QueuesService { startTime, finishTime, ...queueData - } = createQueueDto; - - const machineId = Number(MachineID); - const orderId = Number(OrderID); - const pageId = Number(PageID); - - if (isNaN(machineId)) - throw new BadRequestException(`Invalid MachineID: ${MachineID}`); - if (isNaN(orderId)) - throw new BadRequestException(`Invalid OrderID: ${OrderID}`); - if (isNaN(pageId)) - throw new BadRequestException(`Invalid PageID: ${PageID}`); - - const machine = await this.machineRepository.findOne({ - where: { MachineID: machineId }, + } = dto; + + const machine = await this.machineRepository.findOneByOrFail({ + MachineID: Number(MachineID), }); - if (!machine) - throw new NotFoundException(`Machine with ID ${machineId} not found`); - - const page = await this.pageRepository.findOne({ - where: { PageID: pageId }, + const page = await this.pageRepository.findOneByOrFail({ + PageID: Number(PageID), }); - if (!page) - throw new NotFoundException(`Page with ID ${pageId} not found`); - - let order = null; - let queueTypeName = 'ORDER'; - - if (orderId === -1) { - queueTypeName = 'ผลิตเผื่อ'; - } else { - order = await this.orderRepository.findOne({ - where: { OrderID: orderId }, - }); - if (!order) - throw new NotFoundException(`Order with ID ${orderId} not found`); - } - - const queueType = await this.queueTypeRepository.findOne({ - where: { TypeName: queueTypeName }, + const order = + Number(OrderID) === -1 + ? null + : await this.orderRepository.findOneByOrFail({ + OrderID: Number(OrderID), + }); + const queueType = await this.queueTypeRepository.findOneByOrFail({ + TypeName: order ? 'ORDER' : 'ผลิตเผื่อ', }); - if (!queueType) - throw new NotFoundException(`QueueType '${queueTypeName}' not found`); - - let validEmployeeIds: number[] = []; - let employees = []; - - if (Array.isArray(EmployeeIds) && EmployeeIds.length > 0) { - validEmployeeIds = EmployeeIds.map((id) => Number(id)).filter( - (id) => !isNaN(id) && id > 0, - ); - - if (validEmployeeIds.length !== EmployeeIds.length) { - throw new BadRequestException( - `Invalid EmployeeIds provided: ${EmployeeIds}`, - ); - } - - employees = await this.employeeRepository.findByIds(validEmployeeIds); - - const foundIds = employees.map((e) => e.EmployeeID); - const missingIds = validEmployeeIds.filter( - (id) => !foundIds.includes(id), - ); - - if (missingIds.length > 0) { - throw new NotFoundException( - `Employees not found: ${missingIds.join(', ')}`, - ); - } - } - + + const employees = + Array.isArray(EmployeeIds) && EmployeeIds.length > 0 + ? await this.employeeRepository.findByIds(EmployeeIds.map(Number)) + : []; + + // ตรวจสอบ item if (itemType === 'PRODUCT') { - const product = await this.productRepository.findOne({ - where: { ProductID: itemID }, + await this.productRepository.findOneByOrFail({ + ProductID: Number(itemID), }); - if (!product) { - throw new NotFoundException(`Product with ID ${itemID} not found`); - } } else if (itemType === 'MATERIAL') { - const material = await this.materialRepository.findOne({ - where: { MaterialID: itemID }, + await this.materialRepository.findOneByOrFail({ + MaterialID: Number(itemID), }); - if (!material) { - throw new NotFoundException(`Material with ID ${itemID} not found`); - } } else { throw new BadRequestException(`Invalid itemType: ${itemType}`); } - + const newQueue = this.queueRepository.create({ ...queueData, machine, page, - order: queueTypeName === 'ORDER' ? order : null, + order, employees, QueueType: queueType, - itemID, + itemID: Number(itemID), itemType, startTime: startTime ? new Date(startTime) : undefined, finishTime: finishTime ? new Date(finishTime) : undefined, }); - + newQueues.push(newQueue); } - - const savedQueues = await this.queueRepository.save(newQueues); - - const groupedTargetsMap = new Map<string, ProductionTarget>(); - for (const queue of savedQueues) { - const key = `${queue.order?.OrderID || 'none'}|${queue.itemID}|${queue.itemType}`; + // --- STEP 2: Save Queues --- + const savedQueues = await this.queueRepository.save(newQueues); - const existing = groupedTargetsMap.get(key); + // --- STEP 3: Group Queues by OrderID + item (สร้าง ProductionTarget เดิม) --- + const targetMap = new Map<string, ProductionTarget>(); - const producedQty = queue.producedQuantity || 0; + for (const q of savedQueues) { + const key = `${q.order?.OrderID || 'none'}|${q.itemType}-${q.itemID}`; + const producedQty = q.producedQuantity || 0; + const existing = targetMap.get(key); if (existing) { - // รวม TargetProduced existing.TargetProduced += producedQty; - // ขยายช่วงเวลา start/end (หาค่าน้อยสุด/มากสุด) - if (queue.startTime && (!existing.startTime || queue.startTime < existing.startTime)) { - existing.startTime = queue.startTime; - existing.Date = queue.startTime; // อัปเดตด้วย + if ( + q.startTime && + (!existing.startTime || q.startTime < existing.startTime) + ) { + existing.startTime = q.startTime; + existing.Date = q.startTime; } - - if (queue.finishTime && (!existing.endTime || queue.finishTime > existing.endTime)) { - existing.endTime = queue.finishTime; + if ( + q.finishTime && + (!existing.endTime || q.finishTime > existing.endTime) + ) { + existing.endTime = q.finishTime; } - - // อัปเดตเวลาใหม่ if (existing.startTime && existing.endTime) { existing.totalProductionHours = - (existing.endTime.getTime() - existing.startTime.getTime()) / 3600000; + (existing.endTime.getTime() - existing.startTime.getTime()) / + 3600000; } } else { const target = this.productionTargetRepository.create({ - itemID: queue.itemID, - itemType: queue.itemType, - order: queue.order ?? null, - page: queue.page, - TargetProduced: producedQty, + itemID: q.itemID, + itemType: q.itemType, + order: q.order ?? null, + page: q.page, + TargetProduced: producedQty, // temporary,จะอัปเดตใหม่ ActualProduced: 0, - startTime: queue.startTime, - endTime: queue.finishTime, + startTime: q.startTime, + endTime: q.finishTime, totalProductionHours: - queue.startTime && queue.finishTime - ? (queue.finishTime.getTime() - queue.startTime.getTime()) / 3600000 + q.startTime && q.finishTime + ? (q.finishTime.getTime() - q.startTime.getTime()) / 3600000 : null, Status: 'กำลังรอ', - Date: queue.startTime, + Date: q.startTime, }); - - groupedTargetsMap.set(key, target); + targetMap.set(key, target); } } + const targets = Array.from(targetMap.values()); + await this.productionTargetRepository.save(targets); + + // --- STEP 4: Reverse Deduction (แยกตาม Order) --- + const deduction = await this._reverseDeduction(savedQueues); + // deduction จะได้ array ของ { orderId, key, leftover, totalProduced } + + // สร้าง map จาก deduction ด้วย global key = `${orderId}|${key}` + const leftoverMap = new Map<string, number>(); + for (const entry of deduction) { + const globalKey = `${entry.orderId || 'none'}|${entry.key}`; + leftoverMap.set(globalKey, entry.leftover); + } - const newTargets = Array.from(groupedTargetsMap.values()); + // --- STEP 5: Update ProductionTarget.TargetProduced ให้เป็น leftover --- + for (const target of targets) { + const globalKey = `${target.order?.OrderID || 'none'}|${target.itemType}-${target.itemID}`; + const leftover = leftoverMap.get(globalKey) ?? 0; + target.TargetProduced = leftover; + } + await this.productionTargetRepository.save(targets); - await this.productionTargetRepository.save(newTargets); - return { message: `${savedQueues.length} queues and production targets created successfully`, queues: savedQueues, - Target: newTargets, + Target: targets, + deductionResult: deduction, }; } - - // ✅ Fetch all Queues with relationships + // ============================================================================ + async findAll() { const queues = await this.queueRepository.find({ relations: [ @@ -427,8 +461,6 @@ export class QueuesService { 'QueueType', ], }); - - // Map เพื่อแนบ item เข้าไปในแต่ละ queue for (const queue of queues) { if (queue.itemType === 'PRODUCT') { queue['item'] = await this.productRepository.findOne({ @@ -440,7 +472,6 @@ export class QueuesService { }); } } - return queues; } @@ -456,12 +487,9 @@ export class QueuesService { 'QueueType', ], }); - if (!queue) { throw new NotFoundException(`Queue with ID ${id} not found`); } - - // ✅ แนบ item ให้ด้วยตาม itemType if (queue.itemType === 'PRODUCT') { queue['item'] = await this.productRepository.findOne({ where: { ProductID: queue.itemID }, @@ -471,11 +499,9 @@ export class QueuesService { where: { MaterialID: queue.itemID }, }); } - return queue; } - // ✅ Update Queue async update(id: number, updateQueueDto: UpdateQueueDto) { try { console.log('🔄 Updating Queue ID:', id, updateQueueDto); @@ -490,39 +516,37 @@ export class QueuesService { ...queueData } = updateQueueDto; - // 🔍 ค้นหา Queue ที่จะอัปเดต const existingQueue = await this.queueRepository.findOne({ where: { QueueID: id }, relations: ['machine', 'page', 'order', 'employees'], }); - if (!existingQueue) { throw new NotFoundException(`Queue with ID ${id} not found`); } - // 🔍 Fetch entities if IDs are provided - const machine = MachineID - ? await this.machineRepository.findOne({ where: { MachineID } }) - : existingQueue.machine; - - if (MachineID && !machine) - throw new NotFoundException(`Machine with ID ${MachineID} not found`); - - const page = PageID - ? await this.pageRepository.findOne({ where: { PageID } }) - : existingQueue.page; - - if (PageID && !page) - throw new NotFoundException(`Page with ID ${PageID} not found`); + let machine = existingQueue.machine; + if (MachineID) { + machine = await this.machineRepository.findOne({ + where: { MachineID }, + }); + if (!machine) + throw new NotFoundException(`Machine with ID ${MachineID} not found`); + } - const order = OrderID - ? await this.orderRepository.findOne({ where: { OrderID } }) - : existingQueue.order; + let page = existingQueue.page; + if (PageID) { + page = await this.pageRepository.findOne({ where: { PageID } }); + if (!page) + throw new NotFoundException(`Page with ID ${PageID} not found`); + } - if (OrderID && !order) - throw new NotFoundException(`Order with ID ${OrderID} not found`); + let order = existingQueue.order; + if (OrderID) { + order = await this.orderRepository.findOne({ where: { OrderID } }); + if (!order) + throw new NotFoundException(`Order with ID ${OrderID} not found`); + } - // 🔍 ค้นหา Employees และคำนวณการเชื่อมโยง let employees = existingQueue.employees; if (EmployeeIds) { const fetchedEmployees = @@ -533,7 +557,6 @@ export class QueuesService { employees = fetchedEmployees; } - // ✅ Convert timestamps from string to Date const updateData: Partial<Queue> = { ...queueData, machine, @@ -546,14 +569,12 @@ export class QueuesService { : existingQueue.finishTime, }; - // ✅ ใช้ `save()` แทน `update()` เพื่อรองรับ Many-to-Many (employees) const updatedQueue = await this.queueRepository.save({ ...existingQueue, ...updateData, }); console.log('✅ Queue updated successfully:', updatedQueue); - return updatedQueue; } catch (error) { console.error('❌ Error in updateQueue:', error); @@ -561,19 +582,15 @@ export class QueuesService { } } - // ✅ Delete Queue async remove(id: number) { const queue = await this.queueRepository.findOne({ where: { QueueID: id }, relations: ['machine', 'page', 'order', 'employees'], }); - if (!queue) { throw new NotFoundException(`Queue with ID ${id} not found`); } - await this.queueRepository.delete(id); - return { message: 'Queue deleted successfully', deletedQueue: queue }; } @@ -584,18 +601,15 @@ export class QueuesService { if (!dateStr) throw new BadRequestException('dateStr is required'); - // ✅ แปลง string (dd/MM/yyyy หรือ yyyy-MM-dd) → Date object let date: Date; const parts = dateStr.includes('/') ? dateStr.trim().split('/') : dateStr.trim().split('-'); - if (parts.length !== 3) { throw new BadRequestException( 'Invalid date format. Use dd/MM/yyyy or yyyy-MM-dd', ); } - if (dateStr.includes('/')) { const [day, month, year] = parts.map(Number); date = new Date(year, month - 1, day); @@ -603,24 +617,19 @@ export class QueuesService { const [year, month, day] = parts.map(Number); date = new Date(year, month - 1, day); } - const startOfDay = new Date(date.setHours(0, 0, 0, 0)); - const now = new Date(); - // ✅ ค้นหา Queue ที่ตรงเงื่อนไข const queuesToDelete = await this.queueRepository.find({ where: { startTime: MoreThanOrEqual(startOfDay), createdAt: MoreThanOrEqual(startOfDay), }, }); - if (queuesToDelete.length === 0) { this.logger.warn(`⚠️ No queues matched deletion criteria for ${dateStr}`); return { deletedCount: 0 }; } - // ✅ Log รายละเอียด queue ที่จะลบ this.logger.log( `📌 Queues to be deleted (${queuesToDelete.length} items):`, ); @@ -630,7 +639,6 @@ export class QueuesService { ); }); - // ✅ ลบ queue await this.queueRepository.remove(queuesToDelete); this.logger.log( -- GitLab