Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public record DeviationDetailsResponse(
String resolvedBy,
LocalDateTime resolvedAt,
String resolution,
String locationName,
Long relatedReadingId,
List<DeviationCommentResponse> comments
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public record DeviationResponse(
LocalDateTime reportedAt,
String resolvedBy,
LocalDateTime resolvedAt,
String resolution
String resolution,
String locationName
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public DeviationDetailsResponse getDeviationById(Long organizationId, Long devia
base.resolvedBy(),
base.resolvedAt(),
base.resolution(),
base.locationName(),
deviation.getRelatedReadingId(),
comments
);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface Deviation {
reportedBy: string
reportedAt: string
moduleType: ModuleType
locationName?: string
resolvedBy?: string
resolvedAt?: string
resolution?: string
Expand Down
49 changes: 48 additions & 1 deletion frontend/src/views/DeviationsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,39 @@ type SeverityFilter = 'ALL' | 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
const filterStatus = ref<StatusFilter>('ALL')
const filterModule = ref<ModuleFilter>('ALL')
const filterSeverity = ref<SeverityFilter>('ALL')
const filterLocation = ref<string>('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 ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -253,6 +268,22 @@ onMounted(() => deviationsStore.fetchAll())
>{{ opt.label }}</button>
</div>

<!-- Location (admins/supervisors only, when multiple locations exist) -->
<div v-if="isAdminOrSupervisor && availableLocations.length > 1" class="filter-group">
<button
class="filter-pill"
:class="{ 'filter-pill--active': filterLocation === 'ALL' }"
@click="filterLocation = 'ALL'"
>Alle lokasjoner</button>
<button
v-for="loc in availableLocations"
:key="loc"
class="filter-pill"
:class="{ 'filter-pill--active': filterLocation === loc }"
@click="filterLocation = loc"
>{{ loc }}</button>
</div>

<!-- Clear + count -->
<div class="filter-meta">
<span class="text-muted text-xs">
Expand Down Expand Up @@ -319,6 +350,9 @@ onMounted(() => deviationsStore.fetchAll())
</div>
<p class="dev-meta">
{{ fmtDate(dev.reportedAt) }} &middot; {{ dev.reportedBy }}
<template v-if="isAdminOrSupervisor && dev.locationName">
&middot; <span class="location-tag">{{ dev.locationName }}</span>
</template>
</p>
</button>

Expand Down Expand Up @@ -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);
Expand Down
Loading