| @@ -847,139 +847,90 @@ open class DoPickOrderService( | |||||
| * Groups DoPickOrder and DoPickOrderRecord data to provide summary statistics. | * Groups DoPickOrder and DoPickOrderRecord data to provide summary statistics. | ||||
| */ | */ | ||||
| open fun getTruckScheduleDashboard(targetDate: LocalDate): List<TruckScheduleDashboardResponse> { | open fun getTruckScheduleDashboard(targetDate: LocalDate): List<TruckScheduleDashboardResponse> { | ||||
| // 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<TicketData>() | |||||
| 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<String, Any?>, key: String): String? = row[key]?.toString() | |||||
| fun intVal(row: Map<String, Any?>, 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<String, Any?>, 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<String, Any?>, 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( | 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 { | private fun countFGItemsById(doPickOrderId: Long): Int { | ||||