diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckRoutingSummaryService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckRoutingSummaryService.kt new file mode 100644 index 0000000..76bc380 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckRoutingSummaryService.kt @@ -0,0 +1,186 @@ +package com.ffii.fpsms.modules.deliveryOrder.service + +import com.ffii.core.support.JdbcDao +import org.springframework.stereotype.Service + +@Service +class TruckRoutingSummaryService( + private val jdbcDao: JdbcDao, +) { + fun getStoreOptions(): List> { + val sql = """ + SELECT DISTINCT t.Store_id AS value + FROM truck t + WHERE t.deleted = 0 + AND t.Store_id IS NOT NULL + AND TRIM(t.Store_id) <> '' + ORDER BY t.Store_id + """.trimIndent() + + return jdbcDao.queryForList(sql, emptyMap()).map { row -> + val rawValue = (row["value"] ?: "").toString().trim() + val label = when (rawValue.uppercase()) { + "2F", "2/F" -> "2/F" + "4F", "4/F" -> "4/F" + else -> rawValue + } + mapOf("label" to label, "value" to rawValue) + } + } + + fun getLaneOptions(storeId: String?): List> { + val args = mutableMapOf() + val storeSql = if (!storeId.isNullOrBlank()) { + args["storeId"] = storeId.replace("/", "") + "AND REPLACE(t.Store_id, '/', '') = :storeId" + } else { + "" + } + + val sql = """ + SELECT DISTINCT t.TruckLanceCode AS value + FROM truck t + WHERE t.deleted = 0 + AND t.TruckLanceCode IS NOT NULL + AND TRIM(t.TruckLanceCode) <> '' + $storeSql + ORDER BY t.TruckLanceCode + """.trimIndent() + + return jdbcDao.queryForList(sql, args).map { row -> + val value = (row["value"] ?: "").toString().trim() + mapOf("label" to value, "value" to value) + } + } + + fun search(storeId: String?, truckLanceCode: String?, reportDate: String?): List> { + val args = mutableMapOf() + val storeSql = if (!storeId.isNullOrBlank()) { + args["storeId"] = storeId.replace("/", "") + "AND REPLACE(t.Store_id, '/', '') = :storeId" + } else { + "" + } + val laneSql = if (!truckLanceCode.isNullOrBlank()) { + args["truckLanceCode"] = truckLanceCode.trim() + "AND t.TruckLanceCode = :truckLanceCode" + } else { + "" + } + val storeSqlForMax = if (!storeId.isNullOrBlank()) { + "AND REPLACE(t2.Store_id, '/', '') = :storeId" + } else { + "" + } + val laneSqlForMax = if (!truckLanceCode.isNullOrBlank()) { + "AND t2.TruckLanceCode = :truckLanceCode" + } else { + "" + } + val cartonDateSql = if (!reportDate.isNullOrBlank()) { + args["reportDate"] = reportDate.trim() + "AND dpor.RequiredDeliveryDate = :reportDate" + } else { + "" + } + + val sql = """ + SELECT + CAST( + CASE + WHEN seq_max.maxLoading IS NULL OR t.LoadingSequence IS NULL THEN COALESCE(t.LoadingSequence, 0) + ELSE seq_max.maxLoading - t.LoadingSequence + 1 + END AS CHAR + ) AS dropOffSequence, + COALESCE(s.`code`, '') AS shopCode, + TRIM( + CASE + WHEN LOCATE(' - ', COALESCE(s.name, '')) > 0 + THEN SUBSTRING( + COALESCE(s.name, ''), + LOCATE(' - ', COALESCE(s.name, '')) + 3 + ) + ELSE COALESCE(s.name, '') + END + ) AS shopName, + COALESCE(s.addr3, '') AS address, + CAST(COALESCE(carton_sum.qty, 0) AS CHAR) AS noOfCartons + FROM truck t + LEFT JOIN shop s + ON t.shopId = s.id + AND s.deleted = 0 + LEFT JOIN ( + SELECT truck_id, SUM(cartonQty) AS qty + FROM do_pick_order_record dpor + WHERE dpor.deleted = 0 + $cartonDateSql + GROUP BY truck_id + ) carton_sum ON carton_sum.truck_id = t.id + CROSS JOIN ( + SELECT MAX(t2.LoadingSequence) AS maxLoading + FROM truck t2 + WHERE t2.deleted = 0 + $storeSqlForMax + $laneSqlForMax + ) seq_max + WHERE t.deleted = 0 + $storeSql + $laneSql + ORDER BY t.LoadingSequence DESC, COALESCE(s.`code`, t.ShopCode) ASC + """.trimIndent() + + val rows = jdbcDao.queryForList(sql, args) + if (rows.isNotEmpty()) { + return rows + } + + return listOf( + mapOf( + "dropOffSequence" to "", + "shopCode" to "", + "shopName" to "", + "address" to "", + "noOfCartons" to "", + ) + ) + } + + /** + * Earliest departure time among trucks matching store + lane (for PDF header). + */ + fun getDepartureTimeLabel(storeId: String?, truckLanceCode: String?): String { + val args = mutableMapOf() + val storeSql = if (!storeId.isNullOrBlank()) { + args["storeId"] = storeId.replace("/", "") + "AND REPLACE(t.Store_id, '/', '') = :storeId" + } else { + return "" + } + val laneSql = if (!truckLanceCode.isNullOrBlank()) { + args["truckLanceCode"] = truckLanceCode.trim() + "AND t.TruckLanceCode = :truckLanceCode" + } else { + return "" + } + val sql = """ + SELECT DATE_FORMAT(MIN(t.DepartureTime), '%H:%i') AS departureLabel + FROM truck t + WHERE t.deleted = 0 + AND t.DepartureTime IS NOT NULL + $storeSql + $laneSql + """.trimIndent() + val row = jdbcDao.queryForList(sql, args).firstOrNull() + return (row?.get("departureLabel") ?: "").toString().trim() + } + + fun formatStoreIdForDisplay(raw: String?): String { + if (raw.isNullOrBlank()) return "" + val normalized = raw.trim().replace("/", "") + return when (normalized.uppercase()) { + "2F" -> "2/F" + "4F" -> "4/F" + else -> raw.trim() + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/TruckRoutingSummaryController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/TruckRoutingSummaryController.kt new file mode 100644 index 0000000..99d5177 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/TruckRoutingSummaryController.kt @@ -0,0 +1,66 @@ +package com.ffii.fpsms.modules.deliveryOrder.web + +import com.ffii.fpsms.modules.deliveryOrder.service.TruckRoutingSummaryService +import com.ffii.fpsms.modules.report.service.ReportService +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +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 +import java.time.format.DateTimeFormatter + +@RestController +@RequestMapping("/truck-routing-summary") +class TruckRoutingSummaryController( + private val truckRoutingSummaryService: TruckRoutingSummaryService, + private val reportService: ReportService, +) { + @GetMapping("/store-options") + fun getStoreOptions(): List> = + truckRoutingSummaryService.getStoreOptions() + + @GetMapping("/lane-options") + fun getLaneOptions( + @RequestParam(required = false) storeId: String? + ): List> = + truckRoutingSummaryService.getLaneOptions(storeId) + + @GetMapping("/print") + fun print( + @RequestParam(required = false) storeId: String?, + @RequestParam(required = false) truckLanceCode: String?, + @RequestParam(required = false) date: String? + ): ResponseEntity { + val reportDate = if (date.isNullOrBlank()) { + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + } else { + date + } + + val departureLabel = truckRoutingSummaryService.getDepartureTimeLabel(storeId, truckLanceCode) + val lane = (truckLanceCode ?: "").trim() + val params = mutableMapOf( + "FLOOR_LABEL" to truckRoutingSummaryService.formatStoreIdForDisplay(storeId), + "TRUCK_ROUTE" to lane, + "DEPARTURE_TIME" to departureLabel, + "REPORT_DATE" to reportDate, + ) + val rows = truckRoutingSummaryService.search(storeId, truckLanceCode, reportDate) + val pdfBytes = reportService.createPdfResponse( + "/DeliveryNote/TruckRoutingSummaryPDF.jrxml", + params, + rows + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "TruckRoutingSummary.pdf") + set("filename", "TruckRoutingSummary.pdf") + } + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) + } +} diff --git a/src/main/resources/DeliveryNote/TruckRoutingSummaryPDF.jrxml b/src/main/resources/DeliveryNote/TruckRoutingSummaryPDF.jrxml new file mode 100644 index 0000000..1c2b282 --- /dev/null +++ b/src/main/resources/DeliveryNote/TruckRoutingSummaryPDF.jrxml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + <band height="28"> + <staticText> + <reportElement x="0" y="0" width="555" height="24" uuid="c1d2c147-cf58-41e6-9214-3f8251dd8aa5"/> + <textElement textAlignment="Center" verticalAlignment="Middle"> + <font fontName="微軟正黑體" size="16" isBold="true"/> + </textElement> + <text><![CDATA[送貨路線摘要]]></text> + </staticText> + </band> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +