more codes

This commit is contained in:
androidlover5842
2026-01-24 22:22:37 +05:30
parent 0d3472c60e
commit 6b6d84e40a
12 changed files with 614 additions and 228 deletions

View File

@@ -0,0 +1,98 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.EmailStorage
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.db.repo.InboundEmailRepo
import com.android.trisolarisserver.models.booking.InboundEmail
import com.android.trisolarisserver.models.booking.InboundEmailStatus
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.repo.PropertyRepo
import com.android.trisolarisserver.security.MyPrincipal
import com.android.trisolarisserver.service.EmailIngestionService
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.text.PDFTextStripper
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import java.time.OffsetDateTime
import java.util.UUID
@RestController
@RequestMapping("/properties/{propertyId}/inbound-emails")
class InboundEmailManual(
private val propertyAccess: PropertyAccess,
private val propertyRepo: PropertyRepo,
private val inboundEmailRepo: InboundEmailRepo,
private val emailStorage: EmailStorage,
private val emailIngestionService: EmailIngestionService
) {
@PostMapping("/manual")
@ResponseStatus(HttpStatus.CREATED)
fun uploadManualPdf(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestParam("file") file: MultipartFile
): ManualInboundResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
if (file.isEmpty) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
}
val contentType = file.contentType
if (contentType != null && !contentType.equals("application/pdf", ignoreCase = true)) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Only PDF is supported")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val bytes = file.bytes
val pdfPath = emailStorage.storeUploadedPdf(propertyId, file.originalFilename, bytes)
val text = extractPdfText(bytes)
val inbound = InboundEmail(
property = property,
messageId = "manual-${UUID.randomUUID()}",
subject = file.originalFilename ?: "manual-upload",
fromAddress = null,
receivedAt = OffsetDateTime.now(),
status = InboundEmailStatus.PENDING,
rawPdfPath = pdfPath
)
inboundEmailRepo.save(inbound)
emailIngestionService.ingestManualPdf(property, inbound, text)
return ManualInboundResponse(inboundId = inbound.id!!)
}
private fun extractPdfText(bytes: ByteArray): String {
return try {
PDDocument.load(bytes).use { doc ->
PDFTextStripper().getText(doc)
}
} catch (_: Exception) {
""
}
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
}
data class ManualInboundResponse(
val inboundId: UUID
)

View File

@@ -0,0 +1,139 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.component.RoomImageStorage
import com.android.trisolarisserver.controller.dto.RoomImageResponse
import com.android.trisolarisserver.models.room.RoomImage
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.repo.RoomImageRepo
import com.android.trisolarisserver.repo.RoomRepo
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.core.io.FileSystemResource
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
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.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.UUID
@RestController
@RequestMapping("/properties/{propertyId}/rooms/{roomId}/images")
class RoomImages(
private val propertyAccess: PropertyAccess,
private val roomRepo: RoomRepo,
private val roomImageRepo: RoomImageRepo,
private val storage: RoomImageStorage,
@org.springframework.beans.factory.annotation.Value("\${storage.rooms.publicBaseUrl}")
private val publicBaseUrl: String
) {
@GetMapping
fun list(
@PathVariable propertyId: UUID,
@PathVariable roomId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<RoomImageResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
ensureRoom(propertyId, roomId)
return roomImageRepo.findByRoomIdOrderByCreatedAtDesc(roomId)
.map { it.toResponse(publicBaseUrl) }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun upload(
@PathVariable propertyId: UUID,
@PathVariable roomId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestParam("file") file: MultipartFile
): RoomImageResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val room = ensureRoom(propertyId, roomId)
if (file.isEmpty) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
}
val stored = try {
storage.store(propertyId, roomId, file)
} catch (ex: IllegalArgumentException) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, ex.message ?: "Invalid image")
}
val image = RoomImage(
property = room.property,
room = room,
originalPath = stored.originalPath,
thumbnailPath = stored.thumbnailPath,
contentType = stored.contentType,
sizeBytes = stored.sizeBytes
)
return roomImageRepo.save(image).toResponse(publicBaseUrl)
}
@GetMapping("/{imageId}/file")
fun file(
@PathVariable propertyId: UUID,
@PathVariable roomId: UUID,
@PathVariable imageId: UUID,
@RequestParam(required = false, defaultValue = "full") size: String,
@AuthenticationPrincipal principal: MyPrincipal?
): ResponseEntity<FileSystemResource> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
ensureRoom(propertyId, roomId)
val image = roomImageRepo.findByIdAndRoomIdAndPropertyId(imageId, roomId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Image not found")
val path = if (size.equals("thumb", true)) image.thumbnailPath else image.originalPath
val file = Paths.get(path)
if (!Files.exists(file)) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "File missing")
}
val resource = FileSystemResource(file)
val type = image.contentType
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(type))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"room-${imageId}.${file.fileName}\"")
.contentLength(resource.contentLength())
.body(resource)
}
private fun ensureRoom(propertyId: UUID, roomId: UUID): com.android.trisolarisserver.models.room.Room {
return roomRepo.findByIdAndPropertyId(roomId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room not found")
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
}
private fun RoomImage.toResponse(baseUrl: String): RoomImageResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Image id missing")
return RoomImageResponse(
id = id,
propertyId = property.id!!,
roomId = room.id!!,
url = "$baseUrl/properties/${property.id}/rooms/${room.id}/images/$id/file",
thumbnailUrl = "$baseUrl/properties/${property.id}/rooms/${room.id}/images/$id/file?size=thumb",
contentType = contentType,
sizeBytes = sizeBytes,
createdAt = createdAt.toString()
)
}

View File

@@ -1,6 +1,7 @@
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
@@ -25,6 +26,8 @@ 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
@@ -106,6 +109,43 @@ class Rooms(
}.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(
@@ -182,6 +222,14 @@ class Rooms(
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 {

View File

@@ -25,6 +25,23 @@ data class RoomAvailabilityResponse(
val freeRoomNumbers: List<Int>
)
data class RoomAvailabilityRangeResponse(
val roomTypeName: String,
val freeRoomNumbers: List<Int>,
val freeCount: Int
)
data class RoomImageResponse(
val id: UUID,
val propertyId: UUID,
val roomId: UUID,
val url: String,
val thumbnailUrl: String,
val contentType: String,
val sizeBytes: Long,
val createdAt: String
)
enum class RoomBoardStatus {
FREE,
OCCUPIED,