| @@ -81,6 +81,17 @@ class DoPickOrder { | |||
| @Column(name = "deleted") | |||
| var deleted: Boolean = false | |||
| @Column(name = "pick_order_code", length = 50) | |||
| var pickOrderCode: String? = null | |||
| @Column(name = "delivery_order_code", length = 50) | |||
| var deliveryOrderCode: String? = null | |||
| @Column(name = "loading_sequence") | |||
| var loadingSequence: Int? = null | |||
| @Column(name = "handler_name", length = 100) | |||
| var handlerName: String? = null | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| @@ -96,6 +107,7 @@ class DoPickOrder { | |||
| doOrderId: Long? = null, | |||
| ticketReleaseTime: LocalDateTime? = null, | |||
| shopId: Long? = null, | |||
| handlerName: String? = null, | |||
| handledBy: Long? = null, | |||
| ticketCompleteDateTime: LocalDateTime? = null, | |||
| truckLanceCode: String? = null, | |||
| @@ -103,7 +115,10 @@ class DoPickOrder { | |||
| shopName: String? = null, | |||
| requiredDeliveryDate: LocalDate? = null, | |||
| createdBy: String? = null, | |||
| modifiedBy: String? = null | |||
| modifiedBy: String? = null, | |||
| pickOrderCode: String? = null, | |||
| deliveryOrderCode: String? = null, | |||
| loadingSequence: Int? = null, | |||
| ) { | |||
| this.storeId = storeId | |||
| this.ticketNo = ticketNo | |||
| @@ -113,6 +128,7 @@ class DoPickOrder { | |||
| this.ticketStatus = ticketStatus | |||
| this.truckId = truckId | |||
| this.truckDepartureTime = truckDepartureTime | |||
| this.handlerName = handlerName | |||
| this.shopId = shopId | |||
| this.handledBy = handledBy | |||
| this.ticketCompleteDateTime = ticketCompleteDateTime | |||
| @@ -122,5 +138,8 @@ class DoPickOrder { | |||
| this.requiredDeliveryDate = requiredDeliveryDate | |||
| this.createdBy = createdBy | |||
| this.modifiedBy = modifiedBy | |||
| this.pickOrderCode = pickOrderCode | |||
| this.deliveryOrderCode = deliveryOrderCode | |||
| this.loadingSequence = loadingSequence | |||
| } | |||
| } | |||
| @@ -26,7 +26,14 @@ class DoPickOrderRecord { | |||
| @Column(name = "do_order_id") | |||
| var doOrderId: Long? = null | |||
| @Column(name = "pick_order_code", length = 50) | |||
| var pickOrderCode: String? = null | |||
| @Column(name = "delivery_order_code", length = 50) | |||
| var deliveryOrderCode: String? = null | |||
| @Column(name = "loading_sequence") | |||
| var loadingSequence: Int? = null | |||
| @Enumerated(EnumType.STRING) | |||
| @Column(name = "ticket_status") | |||
| var ticketStatus: DoPickOrderStatus? = null | |||
| @@ -81,7 +88,8 @@ class DoPickOrderRecord { | |||
| @Column(name = "deleted") | |||
| var deleted: Boolean = false | |||
| @Column(name = "handler_name", length = 100) | |||
| var handlerName: String? = null | |||
| // Default constructor for Hibernate | |||
| constructor() | |||
| @@ -94,8 +102,9 @@ class DoPickOrderRecord { | |||
| truckDepartureTime: LocalTime? = null, | |||
| pickOrderId: Long? = null, | |||
| doOrderId: Long? = null, | |||
| shopId: Long? = null, | |||
| ticketReleaseTime: LocalDateTime? = null, | |||
| shopId: Long? = null, | |||
| handlerName: String? = null, | |||
| handledBy: Long? = null, | |||
| ticketCompleteDateTime: LocalDateTime? = null, | |||
| truckLanceCode: String? = null, | |||
| @@ -103,7 +112,10 @@ class DoPickOrderRecord { | |||
| shopName: String? = null, | |||
| requiredDeliveryDate: LocalDate? = null, | |||
| createdBy: String? = null, | |||
| modifiedBy: String? = null | |||
| modifiedBy: String? = null, | |||
| pickOrderCode: String? = null, | |||
| deliveryOrderCode: String? = null, | |||
| loadingSequence: Int? = null, | |||
| ) { | |||
| this.storeId = storeId | |||
| this.ticketNo = ticketNo | |||
| @@ -116,11 +128,15 @@ class DoPickOrderRecord { | |||
| this.shopId = shopId | |||
| this.handledBy = handledBy | |||
| this.ticketCompleteDateTime = ticketCompleteDateTime | |||
| this.handlerName = handlerName | |||
| this.truckLanceCode = truckLanceCode | |||
| this.shopCode = shopCode | |||
| this.shopName = shopName | |||
| this.requiredDeliveryDate = requiredDeliveryDate | |||
| this.createdBy = createdBy | |||
| this.modifiedBy = modifiedBy | |||
| this.pickOrderCode = pickOrderCode | |||
| this.deliveryOrderCode = deliveryOrderCode | |||
| this.loadingSequence = loadingSequence | |||
| } | |||
| } | |||
| @@ -21,4 +21,7 @@ interface DoPickOrderRepository : JpaRepository<DoPickOrder, Long> { | |||
| fun findByPickOrderId(pickOrderId: Long): List<DoPickOrder> | |||
| fun findByTicketStatusIn(statuses: List<DoPickOrderStatus>): List<DoPickOrder> | |||
| // 在 DoPickOrderRepository 中添加这个方法 | |||
| fun findByHandledByAndTicketStatusIn(handledBy: Long, status: List<DoPickOrderStatus>): List<DoPickOrder> | |||
| fun findByStoreIdAndTicketStatusIn(storeId: String, status: List<DoPickOrderStatus>): List<DoPickOrder> | |||
| } | |||
| @@ -563,6 +563,9 @@ val doPickOrderRecord = DoPickOrderRecord( | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| doOrderId = deliveryOrder.id, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = deliveryOrder.deliveryOrderLines.size, | |||
| // ✅ 填充新增字段 | |||
| truckLanceCode = truck?.truckLanceCode, | |||
| shopCode = deliveryOrder.shop?.code, | |||
| @@ -585,6 +588,9 @@ val doPickOrder = DoPickOrder( | |||
| truckDepartureTime = truck?.departureTime, | |||
| shopId = deliveryOrder.shop?.id, | |||
| handledBy = null, | |||
| pickOrderCode = createdPickOrder.code, | |||
| deliveryOrderCode = deliveryOrder.code, | |||
| loadingSequence = deliveryOrder.deliveryOrderLines.size, | |||
| ticketReleaseTime = null, | |||
| // ✅ 填充新增字段 | |||
| truckLanceCode = truck?.truckLanceCode, | |||
| @@ -35,10 +35,15 @@ import java.time.LocalDateTime | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByStoreRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* // ✅ 导入 | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| @Service | |||
| class DoPickOrderService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val userRepository: UserRepository, | |||
| private val pickOrderRepository: PickOrderRepository | |||
| ) { | |||
| fun findReleasedDoPickOrders(): List<DoPickOrder> { | |||
| return doPickOrderRepository.findByTicketStatusIn( | |||
| @@ -107,10 +112,13 @@ class DoPickOrderService( | |||
| // ✅ Updated method to set ticketReleaseTime when assigning order to user | |||
| fun updateHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrder> { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| val user = userRepository.findById(userId).orElse(null) // ✅ 改用 orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| doPickOrders.forEach { | |||
| it.handledBy = userId | |||
| it.handlerName = handlerName | |||
| it.ticketStatus = DoPickOrderStatus.released | |||
| it.ticketReleaseTime = LocalDateTime.now() // ✅ 设置 release time | |||
| it.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| @@ -142,10 +150,13 @@ class DoPickOrderService( | |||
| // ✅ Add method to update DoPickOrderRecord status | |||
| fun updateRecordHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrderRecord> { | |||
| val doPickOrderRecords = doPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| val user = userRepository.findById(userId).orElse(null) // ✅ 改用 orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| doPickOrderRecords.forEach { | |||
| it.handledBy = userId | |||
| it.handlerName = handlerName | |||
| it.ticketStatus = DoPickOrderStatus.released | |||
| it.ticketReleaseTime = LocalDateTime.now() // ✅ 设置 release time | |||
| it.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| return doPickOrderRecordRepository.saveAll(doPickOrderRecords) | |||
| } | |||
| @@ -171,6 +182,121 @@ class DoPickOrderService( | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| fun getSummaryByStore(storeId: String): StoreLaneSummary { | |||
| // ✅ 修改:查询所有状态的订单,不只是 pending | |||
| val allRecords = doPickOrderRepository.findByStoreIdAndTicketStatusIn( | |||
| storeId, | |||
| listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released, DoPickOrderStatus.completed) | |||
| ) | |||
| val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||
| .mapValues { (_, list) -> | |||
| LaneBtn( | |||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||
| unassigned = list.count { it.handledBy == null }, // 未分配的订单数 | |||
| total = list.size // 总订单数(包括已分配和未分配) | |||
| ) | |||
| } | |||
| val timeGroups = grouped.entries | |||
| .groupBy { it.key.first } | |||
| .mapValues { (_, entries) -> | |||
| entries.map { it.value } | |||
| .sortedByDescending { it.unassigned } | |||
| .take(3) | |||
| } | |||
| .filterValues { lanes -> lanes.any { it.unassigned > 0 } } | |||
| .toSortedMap(compareBy { it }) | |||
| .entries.take(4) | |||
| .map { (time, lanes) -> | |||
| LaneRow( | |||
| truckDepartureTime = time?.toString() ?: "", | |||
| lanes = lanes | |||
| ) | |||
| } | |||
| return StoreLaneSummary(storeId = storeId, rows = timeGroups) | |||
| } | |||
| } | |||
| // ✅ 修复:把 assignByLane 移到类里面 | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| val existingOrders = doPickOrderRepository.findByHandledByAndTicketStatusIn( | |||
| request.userId, | |||
| listOf(DoPickOrderStatus.released, DoPickOrderStatus.pending) | |||
| ) | |||
| if (existingOrders.isNotEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "USER_BUSY", name = null, type = null, | |||
| message = "User already has an active pick order. Please complete it first.", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| val candidates = doPickOrderRepository | |||
| .findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| request.storeId, | |||
| DoPickOrderStatus.pending | |||
| ) | |||
| .filter { | |||
| it.handledBy == null && | |||
| it.truckLanceCode == request.truckLanceCode && | |||
| (request.truckDepartureTime == null || | |||
| it.truckDepartureTime?.toString() == request.truckDepartureTime) | |||
| } | |||
| if (candidates.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, code = "NO_ORDERS", name = null, type = null, | |||
| message = "No available orders for lane ${request.truckLanceCode}", | |||
| errorPosition = null, entity = null | |||
| ) | |||
| } | |||
| val firstOrder = candidates.first() | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| // ✅ 更新 do_pick_order | |||
| firstOrder.handledBy = request.userId | |||
| firstOrder.handlerName = handlerName | |||
| firstOrder.ticketStatus = DoPickOrderStatus.released | |||
| firstOrder.ticketReleaseTime = LocalDateTime.now() | |||
| doPickOrderRepository.save(firstOrder) | |||
| // ✅ 同步更新 pick_order 表 | |||
| if (firstOrder.pickOrderId != null) { | |||
| val pickOrder = pickOrderRepository.findById(firstOrder.pickOrderId!!).orElse(null) | |||
| if (pickOrder != null) { | |||
| // ✅ 修复:assignTo 需要 User 对象,不是 Long | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| pickOrder.assignTo = user | |||
| pickOrder.status = PickOrderStatus.RELEASED | |||
| pickOrderRepository.save(pickOrder) | |||
| } | |||
| } | |||
| // 同步更新 record | |||
| val records = doPickOrderRecordRepository.findByPickOrderId(firstOrder.pickOrderId!!) | |||
| records.forEach { | |||
| it.handledBy = request.userId | |||
| it.handlerName = handlerName | |||
| it.ticketStatus = DoPickOrderStatus.released | |||
| it.ticketReleaseTime = LocalDateTime.now() | |||
| } | |||
| doPickOrderRecordRepository.saveAll(records) | |||
| return MessageResponse( | |||
| id = firstOrder.pickOrderId, | |||
| code = "SUCCESS", | |||
| name = null, | |||
| type = null, | |||
| message = "Assigned pick order from lane ${request.truckLanceCode}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "pickOrderId" to firstOrder.pickOrderId, | |||
| "ticketNo" to firstOrder.ticketNo | |||
| ) | |||
| ) | |||
| } | |||
| } // ✅ 类结束 | |||
| @@ -13,6 +13,7 @@ import com.ffii.fpsms.modules.pickOrder.web.models.ConsoPickOrderRequest | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.ConsoPickOrderResponse | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.ReleaseConsoPickOrderRequest | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.SearchPickOrderRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.DoDetailResponse | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.SavePickOrderGroupRequest | |||
| import jakarta.servlet.http.HttpServletRequest | |||
| import jakarta.validation.Valid | |||
| @@ -35,7 +36,7 @@ import java.time.LocalDate | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByStoreRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.* | |||
| @RestController | |||
| @RequestMapping("/doPickOrder") | |||
| class DoPickOrderController( | |||
| @@ -56,4 +57,12 @@ class DoPickOrderController( | |||
| fun getReleasedDoPickOrders(): List<DoPickOrder> { | |||
| return doPickOrderService.findReleasedDoPickOrders() | |||
| } | |||
| @GetMapping("/summary-by-store") | |||
| fun getSummaryByStore(@RequestParam storeId: String): StoreLaneSummary { | |||
| return doPickOrderService.getSummaryByStore(storeId) | |||
| } | |||
| @PostMapping("/assign-by-lane") | |||
| fun assignByLane(@RequestBody request: AssignByLaneRequest): MessageResponse { | |||
| return doPickOrderService.assignByLane(request) | |||
| } | |||
| } | |||
| @@ -29,4 +29,25 @@ data class DoDetailLineResponse( | |||
| val uom: String?, | |||
| val uomCode: String?, | |||
| val shortUom: String?, | |||
| ) | |||
| data class StoreLaneSummary( | |||
| val storeId: String, | |||
| val rows: List<LaneRow> | |||
| ) | |||
| data class LaneRow( | |||
| val truckDepartureTime: String, | |||
| val lanes: List<LaneBtn> | |||
| ) | |||
| data class LaneBtn( | |||
| val truckLanceCode: String, | |||
| val unassigned: Int, | |||
| val total: Int | |||
| ) | |||
| data class AssignByLaneRequest( | |||
| val userId: Long, | |||
| val storeId: String, | |||
| val truckDepartureTime: String?, // 可选:限定出车时间 | |||
| val truckLanceCode: String // 必填:车道编号 | |||
| ) | |||
| @@ -0,0 +1,72 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset enson:altertable_warehouse | |||
| ALTER TABLE do_pick_order_record | |||
| ADD COLUMN pick_order_code VARCHAR(50) NULL AFTER pick_order_id, | |||
| ADD COLUMN delivery_order_code VARCHAR(50) NULL AFTER do_order_id, | |||
| ADD COLUMN handler_name VARCHAR(100) NULL AFTER handled_by, | |||
| ADD COLUMN loading_sequence INT NULL AFTER ShopName; | |||
| ALTER TABLE do_pick_order | |||
| ADD COLUMN pick_order_code VARCHAR(50) NULL AFTER pick_order_id, | |||
| ADD COLUMN delivery_order_code VARCHAR(50) NULL AFTER do_order_id, | |||
| ADD COLUMN handler_name VARCHAR(100) NULL AFTER handled_by, | |||
| ADD COLUMN loading_sequence INT NULL AFTER ShopName; | |||
| ALTER TABLE do_pick_order_record | |||
| MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT FIRST, | |||
| MODIFY COLUMN pick_order_id INT NULL AFTER id, | |||
| MODIFY COLUMN do_order_id INT NULL AFTER pick_order_id, | |||
| MODIFY COLUMN truck_id INT NULL AFTER do_order_id, | |||
| MODIFY COLUMN shop_id INT NULL AFTER truck_id, | |||
| MODIFY COLUMN store_id VARCHAR(10) NULL AFTER shop_id, | |||
| MODIFY COLUMN RequiredDeliveryDate DATE NULL AFTER store_id, | |||
| MODIFY COLUMN truck_departure_time TIME NULL AFTER RequiredDeliveryDate, | |||
| MODIFY COLUMN TruckLanceCode VARCHAR(50) NULL AFTER truck_departure_time, | |||
| MODIFY COLUMN ShopCode VARCHAR(50) NULL AFTER TruckLanceCode, | |||
| MODIFY COLUMN ShopName VARCHAR(255) NULL AFTER ShopCode, | |||
| MODIFY COLUMN loading_sequence INT NULL AFTER ShopName, | |||
| MODIFY COLUMN delivery_order_code VARCHAR(50) NULL AFTER loading_sequence, | |||
| MODIFY COLUMN pick_order_code VARCHAR(50) NULL AFTER delivery_order_code, | |||
| MODIFY COLUMN ticket_no VARCHAR(50) NULL AFTER pick_order_code, | |||
| MODIFY COLUMN ticket_release_time DATETIME NULL AFTER ticket_no, | |||
| MODIFY COLUMN ticketCompleteDateTime DATETIME NULL AFTER ticket_release_time, | |||
| MODIFY COLUMN ticket_status ENUM('pending','released','completed') NULL AFTER ticketCompleteDateTime, | |||
| MODIFY COLUMN handled_by INT NULL AFTER ticket_status, | |||
| MODIFY COLUMN handler_name VARCHAR(100) NULL AFTER handled_by, | |||
| MODIFY COLUMN created DATETIME NULL AFTER handler_name, | |||
| MODIFY COLUMN createdBy VARCHAR(30) NULL AFTER created, | |||
| MODIFY COLUMN version INT NULL AFTER createdBy, | |||
| MODIFY COLUMN modified DATETIME NULL AFTER version, | |||
| MODIFY COLUMN modifiedBy VARCHAR(30) NULL AFTER modified, | |||
| MODIFY COLUMN deleted TINYINT(1) NULL AFTER modifiedBy; | |||
| ALTER TABLE do_pick_order | |||
| MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT FIRST, | |||
| MODIFY COLUMN pick_order_id INT NULL AFTER id, | |||
| MODIFY COLUMN do_order_id INT NULL AFTER pick_order_id, | |||
| MODIFY COLUMN truck_id INT NULL AFTER do_order_id, | |||
| MODIFY COLUMN shop_id INT NULL AFTER truck_id, | |||
| MODIFY COLUMN store_id VARCHAR(10) NULL AFTER shop_id, | |||
| MODIFY COLUMN RequiredDeliveryDate DATE NULL AFTER store_id, | |||
| MODIFY COLUMN truck_departure_time TIME NULL AFTER RequiredDeliveryDate, | |||
| MODIFY COLUMN TruckLanceCode VARCHAR(50) NULL AFTER truck_departure_time, | |||
| MODIFY COLUMN ShopCode VARCHAR(50) NULL AFTER TruckLanceCode, | |||
| MODIFY COLUMN ShopName VARCHAR(255) NULL AFTER ShopCode, | |||
| MODIFY COLUMN loading_sequence INT NULL AFTER ShopName, | |||
| MODIFY COLUMN delivery_order_code VARCHAR(50) NULL AFTER loading_sequence, | |||
| MODIFY COLUMN pick_order_code VARCHAR(50) NULL AFTER delivery_order_code, | |||
| MODIFY COLUMN ticket_no VARCHAR(50) NULL AFTER pick_order_code, | |||
| MODIFY COLUMN ticket_release_time DATETIME NULL AFTER ticket_no, | |||
| MODIFY COLUMN ticketCompleteDateTime DATETIME NULL AFTER ticket_release_time, | |||
| MODIFY COLUMN ticket_status ENUM('pending','released','completed') NULL AFTER ticketCompleteDateTime, | |||
| MODIFY COLUMN handled_by INT NULL AFTER ticket_status, | |||
| MODIFY COLUMN handler_name VARCHAR(100) NULL AFTER handled_by, | |||
| MODIFY COLUMN created DATETIME NULL AFTER handler_name, | |||
| MODIFY COLUMN createdBy VARCHAR(30) NULL AFTER created, | |||
| MODIFY COLUMN version INT NULL AFTER createdBy, | |||
| MODIFY COLUMN modified DATETIME NULL AFTER version, | |||
| MODIFY COLUMN modifiedBy VARCHAR(30) NULL AFTER modified, | |||
| MODIFY COLUMN deleted TINYINT(1) NULL AFTER modifiedBy; | |||