diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt index 9307278..71cdb59 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt @@ -847,139 +847,90 @@ open class DoPickOrderService( * Groups DoPickOrder and DoPickOrderRecord data to provide summary statistics. */ open fun getTruckScheduleDashboard(targetDate: LocalDate): List { - // Fetch all active DoPickOrders for the target date - val doPickOrders = doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( - "2/F", targetDate, listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) - ) + doPickOrderRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( - "4/F", targetDate, listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) - ) - - // Fetch all DoPickOrderRecords for the target date (completed records) - val doPickOrderRecords = doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( - "2/F", targetDate, listOf(DoPickOrderStatus.completed) - ) + doPickOrderRecordRepository.findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( - "4/F", targetDate, listOf(DoPickOrderStatus.completed) - ) - - // Combine both types into a unified data structure for aggregation - data class TicketData( - val storeId: String?, - val truckId: Long?, - val truckLanceCode: String?, - val truckDepartureTime: java.time.LocalTime?, - val shopId: Long?, - val shopCode: String?, - val ticketNo: String?, - val ticketReleaseTime: LocalDateTime?, - val ticketCompleteDateTime: LocalDateTime?, - val ticketStatus: DoPickOrderStatus?, - val doPickOrderId: Long?, - val isRecord: Boolean - ) - - val allTickets = mutableListOf() - - doPickOrders.forEach { dpo -> - allTickets.add(TicketData( - storeId = dpo.storeId, - truckId = dpo.truckId, - truckLanceCode = dpo.truckLanceCode, - truckDepartureTime = dpo.truckDepartureTime, - shopId = dpo.shopId, - shopCode = dpo.shopCode, - ticketNo = dpo.ticketNo, - ticketReleaseTime = dpo.ticketReleaseTime, - ticketCompleteDateTime = dpo.ticketCompleteDateTime, - ticketStatus = dpo.ticketStatus, - doPickOrderId = dpo.id, - isRecord = false - )) - } - - doPickOrderRecords.forEach { record -> - allTickets.add(TicketData( - storeId = record.storeId, - truckId = record.truckId, - truckLanceCode = record.truckLanceCode, - truckDepartureTime = record.truckDepartureTime, - shopId = record.shopId, - shopCode = record.shopCode, - ticketNo = record.ticketNo, - ticketReleaseTime = record.ticketReleaseTime, - ticketCompleteDateTime = record.ticketCompleteDateTime, - ticketStatus = record.ticketStatus, - doPickOrderId = record.recordId, - isRecord = true - )) - } - - // Group by storeId, truckLanceCode, truckDepartureTime - val grouped = allTickets.groupBy { - Triple(it.storeId, it.truckLanceCode, it.truckDepartureTime) - } - - return grouped.map { (key, tickets) -> - val (storeId, truckLanceCode, truckDepartureTime) = key - - // Count distinct shops - val distinctShops = tickets.mapNotNull { it.shopId ?: it.shopCode?.hashCode()?.toLong() }.distinct().size - - // Count distinct tickets - val distinctTickets = tickets.mapNotNull { it.ticketNo }.distinct().size - - // Calculate total items to pick - var totalItems = 0 - tickets.forEach { ticket -> - if (ticket.doPickOrderId != null) { - if (ticket.isRecord) { - totalItems += countFGItemsFromRecordById(ticket.doPickOrderId) - } else { - totalItems += countFGItemsById(ticket.doPickOrderId) - } - } + // Source of truth: delivery_order_pick_order (+ linked pick_order / pick_order_line) + // + // NOTE: delivery_order_pick_order 沒有 truckId 欄位;dashboard 的 truckId 目前僅作為展示/鍵值用途, + // 回傳 null 讓前端保持相容即可。 + val sql = """ + SELECT + dop.storeId AS storeId, + dop.truckLanceCode AS truckLanceCode, + dop.truckDepartureTime AS truckDepartureTime, + COUNT(DISTINCT dop.shopCode) AS numberOfShopsToServe, + COUNT(DISTINCT dop.ticketNo) AS numberOfPickTickets, + COALESCE(SUM(pol_cnt.cnt), 0) AS totalItemsToPick, + SUM(CASE WHEN dop.ticketReleaseTime IS NOT NULL THEN 1 ELSE 0 END) AS numberOfTicketsReleased, + MIN(dop.ticketReleaseTime) AS firstTicketStartTime, + SUM(CASE WHEN dop.ticketCompleteDateTime IS NOT NULL THEN 1 ELSE 0 END) AS numberOfTicketsCompleted, + MAX(dop.ticketCompleteDateTime) AS lastTicketEndTime + FROM fpsmsdb.delivery_order_pick_order dop + LEFT JOIN ( + SELECT + po.deliveryOrderPickOrderId AS dopId, + COUNT(pol.id) AS cnt + FROM fpsmsdb.pick_order po + INNER JOIN fpsmsdb.pick_order_line pol + ON pol.poId = po.id + AND pol.deleted = 0 + WHERE po.deleted = 0 + AND po.deliveryOrderPickOrderId IS NOT NULL + GROUP BY po.deliveryOrderPickOrderId + ) pol_cnt + ON pol_cnt.dopId = dop.id + WHERE dop.deleted = 0 + AND dop.requiredDeliveryDate = :targetDate + AND dop.ticketStatus IN ('pending', 'released', 'completed') + GROUP BY dop.storeId, dop.truckLanceCode, dop.truckDepartureTime + ORDER BY dop.storeId, dop.truckDepartureTime + """.trimIndent() + + val rows = jdbcDao.queryForList(sql, mapOf("targetDate" to targetDate)) + + fun str(row: Map, key: String): String? = row[key]?.toString() + fun intVal(row: Map, key: String): Int = + when (val v = row[key]) { + null -> 0 + is Number -> v.toInt() + else -> v.toString().toBigDecimalOrNull()?.toInt() ?: 0 } - - // Count released tickets (ticketReleaseTime is not null) - val releasedTickets = tickets.count { it.ticketReleaseTime != null } - - // Find first ticket start time (earliest ticketReleaseTime) - val firstTicketStartTime = tickets - .mapNotNull { it.ticketReleaseTime } - .minOrNull() - - // Count completed tickets (ticketCompleteDateTime is not null) - val completedTickets = tickets.count { it.ticketCompleteDateTime != null } - - // Find last ticket end time (latest ticketCompleteDateTime) - val lastTicketEndTime = tickets - .mapNotNull { it.ticketCompleteDateTime } - .maxOrNull() - - // Calculate pick time taken in minutes - val pickTimeTakenMinutes = if (firstTicketStartTime != null && lastTicketEndTime != null) { - ChronoUnit.MINUTES.between(firstTicketStartTime, lastTicketEndTime) - } else { - null + fun timeVal(row: Map, key: String): java.time.LocalTime? = + when (val v = row[key]) { + null -> null + is java.time.LocalTime -> v + is java.sql.Time -> v.toLocalTime() + is java.time.OffsetTime -> v.toLocalTime() + is String -> runCatching { java.time.LocalTime.parse(v) }.getOrNull() + else -> null } - - // Get truck ID (use first non-null) - val truckId = tickets.firstOrNull { it.truckId != null }?.truckId - + fun dtVal(row: Map, key: String): LocalDateTime? = + when (val v = row[key]) { + null -> null + is LocalDateTime -> v + is java.sql.Timestamp -> v.toLocalDateTime() + is String -> runCatching { LocalDateTime.parse(v) }.getOrNull() + else -> null + } + + return rows.map { row -> + val first = dtVal(row, "firstTicketStartTime") + val last = dtVal(row, "lastTicketEndTime") + val minutes = if (first != null && last != null) ChronoUnit.MINUTES.between(first, last) else null + TruckScheduleDashboardResponse( - storeId = storeId, - truckId = truckId, - truckLanceCode = truckLanceCode, - truckDepartureTime = truckDepartureTime, - numberOfShopsToServe = distinctShops, - numberOfPickTickets = distinctTickets, - totalItemsToPick = totalItems, - numberOfTicketsReleased = releasedTickets, - firstTicketStartTime = firstTicketStartTime, - numberOfTicketsCompleted = completedTickets, - lastTicketEndTime = lastTicketEndTime, - pickTimeTakenMinutes = pickTimeTakenMinutes + storeId = str(row, "storeId"), + truckId = null, + truckLanceCode = str(row, "truckLanceCode"), + truckDepartureTime = timeVal(row, "truckDepartureTime"), + numberOfShopsToServe = intVal(row, "numberOfShopsToServe"), + numberOfPickTickets = intVal(row, "numberOfPickTickets"), + totalItemsToPick = intVal(row, "totalItemsToPick"), + numberOfTicketsReleased = intVal(row, "numberOfTicketsReleased"), + firstTicketStartTime = first, + numberOfTicketsCompleted = intVal(row, "numberOfTicketsCompleted"), + lastTicketEndTime = last, + pickTimeTakenMinutes = minutes, ) - }.sortedWith(compareBy({ it.storeId }, { it.truckDepartureTime })) + } } private fun countFGItemsById(doPickOrderId: Long): Int {