Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
73631f5
added meetings (sammanträden) to admin-sidenav.html
MartinStenhagen Apr 22, 2026
c685024
Added meeting entity-class and enum
MartinStenhagen Apr 23, 2026
5f4db84
Added MeetingAgendaItem entity
MartinStenhagen Apr 23, 2026
acf23b6
Added MeetingAgendaDocument entity
MartinStenhagen Apr 23, 2026
d38c9dc
Added repositories
MartinStenhagen Apr 23, 2026
5f7dd8b
Added Flyway migration for meeting tables
MartinStenhagen Apr 23, 2026
aec7c81
Added MeetingService
MartinStenhagen Apr 23, 2026
ab3e731
Added MeetingController
MartinStenhagen Apr 23, 2026
6a57259
Added fragment admin-meetings.html
MartinStenhagen Apr 23, 2026
44ba9b0
Added ui functionality
MartinStenhagen Apr 23, 2026
e112b02
Added agenda documents support to MeetingService and MeetingController
MartinStenhagen Apr 23, 2026
874bc4c
Reworked populateMeetingsPage to support agenda documents.
MartinStenhagen Apr 23, 2026
4a8f3b8
Added agenda document support to ui
MartinStenhagen Apr 23, 2026
eed6f80
Current meetings now lists correctly
MartinStenhagen Apr 23, 2026
f4f2e2d
Merge branch 'refs/heads/main' into feature/meeting-with-registry-cas…
MartinStenhagen Apr 23, 2026
2fad9c5
Added updateMeeting and deleteMeeting to MeetingService
MartinStenhagen Apr 23, 2026
9d3ef5b
Added updateMeeting and deleteMeeting to to MeetingController
MartinStenhagen Apr 23, 2026
9896c31
Added meeting editing to ui
MartinStenhagen Apr 23, 2026
4f21189
Added delete meeting to ui
MartinStenhagen Apr 23, 2026
c52ff73
Added move agenda item to MeetingService and Repo
MartinStenhagen Apr 23, 2026
5268461
Added move agenda item to MeetingController
MartinStenhagen Apr 23, 2026
f761682
Added move agenda item to ui and saveandflush-fix to MeetingService
MartinStenhagen Apr 23, 2026
49eeb8d
Added new exceptions
MartinStenhagen Apr 23, 2026
649f599
Changed to use new exceptions
MartinStenhagen Apr 23, 2026
4a96588
Added MeetingServiceTest
MartinStenhagen Apr 23, 2026
e2fc82f
Added MeetingControllerTest
MartinStenhagen Apr 23, 2026
b68837e
Merge branch 'fix/bucket-file-config-error' into feature/meeting-with…
MartinStenhagen Apr 24, 2026
fd7c0b9
Added check and exception when trying to delete files in use
MartinStenhagen Apr 24, 2026
28998e9
Added FileInUseException to deleteCaseFile
MartinStenhagen Apr 24, 2026
4c91d6b
Added mock to fix tests and added new test for delete file in use
MartinStenhagen Apr 24, 2026
815bfdf
removed localstack_data to adress coderabbit feedback Named volume wi…
MartinStenhagen Apr 24, 2026
a3be5d6
fixed coderabbit feedback: Add MeetingAgendaDocumentRepository mock t…
MartinStenhagen Apr 24, 2026
dac15cc
fixed coderabbit feedback: Inconsistent error response shape — return…
MartinStenhagen Apr 24, 2026
1862f5f
fixed coderabbit feedback: Inconsistent error response shape — return…
MartinStenhagen Apr 24, 2026
587db6e
fixed coderabbit feedback: Remove malformed findById(Meeting, CaseRec…
MartinStenhagen Apr 24, 2026
f6bffde
fixed coderabbit feedback: Inconsistent capitalization in success/err…
MartinStenhagen Apr 24, 2026
a6a76c7
fixed coderabbit feedback: Catch block can throw, losing the original…
MartinStenhagen Apr 24, 2026
dc1c2d1
fixed coderabbit feedback: Narrow catch (IllegalArgumentException) mi…
MartinStenhagen Apr 24, 2026
0652fc9
fixed coderabbit feedback: Typo in user-facing message: "diarum" → "d…
MartinStenhagen Apr 24, 2026
7f32aa6
fixed coderabbit feedback: resequenceAgendaItems risks violating uk_m…
MartinStenhagen Apr 24, 2026
4c9ddbe
fixed coderabbit feedback: Label inconsistency: "Registry:" is Englis…
MartinStenhagen Apr 24, 2026
00849ea
fixed coderabbit feedback: Inconsistent CSRF handling across POST tests.
MartinStenhagen Apr 24, 2026
b839e7a
Merge branch 'main' into feature/meeting-with-registry-caserecord-cas…
MartinStenhagen Apr 24, 2026
2f4cc0b
fixed coderabbit feedback: createMeeting catch can cascade to 500 whe…
MartinStenhagen Apr 24, 2026
52a5683
fixed coderabbit feedback: meetingService.getMeetingById(...) is call…
MartinStenhagen Apr 24, 2026
ab51517
fixed coderabbit feedback: Inconsistent exception types for not-found…
MartinStenhagen Apr 24, 2026
d2ca6b5
fixed coderabbit feedback: Raw enum names ("PLANNED", "COMPLETED", …)…
MartinStenhagen Apr 24, 2026
3889952
fixed coderabbit feedback: The safeRegistryId fallback does nothing —…
MartinStenhagen Apr 24, 2026
8adc1f4
fixed coderabbit feedback: Replace with correct exception type for Ca…
MartinStenhagen Apr 24, 2026
2e2b0aa
fixed coderabbit feedback: Label "Instans" is inconsistent with the r…
MartinStenhagen Apr 24, 2026
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
5 changes: 2 additions & 3 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ services:
AWS_BUCKET_NAME: ${AWS_BUCKET_NAME:-team4you-files}
volumes:
- ./init-localstack.sh:/etc/localstack/init/ready.d/init-localstack.sh
- localstack_data:/var/lib/localstack
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5

volumes:
postgres_data:
localstack_data:
postgres_data:
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface CaseFileRepository extends JpaRepository<CaseFile, Long> {
Optional<CaseFile> findByIdAndCaseRecordId(Long id, Long caseRecordId);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<CaseFile> findTopByCaseRecordIdOrderByDocumentNumberDesc(Long caseRecordId);
List<CaseFile> findByCaseRecordIdOrderByUploadedAtDesc(Long caseRecordId);
}
16 changes: 10 additions & 6 deletions src/main/java/backendlab/team4you/casefile/CaseFileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
import backendlab.team4you.caserecord.CaseRecord;
import backendlab.team4you.caserecord.CaseRecordRepository;
import backendlab.team4you.common.ConfidentialityLevel;
import backendlab.team4you.exceptions.CaseFileNotFoundException;
import backendlab.team4you.exceptions.CaseRecordNotFoundException;
import backendlab.team4you.exceptions.FileStorageConfigurationException;
import backendlab.team4you.exceptions.FileTooLargeException;
import backendlab.team4you.exceptions.InvalidFileNameException;
import backendlab.team4you.exceptions.*;
import backendlab.team4you.meeting.MeetingAgendaDocumentRepository;
import backendlab.team4you.s3.S3Service;
import backendlab.team4you.user.UserEntity;
import org.slf4j.Logger;
Expand Down Expand Up @@ -38,17 +35,20 @@ public class CaseFileService {
private final CaseFileRepository caseFileRepository;
private final CaseFileAccessService caseFileAccessService;
private final S3Service s3Service;
private final MeetingAgendaDocumentRepository meetingAgendaDocumentRepository;

public CaseFileService(
CaseRecordRepository caseRecordRepository,
CaseFileRepository caseFileRepository,
CaseFileAccessService caseFileAccessService,
S3Service s3Service
S3Service s3Service,
MeetingAgendaDocumentRepository meetingAgendaDocumentRepository
) {
this.caseRecordRepository = caseRecordRepository;
this.caseFileRepository = caseFileRepository;
this.caseFileAccessService = caseFileAccessService;
this.s3Service = s3Service;
this.meetingAgendaDocumentRepository = meetingAgendaDocumentRepository;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Transactional
Expand Down Expand Up @@ -180,6 +180,10 @@ public void deleteFile(Long caseRecordId, Long fileId, UserEntity actor) {
}

String s3Key = caseFile.getS3Key();
if (meetingAgendaDocumentRepository.existsByCaseFileId(fileId)) {
throw new FileInUseException("Filen kan inte tas bort eftersom den används som mötesunderlag.");
}

caseFileRepository.delete(caseFile);
try {
s3Service.deleteFile(s3Key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

import backendlab.team4you.casefile.CaseFileService;
import backendlab.team4you.common.ConfidentialityLevel;
import backendlab.team4you.exceptions.CaseFileNotFoundException;
import backendlab.team4you.exceptions.CaseRecordNotFoundException;
import backendlab.team4you.exceptions.FileStorageConfigurationException;
import backendlab.team4you.exceptions.FileTooLargeException;
import backendlab.team4you.exceptions.InvalidFileNameException;
import backendlab.team4you.exceptions.*;
import backendlab.team4you.user.UserEntity;
import backendlab.team4you.user.UserService;
import org.slf4j.Logger;
Expand Down Expand Up @@ -89,6 +85,8 @@ public String deleteCaseFile(
model.addAttribute("errorMessage", "Filen kunde inte hittas.");
} catch (org.springframework.security.access.AccessDeniedException ex) {
model.addAttribute("errorMessage", "Du har inte behörighet att ta bort den här filen.");
} catch (FileInUseException ex) {
model.addAttribute("errorMessage", ex.getMessage());
} catch (Exception ex) {
log.error("Unexpected error while deleting fileId={} for caseId={}", fileId, caseId, ex);
model.addAttribute("errorMessage", "Något gick fel när filen skulle tas bort.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class DuplicateMeetingAgendaDocumentException extends RuntimeException {
public DuplicateMeetingAgendaDocumentException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class DuplicateMeetingAgendaItemException extends RuntimeException {
public DuplicateMeetingAgendaItemException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class FileInUseException extends RuntimeException {
public FileInUseException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public ResponseEntity<ErrorResponseDto> handleAccessDenied(AccessDeniedException
CaseFileNotFoundException.class,
CaseRecordNotFoundException.class,
RegistryNotFoundException.class,
UserNotFoundException.class
UserNotFoundException.class,
MeetingNotFoundException.class,
MeetingAgendaItemNotFoundException.class,
MeetingAgendaDocumentNotFoundException.class
})
public ResponseEntity<ErrorResponseDto> handleNotFound(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
Expand All @@ -58,7 +61,8 @@ public ResponseEntity<ErrorResponseDto> handleNotFound(RuntimeException ex) {
@ExceptionHandler({
InvalidFileNameException.class,
IllegalArgumentException.class,
IllegalStateException.class
IllegalStateException.class,
InvalidMeetingStateException.class
})
public ResponseEntity<ErrorResponseDto> handleBadRequest(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
Expand All @@ -85,7 +89,9 @@ public ResponseEntity<ErrorResponseDto> handleFileTooLarge(FileTooLargeException
DuplicateRegistryNameException.class,
DuplicateRegistryCodeException.class,
DuplicateEmailException.class,
FileKeyConflictException.class
FileKeyConflictException.class,
DuplicateMeetingAgendaItemException.class,
DuplicateMeetingAgendaDocumentException.class
})
public ResponseEntity<ErrorResponseDto> handleConflict(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
Expand Down Expand Up @@ -199,4 +205,15 @@ public ResponseEntity<ErrorResponseDto> handleUnexpected(Exception ex) {
LocalDateTime.now()
));
}

@ExceptionHandler(FileInUseException.class)
public ResponseEntity<ErrorResponseDto> handleFileInUse(FileInUseException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponseDto(
HttpStatus.CONFLICT.value(),
"conflict",
ex.getMessage(),
LocalDateTime.now()
));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice(basePackages = "backendlab.team4you.casefile.ui")
@ControllerAdvice(basePackages = { "backendlab.team4you.casefile.ui","backendlab.team4you.meeting"})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
public class GlobalViewExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalViewExceptionHandler.class);
Expand All @@ -31,7 +31,9 @@ public String handleDuplicateEmail(DuplicateEmailException ex, Model model) {
@ExceptionHandler({
CaseRecordNotFoundException.class,
RegistryNotFoundException.class,
CaseFileNotFoundException.class
CaseFileNotFoundException.class,
MeetingNotFoundException.class,
MeetingAgendaDocumentNotFoundException.class,
})
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(RuntimeException ex, Model model) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class InvalidMeetingStateException extends RuntimeException {
public InvalidMeetingStateException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class MeetingAgendaDocumentNotFoundException extends RuntimeException {
public MeetingAgendaDocumentNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class MeetingAgendaItemNotFoundException extends RuntimeException {
public MeetingAgendaItemNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package backendlab.team4you.exceptions;

public class MeetingNotFoundException extends RuntimeException {
public MeetingNotFoundException(String message) {
super(message);
}
}
159 changes: 159 additions & 0 deletions src/main/java/backendlab/team4you/meeting/Meeting.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package backendlab.team4you.meeting;

import backendlab.team4you.registry.Registry;
import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "meeting")
public class Meeting {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "registry_id", nullable = false)
private Registry registry;

@Column(nullable = false, length = 200)
private String title;

@Column(name = "starts_at", nullable = false)
private LocalDateTime startsAt;

@Column(name = "ends_at")
private LocalDateTime endsAt;

@Column(length = 200)
private String location;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private MeetingStatus status;

@Column(length = 2000)
private String notes;

@OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("agendaOrder ASC")
private List<MeetingAgendaItem> agendaItems = new ArrayList<>();

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;

protected Meeting() {
}

public Meeting(
Registry registry,
String title,
LocalDateTime startsAt,
LocalDateTime endsAt,
String location,
MeetingStatus status,
String notes
) {
this.registry = registry;
this.title = title;
this.startsAt = startsAt;
this.endsAt = endsAt;
this.location = location;
this.status = status;
this.notes = notes;
}

@PrePersist
void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;

if (this.status == null) {
this.status = MeetingStatus.PLANNED;
}
}

@PreUpdate
void onUpdate() {
this.updatedAt = LocalDateTime.now();
}

public Long getId() {
return id;
}

public Registry getRegistry() {
return registry;
}

public void setRegistry(Registry registry) {
this.registry = registry;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public LocalDateTime getStartsAt() {
return startsAt;
}

public void setStartsAt(LocalDateTime startsAt) {
this.startsAt = startsAt;
}

public LocalDateTime getEndsAt() {
return endsAt;
}

public void setEndsAt(LocalDateTime endsAt) {
this.endsAt = endsAt;
}

public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}

public MeetingStatus getStatus() {
return status;
}

public void setStatus(MeetingStatus status) {
this.status = status;
}

public String getNotes() {
return notes;
}

public void setNotes(String notes) {
this.notes = notes;
}

public List<MeetingAgendaItem> getAgendaItems() {
return agendaItems;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}
Loading