| @@ -847,139 +847,90 @@ open class DoPickOrderService( | |||
| * Groups DoPickOrder and DoPickOrderRecord data to provide summary statistics. | |||
| */ | |||
| 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( | |||
| 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 { | |||