|
|
@@ -0,0 +1,336 @@ |
|
|
|
|
|
package com.ffii.fpsms.modules.pickOrder.service |
|
|
|
|
|
|
|
|
|
|
|
import com.ffii.core.support.JdbcDao |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.entity.Truck |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.entity.TruckLaneRenameLog |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.entity.TruckLaneRenameLogRepository |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.web.models.PreviewTruckLaneRenameRequest |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.web.models.PreviewTruckLaneRenameResponse |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.web.models.RenameTruckLaneRequest |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.web.models.RenameTruckLaneResponse |
|
|
|
|
|
import com.ffii.fpsms.modules.pickOrder.web.models.TruckLaneRenameLogResponse |
|
|
|
|
|
import org.springframework.http.HttpStatus |
|
|
|
|
|
import org.springframework.stereotype.Service |
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional |
|
|
|
|
|
import org.springframework.web.server.ResponseStatusException |
|
|
|
|
|
import java.time.LocalTime |
|
|
|
|
|
import java.time.format.DateTimeParseException |
|
|
|
|
|
|
|
|
|
|
|
@Service |
|
|
|
|
|
open class TruckLaneRenameService( |
|
|
|
|
|
private val jdbcDao: JdbcDao, |
|
|
|
|
|
private val truckRepository: TruckRepository, |
|
|
|
|
|
private val renameLogRepository: TruckLaneRenameLogRepository, |
|
|
|
|
|
) { |
|
|
|
|
|
@Transactional(readOnly = true) |
|
|
|
|
|
open fun preview(req: PreviewTruckLaneRenameRequest): PreviewTruckLaneRenameResponse { |
|
|
|
|
|
val code = req.truckLanceCode.trim() |
|
|
|
|
|
if (code.isEmpty()) throw badRequest("LANE_NAME_EMPTY") |
|
|
|
|
|
val floor = TruckLaneRenameRules.floorOf(req.storeId) |
|
|
|
|
|
val remarkKey = TruckLaneRenameRules.remarkKey(floor, req.remark) |
|
|
|
|
|
val params = ticketParams(code, floor, remarkKey) |
|
|
|
|
|
return PreviewTruckLaneRenameResponse( |
|
|
|
|
|
unfinishedCount = count(UNFINISHED_COUNT_SQL, params), |
|
|
|
|
|
completedCount = count(COMPLETED_COUNT_SQL, params), |
|
|
|
|
|
handlerNames = jdbcDao.queryForStrings(HANDLER_SQL, params) |
|
|
|
|
|
.map { it.trim() } |
|
|
|
|
|
.filter { it.isNotEmpty() } |
|
|
|
|
|
.distinct(), |
|
|
|
|
|
openScheduleCount = count(OPEN_SCHEDULE_COUNT_SQL, params), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Transactional |
|
|
|
|
|
open fun rename(req: RenameTruckLaneRequest): RenameTruckLaneResponse { |
|
|
|
|
|
val oldCode = req.truckLanceCode.trim() |
|
|
|
|
|
val newCode = req.replacementTruckLanceCode.trim() |
|
|
|
|
|
val nameError = TruckLaneRenameRules.validateNames(oldCode, newCode) |
|
|
|
|
|
if (nameError != null) throw badRequest(nameError) |
|
|
|
|
|
val floor = TruckLaneRenameRules.floorOf(req.storeId) |
|
|
|
|
|
val remarkKey = TruckLaneRenameRules.remarkKey(floor, req.remark) |
|
|
|
|
|
val newTime = if (req.departureTimeChanged) parseTime(req.newDepartureTime) else null |
|
|
|
|
|
if (req.departureTimeChanged && newTime == null) throw badRequest("LANE_TIME_INVALID") |
|
|
|
|
|
|
|
|
|
|
|
jdbcDao.queryForList( |
|
|
|
|
|
"SELECT id FROM truck WHERE deleted = 0 FOR UPDATE", |
|
|
|
|
|
emptyMap<String, Any>(), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
val live = truckRepository.findAllForRouteBoard().filter { it.deleted != true } |
|
|
|
|
|
val sourceRows = live.filter { inBucket(it, oldCode, floor, remarkKey) } |
|
|
|
|
|
if (sourceRows.isEmpty()) throw badRequest("LANE_NOT_FOUND") |
|
|
|
|
|
|
|
|
|
|
|
val refs = liveBuckets(live) |
|
|
|
|
|
val collision = TruckLaneRenameRules.collisionCode( |
|
|
|
|
|
TruckLaneRenameRules.BucketRef(oldCode, floor, remarkKey), |
|
|
|
|
|
newCode, |
|
|
|
|
|
refs, |
|
|
|
|
|
) |
|
|
|
|
|
if (collision != null) throw conflict(collision) |
|
|
|
|
|
|
|
|
|
|
|
val oldTimeLabel = describeTimes(sourceRows.map { it.departureTime }) |
|
|
|
|
|
val casings = sourceRows.map { it.truckLanceCode?.trim().orEmpty() }.filter { it.isNotEmpty() }.distinct() |
|
|
|
|
|
var scheduleCount = 0 |
|
|
|
|
|
var ticketCount = 0 |
|
|
|
|
|
for (casing in casings) { |
|
|
|
|
|
scheduleCount += applyOpenScheduleRename(casing, newCode, floor, remarkKey) |
|
|
|
|
|
val params = ticketParams(casing, floor, remarkKey).toMutableMap() |
|
|
|
|
|
params["newCode"] = newCode |
|
|
|
|
|
ticketCount += jdbcDao.executeUpdate( |
|
|
|
|
|
updateTicketSql(DOPO_UPDATE_SQL, req.departureTimeChanged), |
|
|
|
|
|
withTime(params, newTime), |
|
|
|
|
|
) |
|
|
|
|
|
jdbcDao.executeUpdate( |
|
|
|
|
|
updateTicketSql(PICK_UPDATE_SQL, req.departureTimeChanged), |
|
|
|
|
|
withTime(params, newTime), |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
for (row in sourceRows) { |
|
|
|
|
|
row.truckLanceCode = newCode |
|
|
|
|
|
if (req.departureTimeChanged) row.departureTime = newTime |
|
|
|
|
|
} |
|
|
|
|
|
truckRepository.saveAll(sourceRows) |
|
|
|
|
|
|
|
|
|
|
|
val log = TruckLaneRenameLog() |
|
|
|
|
|
log.oldCode = oldCode |
|
|
|
|
|
log.newCode = newCode |
|
|
|
|
|
log.storeId = floor |
|
|
|
|
|
log.remark = remarkKey.ifEmpty { null } |
|
|
|
|
|
log.oldDepartureTime = oldTimeLabel |
|
|
|
|
|
log.newDepartureTime = if (req.departureTimeChanged) newTime.toString() else oldTimeLabel |
|
|
|
|
|
log.ticketCount = ticketCount |
|
|
|
|
|
renameLogRepository.save(log) |
|
|
|
|
|
|
|
|
|
|
|
return RenameTruckLaneResponse( |
|
|
|
|
|
truckLanceCode = newCode, |
|
|
|
|
|
ticketCount = ticketCount, |
|
|
|
|
|
scheduleLineCount = scheduleCount, |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Restore writes master rows back to the snapshot name and does not move unfinished tickets. |
|
|
|
|
|
* Open schedule from/to that still point at the current name are moved back here. |
|
|
|
|
|
*/ |
|
|
|
|
|
@Transactional |
|
|
|
|
|
open fun rewriteOpenScheduleNames( |
|
|
|
|
|
oldCode: String, |
|
|
|
|
|
newCode: String, |
|
|
|
|
|
storeId: String?, |
|
|
|
|
|
remark: String?, |
|
|
|
|
|
): Int { |
|
|
|
|
|
val floor = TruckLaneRenameRules.floorOf(storeId) |
|
|
|
|
|
val remarkKey = TruckLaneRenameRules.remarkKey(floor, remark) |
|
|
|
|
|
return applyOpenScheduleRename(oldCode.trim(), newCode.trim(), floor, remarkKey) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Transactional(readOnly = true) |
|
|
|
|
|
open fun recentLogs(): List<TruckLaneRenameLogResponse> = |
|
|
|
|
|
renameLogRepository.findTop50ByDeletedFalseOrderByIdDesc().map { row -> |
|
|
|
|
|
TruckLaneRenameLogResponse( |
|
|
|
|
|
id = row.id ?: 0, |
|
|
|
|
|
created = row.created?.toString(), |
|
|
|
|
|
createdBy = row.createdBy, |
|
|
|
|
|
oldCode = row.oldCode, |
|
|
|
|
|
newCode = row.newCode, |
|
|
|
|
|
storeId = row.storeId, |
|
|
|
|
|
remark = row.remark, |
|
|
|
|
|
oldDepartureTime = row.oldDepartureTime, |
|
|
|
|
|
newDepartureTime = row.newDepartureTime, |
|
|
|
|
|
ticketCount = row.ticketCount, |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun applyOpenScheduleRename( |
|
|
|
|
|
oldCode: String, |
|
|
|
|
|
newCode: String, |
|
|
|
|
|
floor: String, |
|
|
|
|
|
remarkKey: String, |
|
|
|
|
|
): Int { |
|
|
|
|
|
if (oldCode.isEmpty() || newCode.isEmpty() || oldCode == newCode) return 0 |
|
|
|
|
|
val params = ticketParams(oldCode, floor, remarkKey).toMutableMap() |
|
|
|
|
|
params["newCode"] = newCode |
|
|
|
|
|
val matched = count(OPEN_SCHEDULE_COUNT_SQL, params) |
|
|
|
|
|
jdbcDao.executeUpdate(SCHEDULE_FROM_SQL, params) |
|
|
|
|
|
jdbcDao.executeUpdate(SCHEDULE_TO_SQL, params) |
|
|
|
|
|
return matched |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun liveBuckets(live: List<Truck>): List<TruckLaneRenameRules.BucketRef> { |
|
|
|
|
|
val grouped = linkedMapOf<Triple<String, String, String>, String>() |
|
|
|
|
|
for (row in live) { |
|
|
|
|
|
val code = row.truckLanceCode?.trim().orEmpty() |
|
|
|
|
|
if (code.isEmpty()) continue |
|
|
|
|
|
val floor = TruckLaneRenameRules.floorOf(row.storeId) |
|
|
|
|
|
val remarkKey = TruckLaneRenameRules.remarkKey(floor, row.remark) |
|
|
|
|
|
val key = Triple(code.lowercase(), floor, remarkKey) |
|
|
|
|
|
grouped.putIfAbsent(key, code) |
|
|
|
|
|
} |
|
|
|
|
|
return grouped.map { (key, code) -> |
|
|
|
|
|
TruckLaneRenameRules.BucketRef(code, key.second, key.third) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun inBucket(row: Truck, code: String, floor: String, remarkKey: String): Boolean { |
|
|
|
|
|
val rowFloor = TruckLaneRenameRules.floorOf(row.storeId) |
|
|
|
|
|
val rowRemark = TruckLaneRenameRules.remarkKey(rowFloor, row.remark) |
|
|
|
|
|
return rowFloor == floor && |
|
|
|
|
|
rowRemark == remarkKey && |
|
|
|
|
|
row.truckLanceCode?.trim().orEmpty().equals(code, ignoreCase = true) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun describeTimes(times: List<LocalTime?>): String { |
|
|
|
|
|
val labels = times.map { it?.withNano(0)?.toString() ?: "" }.distinct() |
|
|
|
|
|
return if (labels.size <= 1) labels.firstOrNull().orEmpty() else "mixed" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun parseTime(raw: String?): LocalTime? { |
|
|
|
|
|
val text = raw?.trim().orEmpty() |
|
|
|
|
|
if (text.isEmpty()) return null |
|
|
|
|
|
val normalized = if (text.length == 5) "$text:00" else text |
|
|
|
|
|
return try { |
|
|
|
|
|
LocalTime.parse(normalized) |
|
|
|
|
|
} catch (_: DateTimeParseException) { |
|
|
|
|
|
null |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun withTime(params: MutableMap<String, Any?>, time: LocalTime?): Map<String, Any?> { |
|
|
|
|
|
if (time != null) params["newTime"] = time.toString() |
|
|
|
|
|
return params |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun updateTicketSql(base: String, withTime: Boolean): String { |
|
|
|
|
|
if (!withTime) return base |
|
|
|
|
|
return base.replace("SET colCode = :newCode", "SET colCode = :newCode, colTime = :newTime") |
|
|
|
|
|
.replace("SET TruckLanceCode = :newCode", "SET TruckLanceCode = :newCode, truck_departure_time = :newTime") |
|
|
|
|
|
.replace("SET truckLanceCode = :newCode", "SET truckLanceCode = :newCode, truckDepartureTime = :newTime") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun ticketParams(code: String, floor: String, remarkKey: String): Map<String, Any?> = mapOf( |
|
|
|
|
|
"oldCode" to code, |
|
|
|
|
|
"floor" to floor, |
|
|
|
|
|
"remark" to remarkKey, |
|
|
|
|
|
"blankRemark" to if (remarkKey.isEmpty()) 1 else 0, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
private fun count(sql: String, params: Map<String, Any?>): Int { |
|
|
|
|
|
val row = jdbcDao.queryForList(sql, params).firstOrNull() ?: return 0 |
|
|
|
|
|
val value = row.values.firstOrNull() |
|
|
|
|
|
return (value as? Number)?.toInt() ?: 0 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun badRequest(code: String) = ResponseStatusException(HttpStatus.BAD_REQUEST, code) |
|
|
|
|
|
|
|
|
|
|
|
private fun conflict(code: String) = ResponseStatusException(HttpStatus.CONFLICT, code) |
|
|
|
|
|
|
|
|
|
|
|
companion object { |
|
|
|
|
|
private const val FLOOR_MATCH = """ |
|
|
|
|
|
( |
|
|
|
|
|
(:floor = '4F' AND UPPER(REPLACE(TRIM(storeCol), ' ', '')) IN ('4F', '4/F')) |
|
|
|
|
|
OR (:floor <> '4F' AND UPPER(REPLACE(TRIM(storeCol), ' ', '')) IN ('2F', '2/F')) |
|
|
|
|
|
) |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
private const val REMARK_MATCH_4F = """ |
|
|
|
|
|
( |
|
|
|
|
|
:floor <> '4F' |
|
|
|
|
|
OR ( |
|
|
|
|
|
(:blankRemark = 1 AND (remarkCol IS NULL OR TRIM(remarkCol) = '')) |
|
|
|
|
|
OR (:blankRemark = 0 AND TRIM(remarkCol) = :remark) |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
private val UNFINISHED_COUNT_SQL = """ |
|
|
|
|
|
SELECT COUNT(*) AS cnt |
|
|
|
|
|
FROM delivery_order_pick_order |
|
|
|
|
|
WHERE deleted = 0 |
|
|
|
|
|
AND ticketStatus IN ('pending', 'released') |
|
|
|
|
|
AND TRIM(truckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "storeId")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val COMPLETED_COUNT_SQL = """ |
|
|
|
|
|
SELECT COUNT(*) AS cnt |
|
|
|
|
|
FROM delivery_order_pick_order |
|
|
|
|
|
WHERE deleted = 0 |
|
|
|
|
|
AND ticketStatus = 'completed' |
|
|
|
|
|
AND TRIM(truckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "storeId")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val HANDLER_SQL = """ |
|
|
|
|
|
SELECT DISTINCT TRIM(handlerName) |
|
|
|
|
|
FROM delivery_order_pick_order |
|
|
|
|
|
WHERE deleted = 0 |
|
|
|
|
|
AND ticketStatus IN ('pending', 'released') |
|
|
|
|
|
AND handledBy IS NOT NULL |
|
|
|
|
|
AND handlerName IS NOT NULL |
|
|
|
|
|
AND TRIM(handlerName) <> '' |
|
|
|
|
|
AND TRIM(truckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "storeId")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val DOPO_UPDATE_SQL = """ |
|
|
|
|
|
UPDATE delivery_order_pick_order |
|
|
|
|
|
SET truckLanceCode = :newCode, version = version + 1 |
|
|
|
|
|
WHERE deleted = 0 |
|
|
|
|
|
AND ticketStatus IN ('pending', 'released') |
|
|
|
|
|
AND TRIM(truckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "storeId")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val PICK_UPDATE_SQL = """ |
|
|
|
|
|
UPDATE do_pick_order |
|
|
|
|
|
SET TruckLanceCode = :newCode, version = version + 1 |
|
|
|
|
|
WHERE deleted = 0 |
|
|
|
|
|
AND ticket_status IN ('pending', 'released') |
|
|
|
|
|
AND TRIM(TruckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "store_id")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val OPEN_SCHEDULE_COUNT_SQL = """ |
|
|
|
|
|
SELECT COUNT(*) AS cnt |
|
|
|
|
|
FROM truck_lane_schedule_line l |
|
|
|
|
|
JOIN truck_lane_schedule s ON s.id = l.scheduleId |
|
|
|
|
|
WHERE l.deleted = 0 AND s.deleted = 0 |
|
|
|
|
|
AND s.status NOT IN ('APPLIED', 'CANCELLED') |
|
|
|
|
|
AND ( |
|
|
|
|
|
( |
|
|
|
|
|
TRIM(l.fromTruckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "l.fromStoreId")} |
|
|
|
|
|
AND ${REMARK_MATCH_4F.replace("remarkCol", "l.fromRemark")} |
|
|
|
|
|
) |
|
|
|
|
|
OR ( |
|
|
|
|
|
TRIM(l.toTruckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "l.toStoreId")} |
|
|
|
|
|
AND ${REMARK_MATCH_4F.replace("remarkCol", "l.toRemark")} |
|
|
|
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val SCHEDULE_FROM_SQL = """ |
|
|
|
|
|
UPDATE truck_lane_schedule_line l |
|
|
|
|
|
JOIN truck_lane_schedule s ON s.id = l.scheduleId |
|
|
|
|
|
SET l.fromTruckLanceCode = :newCode, l.version = l.version + 1 |
|
|
|
|
|
WHERE l.deleted = 0 AND s.deleted = 0 |
|
|
|
|
|
AND s.status NOT IN ('APPLIED', 'CANCELLED') |
|
|
|
|
|
AND TRIM(l.fromTruckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "l.fromStoreId")} |
|
|
|
|
|
AND ${REMARK_MATCH_4F.replace("remarkCol", "l.fromRemark")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
|
|
|
|
private val SCHEDULE_TO_SQL = """ |
|
|
|
|
|
UPDATE truck_lane_schedule_line l |
|
|
|
|
|
JOIN truck_lane_schedule s ON s.id = l.scheduleId |
|
|
|
|
|
SET l.toTruckLanceCode = :newCode, l.version = l.version + 1 |
|
|
|
|
|
WHERE l.deleted = 0 AND s.deleted = 0 |
|
|
|
|
|
AND s.status NOT IN ('APPLIED', 'CANCELLED') |
|
|
|
|
|
AND TRIM(l.toTruckLanceCode) = :oldCode |
|
|
|
|
|
AND ${FLOOR_MATCH.replace("storeCol", "l.toStoreId")} |
|
|
|
|
|
AND ${REMARK_MATCH_4F.replace("remarkCol", "l.toRemark")} |
|
|
|
|
|
""".trimIndent() |
|
|
|
|
|
} |
|
|
|
|
|
} |