diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/missions/CreateOrUpdateMission.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/missions/CreateOrUpdateMission.kt index 172deac09..0c202d1e3 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/missions/CreateOrUpdateMission.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/missions/CreateOrUpdateMission.kt @@ -18,56 +18,60 @@ class CreateOrUpdateMission( private val missionRepository: IMissionRepository, private val facadeRepository: IFacadeAreasRepository, private val reportingRepository: IReportingRepository, - ) { @Throws(IllegalArgumentException::class) - fun execute(mission: MissionEntity?, attachedReportingIds: List? = null): MissionDTO { - require(mission != null) { - "No mission to create or update" - } - val envActions = mission.envActions?.map { - when (it.actionType) { - ActionTypeEnum.CONTROL -> { - (it as EnvActionControlEntity).copy( - facade = ( - it.geom - ?: mission.geom - )?.let { geom -> facadeRepository.findFacadeFromGeometry(geom) }, - department = ( - it.geom - ?: mission.geom - )?.let { geom -> departmentRepository.findDepartmentFromGeometry(geom) }, - ) - } - - ActionTypeEnum.SURVEILLANCE -> { - val surveillance = it as EnvActionSurveillanceEntity - /* - When coverMissionZone is true, use mission geometry in priority, fall back to action geometry. - When coverMissionZone is not true, prioritize the other way around. - Ideally the fallbacks should not be needed, but if coverMissionZone is true and the mission geom - is null, or if coverMissionZone is false and the action geom is null, then rather that nothing, - better use the geometry that is available, if any. - */ - val geometry = if (surveillance.coverMissionZone == true) { - ( - mission.geom - ?: surveillance.geom - ) - } else { - (surveillance.geom ?: mission.geom) + fun execute(mission: MissionEntity?, attachedReportingIds: List? = listOf()): MissionDTO { + require(mission != null) { "No mission to create or update" } + val envActions = + mission.envActions?.map { + when (it.actionType) { + ActionTypeEnum.CONTROL -> { + (it as EnvActionControlEntity).copy( + facade = + (it.geom ?: mission.geom)?.let { geom -> + facadeRepository.findFacadeFromGeometry(geom) + }, + department = + (it.geom ?: mission.geom)?.let { geom -> + departmentRepository.findDepartmentFromGeometry( + geom, + ) + }, + ) + } + ActionTypeEnum.SURVEILLANCE -> { + val surveillance = it as EnvActionSurveillanceEntity + /* + When coverMissionZone is true, use mission geometry in priority, fall back to action geometry. + When coverMissionZone is not true, prioritize the other way around. + Ideally the fallbacks should not be needed, but if coverMissionZone is true and the mission geom + is null, or if coverMissionZone is false and the action geom is null, then rather that nothing, + better use the geometry that is available, if any. + */ + val geometry = + if (surveillance.coverMissionZone == true) { + (mission.geom ?: surveillance.geom) + } else { + (surveillance.geom ?: mission.geom) + } + surveillance.copy( + facade = + geometry?.let { geom -> + facadeRepository.findFacadeFromGeometry(geom) + }, + department = + geometry?.let { geom -> + departmentRepository.findDepartmentFromGeometry( + geom, + ) + }, + ) + } + ActionTypeEnum.NOTE -> { + (it as EnvActionNoteEntity).copy() } - surveillance.copy( - facade = geometry?.let { geom -> facadeRepository.findFacadeFromGeometry(geom) }, - department = geometry?.let { geom -> departmentRepository.findDepartmentFromGeometry(geom) }, - ) - } - - ActionTypeEnum.NOTE -> { - (it as EnvActionNoteEntity).copy() } } - } var facade: String? = null @@ -75,19 +79,20 @@ class CreateOrUpdateMission( facade = facadeRepository.findFacadeFromGeometry(mission.geom) } - val missionToSave = mission.copy( - facade = facade, - envActions = envActions, - ) + val missionToSave = + mission.copy( + facade = facade, + envActions = envActions, + ) val savedMission = missionRepository.save(missionToSave) if (savedMission.mission.id == null) { throw IllegalArgumentException("Mission id is null") } - - if (attachedReportingIds != null) { - reportingRepository.attachReportingsToMission(attachedReportingIds, savedMission.mission.id) - } + reportingRepository.attachReportingsToMission( + attachedReportingIds ?: listOf(), + savedMission.mission.id, + ) return missionRepository.findById(savedMission.mission.id) } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/reportings/CreateOrUpdateReporting.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/reportings/CreateOrUpdateReporting.kt index 66e318542..b52c0f932 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/reportings/CreateOrUpdateReporting.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/reportings/CreateOrUpdateReporting.kt @@ -6,7 +6,6 @@ import fr.gouv.cacem.monitorenv.domain.repositories.* import fr.gouv.cacem.monitorenv.domain.use_cases.reportings.dtos.ReportingDTO import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.time.ZonedDateTime @UseCase class CreateOrUpdateReporting( @@ -29,23 +28,10 @@ class CreateOrUpdateReporting( seaFront = facadeRepository.findFacadeFromGeometry(reporting.geom) } - var attachedToMissionAtUtc: ZonedDateTime? = null - var detachedFromMissionAtUtc: ZonedDateTime? = null - if (reporting.missionId != null && reporting.attachedToMissionAtUtc == null) { - attachedToMissionAtUtc = ZonedDateTime.now() - detachedFromMissionAtUtc = null - } - - if (reporting.missionId == null && reporting.attachedToMissionAtUtc != null) { - detachedFromMissionAtUtc = ZonedDateTime.now() - } - val savedReport = reportingRepository.save( reporting.copy( seaFront = seaFront, - attachedToMissionAtUtc = attachedToMissionAtUtc, - detachedFromMissionAtUtc = detachedFromMissionAtUtc, ), ) diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/MissionAttachedReportingDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/MissionAttachedReportingDataOutput.kt index 4f145ec10..514898cd7 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/MissionAttachedReportingDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/MissionAttachedReportingDataOutput.kt @@ -9,6 +9,7 @@ import fr.gouv.cacem.monitorenv.domain.use_cases.reportings.dtos.ReportingDTO import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.publicapi.outputs.ControlUnitDataOutput import org.locationtech.jts.geom.Geometry import java.time.ZonedDateTime +import java.util.UUID data class MissionAttachedReportingDataOutput( val id: Int, @@ -36,6 +37,9 @@ data class MissionAttachedReportingDataOutput( val validityTime: Int? = null, val isArchived: Boolean, val openBy: String? = null, + val attachedToMissionAtUtc: ZonedDateTime? = null, + val detachedFromMissionAtUtc: ZonedDateTime? = null, + val attachedEnvActionId: UUID? = null, ) { companion object { fun fromReportingDTO( @@ -47,7 +51,8 @@ data class MissionAttachedReportingDataOutput( reportingId = dto.reporting.reportingId, sourceType = dto.reporting.sourceType, semaphoreId = dto.reporting.semaphoreId, - semaphore = if (dto.semaphore != null) { + semaphore = + if (dto.semaphore != null) { SemaphoreDataOutput.fromSemaphoreEntity( dto.semaphore, ) @@ -57,17 +62,19 @@ data class MissionAttachedReportingDataOutput( controlUnitId = dto.reporting.controlUnitId, controlUnit = if (dto.controlUnit != null) { - ControlUnitDataOutput - .fromFullControlUnit( - dto.controlUnit, - ) + ControlUnitDataOutput.fromFullControlUnit( + dto.controlUnit, + ) } else { null }, displayedSource = when (dto.reporting.sourceType) { - SourceTypeEnum.SEMAPHORE -> dto?.semaphore?.unit ?: dto?.semaphore?.name - // TODO This is really strange : `fullControlUnit?.controlUnit` can't be null and I have to add another `?`... + SourceTypeEnum.SEMAPHORE -> + dto?.semaphore?.unit + ?: dto?.semaphore?.name + // TODO This is really strange : `fullControlUnit?.controlUnit` + // can't be null and I have to add another `?`... SourceTypeEnum.CONTROL_UNIT -> dto?.controlUnit?.controlUnit?.name SourceTypeEnum.OTHER -> dto.reporting.sourceName else -> "" @@ -89,6 +96,8 @@ data class MissionAttachedReportingDataOutput( validityTime = dto.reporting.validityTime, isArchived = dto.reporting.isArchived, openBy = dto.reporting.openBy, + attachedToMissionAtUtc = dto.reporting.attachedToMissionAtUtc, + detachedFromMissionAtUtc = dto.reporting.detachedFromMissionAtUtc, ) } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/MissionModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/MissionModel.kt index 1c9467b1d..fad170d1c 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/MissionModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/MissionModel.kt @@ -156,9 +156,25 @@ data class MissionModel( fun toMissionDTO(objectMapper: ObjectMapper): MissionDTO { return MissionDTO( mission = this.toMissionEntity(objectMapper), - attachedReportingIds = this.attachedReportings?.map { it.id as Int } ?: listOf(), + attachedReportingIds = + this.attachedReportings + ?.filter { it.detachedFromMissionAtUtc == null } + ?.map { it.id as Int } + ?: listOf(), attachedReportings = - this.attachedReportings?.map { it.toReportingDTO(objectMapper) } + this.attachedReportings + ?.filter { it.detachedFromMissionAtUtc == null } + ?.map { it.toReportingDTO(objectMapper) } + ?: listOf(), + detachedReportings = + this.attachedReportings + ?.filter { it.detachedFromMissionAtUtc != null } + ?.map { it.toReportingDTO(objectMapper) } + ?: listOf(), + detachedReportingIds = + this.attachedReportings + ?.filter { it.detachedFromMissionAtUtc != null } + ?.map { it.id as Int } ?: listOf(), ) } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaMissionRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaMissionRepository.kt index ae3c4f301..470bc1d1c 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaMissionRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaMissionRepository.kt @@ -12,6 +12,7 @@ import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.interfaces. import org.springframework.dao.DataIntegrityViolationException import org.springframework.dao.InvalidDataAccessApiUsageException import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.Modifying import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional import java.time.Instant @@ -27,6 +28,7 @@ class JpaMissionRepository( } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun delete(missionId: Int) { dbMissionRepository.delete(missionId) } @@ -49,7 +51,8 @@ class JpaMissionRepository( missionSources = convertToPGArray(missionSourcesAsStringArray), seaFronts = convertToPGArray(seaFronts), pageable = pageable, - ).map { it.toMissionDTO(mapper) } + ) + .map { it.toMissionDTO(mapper) } } override fun findById(missionId: Int): MissionDTO { @@ -57,14 +60,17 @@ class JpaMissionRepository( } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun save(mission: MissionEntity): MissionDTO { return try { // Extract all control units resources unique baseIds - val uniqueBaseIds = mission.controlUnits.flatMap { controlUnit -> - controlUnit.resources.map { it.baseId } - }.distinct() + val uniqueBaseIds = + mission.controlUnits + .flatMap { controlUnit -> controlUnit.resources.map { it.baseId } } + .distinct() // Fetch all of them as models - val baseModels = dbBaseRepository.findAllById(uniqueBaseIds).map { BaseModel.fromFullBase(it) } + val baseModels = + dbBaseRepository.findAllById(uniqueBaseIds).map { BaseModel.fromFullBase(it) } // Create a `[baseId] → BaseModel` map val baseModelMap = baseModels.associateBy { requireNotNull(it.id) } @@ -73,13 +79,14 @@ class JpaMissionRepository( } catch (e: Exception) { when (e) { // TODO Is `InvalidDataAccessApiUsageException` necessary? - is DataIntegrityViolationException, is InvalidDataAccessApiUsageException -> { + is DataIntegrityViolationException, + is InvalidDataAccessApiUsageException, + -> { throw ControlResourceOrUnitNotFoundException( "Invalid control unit or resource id: not found in referential.", e, ) } - else -> throw e } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaReportingRepository.kt index cf4c1f686..836588617 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaReportingRepository.kt @@ -113,6 +113,7 @@ class JpaReportingRepository( } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun delete(reportingId: Int) { dbReportingRepository.delete(reportingId) } @@ -122,16 +123,19 @@ class JpaReportingRepository( } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun archiveOutdatedReportings(): Int { return dbReportingRepository.archiveOutdatedReportings() } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun archiveReportings(ids: List) { dbReportingRepository.archiveReportings(ids) } @Transactional + @Modifying(clearAutomatically = true, flushAutomatically = true) override fun deleteReportings(ids: List) { dbReportingRepository.deleteReportings(ids) } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBReportingRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBReportingRepository.kt index 53b41303c..6feb63b43 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBReportingRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBReportingRepository.kt @@ -40,8 +40,8 @@ interface IDBReportingRepository : JpaRepository { SET mission_id = :missionId, attached_to_mission_at_utc = CASE WHEN (mission_id IS NULL OR mission_id = (:missionId)) AND id IN (:reportingIds) THEN NOW() ELSE attached_to_mission_at_utc END, - detached_from_mission_at_utc = CASE WHEN id NOT IN (:reportingIds) THEN NOW() ELSE NULL END - WHERE id in (:reportingIds) OR (mission_id = :missionId AND detached_from_mission_at_utc IS NULL) + detached_from_mission_at_utc = CASE WHEN (id NOT IN (:reportingIds) OR (:reportingIds) IS NULL ) THEN NOW() ELSE NULL END + WHERE id IN (:reportingIds) OR (mission_id = :missionId AND detached_from_mission_at_utc IS NULL) """, nativeQuery = true, ) diff --git a/frontend/src/domain/use_cases/missions/saveMission.ts b/frontend/src/domain/use_cases/missions/saveMission.ts index ccc83006c..35abbe57c 100644 --- a/frontend/src/domain/use_cases/missions/saveMission.ts +++ b/frontend/src/domain/use_cases/missions/saveMission.ts @@ -15,7 +15,7 @@ export const saveMission = const { sideWindow: { currentPath } } = getState() - const valuesToSave = omit(values, ['attachedReportings']) + const valuesToSave = omit(values, ['attachedReportings', 'detachedReportings', 'detachedReportingIds']) const routeParams = getMissionPageRoute(currentPath) const missionIsNewMission = isNewMission(routeParams?.params?.id)