From a4e1b7e85ae89d0f798162ffe9df51a33f87e313 Mon Sep 17 00:00:00 2001 From: Oleandertengesdal Date: Fri, 10 Apr 2026 10:28:07 +0200 Subject: [PATCH] Add location support to deviations Persist and expose deviation location: add a nullable location_id FK and index (V13 migration). --- .../api/dto/DeviationDetailsResponse.java | 1 + .../deviations/api/dto/DeviationMapper.java | 1 + .../deviations/api/dto/DeviationResponse.java | 3 +- .../application/DeviationService.java | 2 + .../deviations/domain/Deviation.java | 9 ++++ .../V13__add_location_to_deviations.sql | 8 +++ frontend/src/types/index.ts | 1 + frontend/src/views/DeviationsView.vue | 49 ++++++++++++++++++- 8 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V13__add_location_to_deviations.sql diff --git a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationDetailsResponse.java b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationDetailsResponse.java index 9d31af8..190bf88 100644 --- a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationDetailsResponse.java +++ b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationDetailsResponse.java @@ -19,6 +19,7 @@ public record DeviationDetailsResponse( String resolvedBy, LocalDateTime resolvedAt, String resolution, + String locationName, Long relatedReadingId, List comments ) {} \ No newline at end of file diff --git a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationMapper.java b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationMapper.java index 343c4c2..5cdb4c9 100644 --- a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationMapper.java +++ b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationMapper.java @@ -11,5 +11,6 @@ public interface DeviationMapper { @Mapping(target = "reportedBy", source = "reportedByName") @Mapping(target = "resolvedBy", source = "resolvedByName") @Mapping(target = "reportedAt", source = "createdAt") + @Mapping(target = "locationName", source = "locationName") DeviationResponse toResponse(Deviation deviation); } diff --git a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationResponse.java b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationResponse.java index 827618e..33bdb5c 100644 --- a/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationResponse.java +++ b/backend/src/main/java/backend/fullstack/deviations/api/dto/DeviationResponse.java @@ -17,5 +17,6 @@ public record DeviationResponse( LocalDateTime reportedAt, String resolvedBy, LocalDateTime resolvedAt, - String resolution + String resolution, + String locationName ) {} diff --git a/backend/src/main/java/backend/fullstack/deviations/application/DeviationService.java b/backend/src/main/java/backend/fullstack/deviations/application/DeviationService.java index b48f1b3..ca233ed 100644 --- a/backend/src/main/java/backend/fullstack/deviations/application/DeviationService.java +++ b/backend/src/main/java/backend/fullstack/deviations/application/DeviationService.java @@ -91,6 +91,7 @@ public DeviationDetailsResponse getDeviationById(Long organizationId, Long devia base.resolvedBy(), base.resolvedAt(), base.resolution(), + base.locationName(), deviation.getRelatedReadingId(), comments ); @@ -111,6 +112,7 @@ public DeviationResponse createDeviation(Long organizationId, Long userId, Devia .severity(request.severity()) .moduleType(request.moduleType()) .reportedBy(reporter) + .location(reporter.getHomeLocation()) .build(); Deviation saved = deviationRepository.save(deviation); diff --git a/backend/src/main/java/backend/fullstack/deviations/domain/Deviation.java b/backend/src/main/java/backend/fullstack/deviations/domain/Deviation.java index d70d912..2158f7b 100644 --- a/backend/src/main/java/backend/fullstack/deviations/domain/Deviation.java +++ b/backend/src/main/java/backend/fullstack/deviations/domain/Deviation.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import backend.fullstack.location.Location; import backend.fullstack.organization.Organization; import backend.fullstack.temperature.domain.TemperatureReading; import backend.fullstack.user.User; @@ -74,6 +75,10 @@ public class Deviation { @Column(name = "module_type", nullable = false, length = 30) private DeviationModuleType moduleType; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "reported_by_id", nullable = false) private User reportedBy; @@ -124,6 +129,10 @@ public Long getRelatedReadingId() { return relatedReading != null ? relatedReading.getId() : null; } + public String getLocationName() { + return location != null ? location.getName() : null; + } + public void addComment(DeviationComment comment) { comments.add(comment); comment.setDeviation(this); diff --git a/backend/src/main/resources/db/migration/V13__add_location_to_deviations.sql b/backend/src/main/resources/db/migration/V13__add_location_to_deviations.sql new file mode 100644 index 0000000..82a3322 --- /dev/null +++ b/backend/src/main/resources/db/migration/V13__add_location_to_deviations.sql @@ -0,0 +1,8 @@ +ALTER TABLE deviations + ADD COLUMN location_id BIGINT NULL; + +ALTER TABLE deviations + ADD CONSTRAINT fk_deviations_location + FOREIGN KEY (location_id) REFERENCES locations(id); + +CREATE INDEX idx_deviations_location_id ON deviations(location_id); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ec38087..9e45e58 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -132,6 +132,7 @@ export interface Deviation { reportedBy: string reportedAt: string moduleType: ModuleType + locationName?: string resolvedBy?: string resolvedAt?: string resolution?: string diff --git a/frontend/src/views/DeviationsView.vue b/frontend/src/views/DeviationsView.vue index a7e4802..472c640 100644 --- a/frontend/src/views/DeviationsView.vue +++ b/frontend/src/views/DeviationsView.vue @@ -25,24 +25,39 @@ type SeverityFilter = 'ALL' | 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' const filterStatus = ref('ALL') const filterModule = ref('ALL') const filterSeverity = ref('ALL') +const filterLocation = ref('ALL') + +const isAdminOrSupervisor = computed(() => { + const r = authStore.user?.role + return r === 'ADMIN' || r === 'SUPERVISOR' +}) + +const availableLocations = computed(() => { + const names = deviationsStore.deviations + .map(d => d.locationName) + .filter((n): n is string => !!n) + return [...new Set(names)].sort() +}) const filteredDeviations = computed(() => deviationsStore.deviations.filter(d => { if (filterStatus.value !== 'ALL' && d.status !== filterStatus.value) return false if (filterModule.value !== 'ALL' && d.moduleType !== filterModule.value) return false if (filterSeverity.value !== 'ALL' && d.severity !== filterSeverity.value) return false + if (filterLocation.value !== 'ALL' && d.locationName !== filterLocation.value) return false return true }) ) const hasActiveFilter = computed( - () => filterStatus.value !== 'ALL' || filterModule.value !== 'ALL' || filterSeverity.value !== 'ALL' + () => filterStatus.value !== 'ALL' || filterModule.value !== 'ALL' || filterSeverity.value !== 'ALL' || filterLocation.value !== 'ALL' ) function clearFilters() { filterStatus.value = 'ALL' filterModule.value = 'ALL' filterSeverity.value = 'ALL' + filterLocation.value = 'ALL' } // ── Expand / comments ───────────────────────────────────────────────────────── @@ -253,6 +268,22 @@ onMounted(() => deviationsStore.fetchAll()) >{{ opt.label }} + +
+ + +
+
@@ -319,6 +350,9 @@ onMounted(() => deviationsStore.fetchAll())

{{ fmtDate(dev.reportedAt) }} · {{ dev.reportedBy }} +

@@ -567,6 +601,19 @@ onMounted(() => deviationsStore.fetchAll()) margin: 0; } +.location-tag { + display: inline-block; + font-size: 0.6875rem; + font-weight: 600; + color: var(--c-primary); + background: color-mix(in srgb, var(--c-primary) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--c-primary) 25%, transparent); + border-radius: 999px; + padding: 0 0.4rem; + line-height: 1.6; + vertical-align: middle; +} + /* ── Chevron ── */ .dev-chevron { color: var(--c-text-3);