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 { 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 { 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 { 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 { 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): 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 ) }