250 lines
10 KiB
Kotlin
250 lines
10 KiB
Kotlin
package com.android.trisolarisserver.controller
|
|
|
|
import com.android.trisolarisserver.component.PropertyAccess
|
|
import com.android.trisolarisserver.controller.dto.RoomAvailabilityRangeResponse
|
|
import com.android.trisolarisserver.controller.dto.RoomAvailabilityResponse
|
|
import com.android.trisolarisserver.controller.dto.RoomBoardResponse
|
|
import com.android.trisolarisserver.controller.dto.RoomBoardStatus
|
|
import com.android.trisolarisserver.controller.dto.RoomResponse
|
|
import com.android.trisolarisserver.controller.dto.RoomUpsertRequest
|
|
import com.android.trisolarisserver.repo.PropertyRepo
|
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
|
import com.android.trisolarisserver.repo.RoomRepo
|
|
import com.android.trisolarisserver.repo.RoomStayRepo
|
|
import com.android.trisolarisserver.repo.RoomTypeRepo
|
|
import com.android.trisolarisserver.models.room.Room
|
|
import com.android.trisolarisserver.models.property.Role
|
|
import com.android.trisolarisserver.security.MyPrincipal
|
|
import org.springframework.http.HttpStatus
|
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
|
import org.springframework.web.bind.annotation.GetMapping
|
|
import org.springframework.web.bind.annotation.PathVariable
|
|
import org.springframework.web.bind.annotation.PostMapping
|
|
import org.springframework.web.bind.annotation.PutMapping
|
|
import org.springframework.web.bind.annotation.RequestBody
|
|
import org.springframework.web.bind.annotation.RequestMapping
|
|
import org.springframework.web.bind.annotation.ResponseStatus
|
|
import org.springframework.web.bind.annotation.RestController
|
|
import org.springframework.web.server.ResponseStatusException
|
|
import java.time.LocalDate
|
|
import java.time.ZoneId
|
|
import java.util.UUID
|
|
|
|
@RestController
|
|
@RequestMapping("/properties/{propertyId}/rooms")
|
|
class Rooms(
|
|
private val propertyAccess: PropertyAccess,
|
|
private val roomRepo: RoomRepo,
|
|
private val roomStayRepo: RoomStayRepo,
|
|
private val propertyRepo: PropertyRepo,
|
|
private val roomTypeRepo: RoomTypeRepo,
|
|
private val propertyUserRepo: PropertyUserRepo
|
|
) {
|
|
|
|
@GetMapping
|
|
fun listRooms(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): List<RoomResponse> {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
|
|
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
|
|
if (isAgentOnly(roles)) {
|
|
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
|
|
return rooms
|
|
.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
|
|
.map { it.toRoomResponse() }
|
|
}
|
|
return rooms
|
|
.map { it.toRoomResponse() }
|
|
}
|
|
|
|
@GetMapping("/board")
|
|
fun roomBoard(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): List<RoomBoardResponse> {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
|
|
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
|
|
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
|
|
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
|
|
|
|
val mapped = rooms.map { room ->
|
|
val status = when {
|
|
room.maintenance -> RoomBoardStatus.MAINTENANCE
|
|
!room.active -> RoomBoardStatus.INACTIVE
|
|
occupiedRoomIds.contains(room.id) -> RoomBoardStatus.OCCUPIED
|
|
else -> RoomBoardStatus.FREE
|
|
}
|
|
RoomBoardResponse(
|
|
roomNumber = room.roomNumber,
|
|
roomTypeName = room.roomType.name,
|
|
status = status
|
|
)
|
|
}
|
|
return if (isAgentOnly(roles)) mapped.filter { it.status == RoomBoardStatus.FREE } else mapped
|
|
}
|
|
|
|
@GetMapping("/availability")
|
|
fun roomAvailability(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): List<RoomAvailabilityResponse> {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
|
|
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
|
|
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet()
|
|
|
|
val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
|
|
val grouped = freeRooms.groupBy { it.roomType.name }
|
|
return grouped.entries.map { (typeName, roomList) ->
|
|
RoomAvailabilityResponse(
|
|
roomTypeName = typeName,
|
|
freeRoomNumbers = roomList.map { it.roomNumber }
|
|
)
|
|
}.sortedBy { it.roomTypeName }
|
|
}
|
|
|
|
@GetMapping("/availability-range")
|
|
fun roomAvailabilityRange(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@org.springframework.web.bind.annotation.RequestParam("from") from: String,
|
|
@org.springframework.web.bind.annotation.RequestParam("to") to: String
|
|
): List<RoomAvailabilityRangeResponse> {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
|
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
|
|
val fromDate = parseDate(from)
|
|
val toDate = parseDate(to)
|
|
if (!toDate.isAfter(fromDate)) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range")
|
|
}
|
|
val zone = ZoneId.of(property.timezone)
|
|
val fromAt = fromDate.atStartOfDay(zone).toOffsetDateTime()
|
|
val toAt = toDate.atStartOfDay(zone).toOffsetDateTime()
|
|
|
|
val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId)
|
|
val occupiedRoomIds = roomStayRepo.findOccupiedRoomIdsBetween(propertyId, fromAt, toAt).toHashSet()
|
|
|
|
val freeRooms = rooms.filter { it.active && !it.maintenance && !occupiedRoomIds.contains(it.id) }
|
|
val grouped = freeRooms.groupBy { it.roomType.name }
|
|
return grouped.entries.map { (typeName, roomList) ->
|
|
RoomAvailabilityRangeResponse(
|
|
roomTypeName = typeName,
|
|
freeRoomNumbers = roomList.map { it.roomNumber },
|
|
freeCount = roomList.size
|
|
)
|
|
}.sortedBy { it.roomTypeName }
|
|
}
|
|
|
|
@PostMapping
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
fun createRoom(
|
|
@PathVariable propertyId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: RoomUpsertRequest
|
|
): RoomResponse {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
|
|
if (roomRepo.existsByPropertyIdAndRoomNumber(propertyId, request.roomNumber)) {
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
|
|
}
|
|
|
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
|
|
|
val room = Room(
|
|
property = property,
|
|
roomType = roomType,
|
|
roomNumber = request.roomNumber,
|
|
floor = request.floor,
|
|
hasNfc = request.hasNfc,
|
|
active = request.active,
|
|
maintenance = request.maintenance,
|
|
notes = request.notes
|
|
)
|
|
|
|
return roomRepo.save(room).toRoomResponse()
|
|
}
|
|
|
|
@PutMapping("/{roomId}")
|
|
fun updateRoom(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable roomId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: RoomUpsertRequest
|
|
): RoomResponse {
|
|
requirePrincipal(principal)
|
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
|
|
|
val room = roomRepo.findByIdAndPropertyId(roomId, propertyId)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found for property")
|
|
|
|
if (roomRepo.existsByPropertyIdAndRoomNumberAndIdNot(propertyId, request.roomNumber, roomId)) {
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
|
|
}
|
|
|
|
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
|
|
|
room.roomNumber = request.roomNumber
|
|
room.floor = request.floor
|
|
room.roomType = roomType
|
|
room.hasNfc = request.hasNfc
|
|
room.active = request.active
|
|
room.maintenance = request.maintenance
|
|
room.notes = request.notes
|
|
|
|
return roomRepo.save(room).toRoomResponse()
|
|
}
|
|
|
|
private fun requirePrincipal(principal: MyPrincipal?) {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
}
|
|
|
|
private fun isAgentOnly(roles: Set<Role>): Boolean {
|
|
if (!roles.contains(Role.AGENT)) return false
|
|
val privileged = setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.HOUSEKEEPING, Role.FINANCE)
|
|
return roles.none { it in privileged }
|
|
}
|
|
|
|
private fun parseDate(value: String): LocalDate {
|
|
return try {
|
|
LocalDate.parse(value.trim())
|
|
} catch (_: Exception) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date format")
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun Room.toRoomResponse(): RoomResponse {
|
|
val roomId = id ?: throw IllegalStateException("Room id is null")
|
|
val roomTypeId = roomType.id ?: throw IllegalStateException("Room type id is null")
|
|
return RoomResponse(
|
|
id = roomId,
|
|
roomNumber = roomNumber,
|
|
floor = floor,
|
|
roomTypeId = roomTypeId,
|
|
roomTypeName = roomType.name,
|
|
hasNfc = hasNfc,
|
|
active = active,
|
|
maintenance = maintenance,
|
|
notes = notes
|
|
)
|
|
}
|