From 43529caa25bc457db7ad715825c8aa1c1d98ee6b Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 26 Jan 2026 12:38:27 +0800 Subject: [PATCH] Dashboard: Goods Receipt Status --- .../service/GoodsReceiptStatusService.kt | 111 ++++++++++++++++++ .../dashboard/web/DashboardController.kt | 26 ++++ .../web/models/GoodsReceiptStatusResponse.kt | 12 ++ .../entity/PurchaseOrderRepository.kt | 15 +++ .../modules/qc/entity/QcResultRepository.kt | 27 +++++ 5 files changed, 191 insertions(+) create mode 100644 src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/dashboard/web/DashboardController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt new file mode 100644 index 0000000..7764fcb --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt @@ -0,0 +1,111 @@ +package com.ffii.fpsms.modules.dashboard.service + +import com.ffii.fpsms.modules.dashboard.web.models.GoodsReceiptStatusResponse +import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository +import com.ffii.fpsms.modules.qc.entity.QcResultRepository +import com.ffii.fpsms.modules.stock.entity.StockInLineRepository +import org.springframework.stereotype.Service +import java.time.LocalDate + +@Service +class GoodsReceiptStatusService( + private val purchaseOrderRepository: PurchaseOrderRepository, + private val stockInLineRepository: StockInLineRepository, + private val qcResultRepository: QcResultRepository +) { + private data class Agg( + val supplierId: Long?, + val supplierName: String, + var expectedNoOfDelivery: Int = 0, + val ordersReceivedAtDock: MutableSet = mutableSetOf(), + var itemsInspected: Int = 0, + var itemsWithIqcIssue: Int = 0, + var itemsCompletedPutAway: Int = 0 + ) + + fun getGoodsReceiptStatus(date: LocalDate): List { + val from = date.atStartOfDay() + val to = date.plusDays(1).atStartOfDay() + + val purchaseOrders = purchaseOrderRepository.findAllByEstimatedArrivalDateRange(from, to) + val stockInLines = stockInLineRepository.findByReceiptDateAndDeletedFalse(date) + + val stockInLineIds = stockInLines.mapNotNull { it.id } + val inspectedLineIdSet = if (stockInLineIds.isEmpty()) { + emptySet() + } else { + qcResultRepository + .findDistinctStockInLineIdsByStockInLineIdInAndDeletedFalse(stockInLineIds) + .toSet() + } + val failedLineIdSet = if (stockInLineIds.isEmpty()) { + emptySet() + } else { + qcResultRepository + .findDistinctFailedStockInLineIdsByStockInLineIdInAndDeletedFalse(stockInLineIds) + .toSet() + } + + val bySupplier = mutableMapOf() + + fun upsertAgg(supplierId: Long?, supplierName: String): Agg { + return bySupplier.getOrPut(supplierId) { + Agg(supplierId = supplierId, supplierName = supplierName) + } + } + + // Expected deliveries (by PO estimated arrival date) + purchaseOrders.forEach { po -> + val supplier = po.supplier + val supplierId = supplier?.id + val supplierName = supplier?.name ?: "N/A" + upsertAgg(supplierId, supplierName).expectedNoOfDelivery += 1 + } + + // Receiving / IQC / Put-away (by stock-in receipt date) + stockInLines.forEach { sil -> + val supplier = sil.purchaseOrder?.supplier + val supplierId = supplier?.id + val supplierName = supplier?.name ?: "N/A" + val agg = upsertAgg(supplierId, supplierName) + + val silId = sil.id ?: return@forEach + + // Orders received at dock: count orders with DN + (supplier) lot no entered + val poId = sil.purchaseOrder?.id + if (poId != null && !sil.dnNo.isNullOrBlank() && !sil.productLotNo.isNullOrBlank()) { + agg.ordersReceivedAtDock.add(poId) + } + + // Items inspected: any IQC result recorded + if (inspectedLineIdSet.contains(silId)) { + agg.itemsInspected += 1 + } + + // Items with IQC issue: any failed IQC criteria + if (failedLineIdSet.contains(silId)) { + agg.itemsWithIqcIssue += 1 + } + + // Put-away completed at store: stock-in line completed + if (sil.status.equals("completed", ignoreCase = true)) { + agg.itemsCompletedPutAway += 1 + } + } + + return bySupplier.values + .map { agg -> + GoodsReceiptStatusResponse( + supplierId = agg.supplierId, + supplierName = agg.supplierName, + expectedNoOfDelivery = agg.expectedNoOfDelivery, + noOfOrdersReceivedAtDock = agg.ordersReceivedAtDock.size, + noOfItemsInspected = agg.itemsInspected, + noOfItemsWithIqcIssue = agg.itemsWithIqcIssue, + noOfItemsCompletedPutAwayAtStore = agg.itemsCompletedPutAway + ) + } + .sortedBy { it.supplierName } + } +} + diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/web/DashboardController.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/web/DashboardController.kt new file mode 100644 index 0000000..0270ad5 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/web/DashboardController.kt @@ -0,0 +1,26 @@ +package com.ffii.fpsms.modules.dashboard.web + +import com.ffii.fpsms.modules.dashboard.service.GoodsReceiptStatusService +import com.ffii.fpsms.modules.dashboard.web.models.GoodsReceiptStatusResponse +import org.springframework.format.annotation.DateTimeFormat +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate + +@RestController +@RequestMapping("/dashboard") +class DashboardController( + private val goodsReceiptStatusService: GoodsReceiptStatusService +) { + @GetMapping("/goods-receipt-status") + fun getGoodsReceiptStatus( + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + date: LocalDate? + ): List { + return goodsReceiptStatusService.getGoodsReceiptStatus(date ?: LocalDate.now()) + } +} + diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt new file mode 100644 index 0000000..3ad59cf --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt @@ -0,0 +1,12 @@ +package com.ffii.fpsms.modules.dashboard.web.models + +data class GoodsReceiptStatusResponse( + val supplierId: Long?, + val supplierName: String, + val expectedNoOfDelivery: Int, + val noOfOrdersReceivedAtDock: Int, + val noOfItemsInspected: Int, + val noOfItemsWithIqcIssue: Int, + val noOfItemsCompletedPutAwayAtStore: Int +) + diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt index db24189..707e8c2 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderRepository.kt @@ -6,6 +6,7 @@ import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderInfo import org.springframework.data.domain.Page import org.springframework.stereotype.Repository import java.io.Serializable +import java.time.LocalDateTime import java.util.Optional import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query @@ -19,5 +20,19 @@ interface PurchaseOrderRepository : AbstractRepository { fun findByIdAndDeletedFalse(id: Long): Optional + @Query( + """ + SELECT po FROM PurchaseOrder po + WHERE po.deleted = false + AND po.estimatedArrivalDate IS NOT NULL + AND po.estimatedArrivalDate >= :from + AND po.estimatedArrivalDate < :to + """ + ) + fun findAllByEstimatedArrivalDateRange( + @Param("from") from: LocalDateTime, + @Param("to") to: LocalDateTime + ): List + override fun findAll(pageable: Pageable): Page } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt index 42259e8..0af9e71 100644 --- a/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultRepository.kt @@ -3,10 +3,37 @@ package com.ffii.fpsms.modules.qc.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo import com.ffii.fpsms.modules.stock.entity.StockInLine +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @Repository interface QcResultRepository: AbstractRepository { fun findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId: Long): List fun findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId: Long): List + + @Query( + """ + SELECT DISTINCT qr.stockInLine.id + FROM QcResult qr + WHERE qr.deleted = false + AND qr.stockInLine.id IN :stockInLineIds + """ + ) + fun findDistinctStockInLineIdsByStockInLineIdInAndDeletedFalse( + @Param("stockInLineIds") stockInLineIds: List + ): List + + @Query( + """ + SELECT DISTINCT qr.stockInLine.id + FROM QcResult qr + WHERE qr.deleted = false + AND qr.stockInLine.id IN :stockInLineIds + AND (qr.qcPassed = false OR COALESCE(qr.failQty, 0) > 0) + """ + ) + fun findDistinctFailedStockInLineIdsByStockInLineIdInAndDeletedFalse( + @Param("stockInLineIds") stockInLineIds: List + ): List } \ No newline at end of file