diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt new file mode 100644 index 00000000..40acce88 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt @@ -0,0 +1,32 @@ +package com.ffii.fpsms.modules.pickOrder.entity + +import com.ffii.core.entity.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table + +@Entity +@Table(name = "truck_lane_rename_log") +open class TruckLaneRenameLog : BaseEntity() { + @Column(name = "oldCode", nullable = false, length = 50) + open var oldCode: String = "" + + @Column(name = "newCode", nullable = false, length = 50) + open var newCode: String = "" + + @Column(name = "storeId", nullable = false, length = 10) + open var storeId: String = "" + + @Column(name = "remark", length = 255) + open var remark: String? = null + + /** A clock time, or `mixed` when rows in the bucket did not share one time. */ + @Column(name = "oldDepartureTime", length = 20) + open var oldDepartureTime: String? = null + + @Column(name = "newDepartureTime", length = 20) + open var newDepartureTime: String? = null + + @Column(name = "ticketCount", nullable = false) + open var ticketCount: Int = 0 +} diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLogRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLogRepository.kt new file mode 100644 index 00000000..773915ea --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLogRepository.kt @@ -0,0 +1,9 @@ +package com.ffii.fpsms.modules.pickOrder.entity + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface TruckLaneRenameLogRepository : JpaRepository { + fun findTop50ByDeletedFalseOrderByIdDesc(): List +} diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameRules.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameRules.kt new file mode 100644 index 00000000..6db99169 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameRules.kt @@ -0,0 +1,61 @@ +package com.ffii.fpsms.modules.pickOrder.service + +/** + * Bucket identity shared by the route board and the rename transaction. + * Remark is part of the key only on 4F. Comparison trims, and does not collapse inner spaces. + */ +object TruckLaneRenameRules { + const val RESERVED = "車線-X" + const val MAX_LEN = 50 + + data class BucketRef( + val code: String, + val floor: String, + val remarkKey: String, + ) + + fun floorOf(storeId: String?): String { + val compact = storeId?.trim()?.uppercase()?.replace(" ", "")?.replace("/", "") ?: "" + return if (compact == "4F") "4F" else "2F" + } + + /** Blank, null, and whitespace-only remarks share the empty key. 2F ignores remark. */ + fun remarkKey(floor: String, remark: String?): String { + if (floor != "4F") return "" + return remark?.trim().orEmpty() + } + + /** Null when the name is acceptable. Otherwise a stable error code. */ + fun validateNames(oldName: String, newName: String): String? { + val oldTrim = oldName.trim() + val newTrim = newName.trim() + if (oldTrim.isEmpty() || newTrim.isEmpty()) return "LANE_NAME_EMPTY" + if (newTrim.length > MAX_LEN) return "LANE_NAME_TOO_LONG" + if (oldTrim == RESERVED || newTrim == RESERVED) return "LANE_NAME_RESERVED" + return null + } + + /** + * Same-remark collision ignores case and excludes the source bucket. + * A different remark refuses only when the trimmed new name is exactly the same, + * because tickets have no remark and could not tell the buckets apart. + */ + fun collisionCode(source: BucketRef, newCode: String, live: List): String? { + val next = newCode.trim() + for (other in live) { + if (other.floor != source.floor) continue + val sameSource = other.remarkKey == source.remarkKey && + other.code.trim().equals(source.code.trim(), ignoreCase = true) + if (sameSource) continue + if (other.remarkKey == source.remarkKey && + other.code.trim().equals(next, ignoreCase = true) + ) { + return "LANE_NAME_TAKEN" + } + if (other.remarkKey != source.remarkKey && other.code.trim() == next) { + return "LANE_NAME_REMARK_AMBIGUOUS" + } + } + return null + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt new file mode 100644 index 00000000..74e8e800 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt @@ -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(), + ) + + 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 = + 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): List { + val grouped = linkedMapOf, 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): 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, time: LocalTime?): Map { + 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 = 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): 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() + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneVersionService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneVersionService.kt index c5623d2a..f120a522 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneVersionService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneVersionService.kt @@ -16,6 +16,7 @@ open class TruckLaneVersionService( private val truckLaneVersionRepository: TruckLaneVersionRepository, private val truckLaneVersionLineRepository: TruckLaneVersionLineRepository, private val logisticRepository: LogisticRepository, + private val truckLaneRenameService: TruckLaneRenameService, ) { private fun toResponse(v: TruckLaneVersion): TruckLaneVersionResponse = TruckLaneVersionResponse( @@ -271,6 +272,7 @@ open class TruckLaneVersionService( } } + rewriteOpenSchedulesBackToSnapshot(version, lines) restoreRowsFromLines(version, lines) if (!skipPostSnapshot) { @@ -316,6 +318,7 @@ open class TruckLaneVersionService( return "No snapshot lines for affected lanes (versionId=$versionId)" } + rewriteOpenSchedulesBackToSnapshot(version, scopedLines) val fullSnapshotIds = allLines.mapNotNull { it.truckRowId }.filter { it > 0 }.toSet() for (lane in codes) { val currentLane = truckRepository.findAllByTruckLanceCodeAndDeletedFalse(lane) @@ -340,6 +343,29 @@ open class TruckLaneVersionService( return "Restored ${scopedLines.size} rows in ${codes.size} lane(s) from versionId=$versionId" } + /** + * Open schedule from/to that still use the live name are pointed back at the snapshot name. + * Unfinished delivery tickets are left on the current name. + */ + private fun rewriteOpenSchedulesBackToSnapshot( + version: TruckLaneVersion, + lines: List, + ) { + val ids = lines.mapNotNull { it.truckRowId }.filter { it > 0 }.distinct() + if (ids.isEmpty()) return + val trucks = truckRepository.findAllById(ids).associateBy { it.id } + val seen = HashSet() + for (line in lines) { + val truck = trucks[line.truckRowId] ?: continue + val before = truck.truckLanceCode?.trim().orEmpty() + val after = (line.truckLanceCode ?: version.truckLanceCode)?.trim().orEmpty() + if (before.isEmpty() || after.isEmpty() || before == after) continue + val key = "$before|$after|${truck.storeId}|${truck.remark}" + if (!seen.add(key)) continue + truckLaneRenameService.rewriteOpenScheduleNames(before, after, truck.storeId, truck.remark) + } + } + private fun restoreRowsFromLines( version: TruckLaneVersion, lines: List, diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt index c4b4edb2..59022d4a 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt @@ -18,8 +18,11 @@ import org.springframework.web.multipart.MultipartHttpServletRequest import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest import com.ffii.fpsms.modules.pickOrder.web.models.CreateTruckWithoutShopRequest import com.ffii.fpsms.modules.pickOrder.web.models.UpdateTruckShopDetailsRequest +import com.ffii.fpsms.modules.pickOrder.service.TruckLaneRenameService import com.ffii.fpsms.modules.pickOrder.service.TruckService 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.RenameTruckLaneRequest import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckLane import com.ffii.fpsms.modules.pickOrder.web.models.TruckLaneCombinationResponse import com.ffii.fpsms.modules.pickOrder.web.models.ExportRouteLanesRequest @@ -37,6 +40,7 @@ import jakarta.validation.Valid open class TruckController( private val truckService: TruckService, private val truckRepository: TruckRepository, + private val truckLaneRenameService: TruckLaneRenameService, ) { @@ -469,4 +473,15 @@ open class TruckController( } } + @PostMapping("/previewTruckLaneRename") + open fun previewTruckLaneRename(@RequestBody req: PreviewTruckLaneRenameRequest) = + truckLaneRenameService.preview(req) + + @PostMapping("/renameTruckLane") + open fun renameTruckLane(@RequestBody req: RenameTruckLaneRequest) = + truckLaneRenameService.rename(req) + + @GetMapping("/truckLaneRenameLog") + open fun truckLaneRenameLog() = truckLaneRenameService.recentLogs() + } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt new file mode 100644 index 00000000..b4e9449d --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt @@ -0,0 +1,43 @@ +package com.ffii.fpsms.modules.pickOrder.web.models + +data class PreviewTruckLaneRenameRequest( + val truckLanceCode: String = "", + val storeId: String = "2F", + val remark: String? = null, +) + +data class PreviewTruckLaneRenameResponse( + val unfinishedCount: Int = 0, + val completedCount: Int = 0, + val handlerNames: List = emptyList(), + val openScheduleCount: Int = 0, +) + +data class RenameTruckLaneRequest( + val truckLanceCode: String = "", + val storeId: String = "2F", + val remark: String? = null, + @param:com.fasterxml.jackson.annotation.JsonProperty("newTruckLanceCode") + val replacementTruckLanceCode: String = "", + val departureTimeChanged: Boolean = false, + val newDepartureTime: String? = null, +) + +data class RenameTruckLaneResponse( + val truckLanceCode: String = "", + val ticketCount: Int = 0, + val scheduleLineCount: Int = 0, +) + +data class TruckLaneRenameLogResponse( + val id: Long = 0, + val created: String? = null, + val createdBy: String? = null, + val oldCode: String = "", + val newCode: String = "", + val storeId: String = "", + val remark: String? = null, + val oldDepartureTime: String? = null, + val newDepartureTime: String? = null, + val ticketCount: Int = 0, +) diff --git a/src/main/resources/db/changelog/changes/20261026_truck_lane_rename/01_create_truck_lane_rename_log.sql b/src/main/resources/db/changelog/changes/20261026_truck_lane_rename/01_create_truck_lane_rename_log.sql new file mode 100644 index 00000000..f7f36449 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20261026_truck_lane_rename/01_create_truck_lane_rename_log.sql @@ -0,0 +1,23 @@ +-- liquibase formatted sql +-- changeset fpsms:20261026_01_create_truck_lane_rename_log +-- preconditions onFail:MARK_RAN +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'truck_lane_rename_log' + +CREATE TABLE IF NOT EXISTS `truck_lane_rename_log` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` VARCHAR(30) NULL DEFAULT NULL, + `version` INT NOT NULL DEFAULT '0', + `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modifiedBy` VARCHAR(30) NULL DEFAULT NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT '0', + `oldCode` VARCHAR(50) NOT NULL, + `newCode` VARCHAR(50) NOT NULL, + `storeId` VARCHAR(10) NOT NULL, + `remark` VARCHAR(255) NULL DEFAULT NULL, + `oldDepartureTime` VARCHAR(20) NULL DEFAULT NULL, + `newDepartureTime` VARCHAR(20) NULL DEFAULT NULL, + `ticketCount` INT NOT NULL DEFAULT '0', + CONSTRAINT pk_truck_lane_rename_log PRIMARY KEY (`id`) +);