Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e3727ab
Added Protocol-entity
MartinStenhagen Apr 24, 2026
9ea0a2f
Added ProtocolParagraph entity
MartinStenhagen Apr 24, 2026
7aa6f76
Added ProtocolParagraphSequence entity and repositories
MartinStenhagen Apr 24, 2026
d25196f
Added exceptions for protocol
MartinStenhagen Apr 24, 2026
6d6884a
Added ProtocolService and reworked MeetingNotFoundException
MartinStenhagen Apr 24, 2026
8218620
Added flyway-migration for protocol
MartinStenhagen Apr 24, 2026
9beeee6
Added query to MeetingRepository and ProtocolController
MartinStenhagen Apr 24, 2026
8f147ea
added basic ui for protocol
MartinStenhagen Apr 24, 2026
65ef2b9
added decision to ProtocolParagraph and enum
MartinStenhagen Apr 24, 2026
2f5aaef
added flyway-migration for decision and getters etc to ProtocolParagraph
MartinStenhagen Apr 24, 2026
7c6c689
added decision support to ProtocolService and created ProtocolParagra…
MartinStenhagen Apr 24, 2026
af9a1c2
added method to controller and missing import to ProtocolService
MartinStenhagen Apr 24, 2026
9dd20de
added ui functionality
MartinStenhagen Apr 24, 2026
986b921
some improvements and ProtocolServiceTest
MartinStenhagen Apr 24, 2026
08b0697
Added ProtocolControllerTest
MartinStenhagen Apr 24, 2026
3319825
mvn test fixes
MartinStenhagen Apr 24, 2026
747c013
Fixed coderabbit feedback: Mapping IllegalStateException to 400 is to…
MartinStenhagen Apr 27, 2026
ed1c530
Fixed coderabbit feedback: meetingsPage always returns the HTMX fragm…
MartinStenhagen Apr 27, 2026
1d93b80
Fixed coderabbit feedback: Race condition on first paragraph-sequence…
MartinStenhagen Apr 27, 2026
b126875
Fixed coderabbit feedback: Align foreign key constraint with JPA casc…
MartinStenhagen Apr 27, 2026
2d9433b
Fixed coderabbit feedback: Add :focus-visible styles for buttons (a11y).
MartinStenhagen Apr 27, 2026
56fede1
Fixed coderabbit feedback: Stub on getMeetingsForRegistry at line 124…
MartinStenhagen Apr 27, 2026
2033082
Fixed coderabbit feedback: Tests now require a hardcoded local Postgr…
MartinStenhagen Apr 27, 2026
f650d13
Updated CaseFileService and tests
MartinStenhagen Apr 27, 2026
97c2e8b
Fixed coderabbit feedback: Inconsistent capitalization in success mes…
MartinStenhagen Apr 27, 2026
e74feb7
Fixed coderabbit feedback: Retry loop is ineffective—transaction pois…
MartinStenhagen Apr 27, 2026
762d1ee
fixed excepted success-message after changes in the controller
MartinStenhagen Apr 27, 2026
ca02933
Merge remote-tracking branch 'origin/main' into feature/meeting-proto…
MartinStenhagen Apr 27, 2026
b78fe1a
Fixed coderabbit feedback
MartinStenhagen Apr 27, 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
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-postgres.sql:/docker-entrypoint-initdb.d/init-postgres.sql:ro

localstack:
image: localstack/localstack:3.8.1
Expand Down
1 change: 1 addition & 0 deletions init-postgres.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE DATABASE team4you_test;
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,17 @@ 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);
} catch (Exception e) {
log.error("Failed to delete S3 object after DB deletion: {}", s3Key, e);
throw e;
} catch (Exception exception) {
log.error("Failed to delete S3 object after DB deletion: {}", s3Key, exception);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"backendlab.team4you.caserecord",
"backendlab.team4you.registry",
"backendlab.team4you.user",
"backendlab.team4you.controller"
"backendlab.team4you.controller",
"backendlab.team4you.protocol"
})
public class GlobalRestExceptionHandler {

Expand All @@ -46,7 +47,9 @@ public ResponseEntity<ErrorResponseDto> handleAccessDenied(AccessDeniedException
UserNotFoundException.class,
MeetingNotFoundException.class,
MeetingAgendaItemNotFoundException.class,
MeetingAgendaDocumentNotFoundException.class
MeetingAgendaDocumentNotFoundException.class,
ProtocolNotFoundException.class,
ProtocolParagraphNotFoundException.class
})
public ResponseEntity<ErrorResponseDto> handleNotFound(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
Expand All @@ -61,7 +64,6 @@ public ResponseEntity<ErrorResponseDto> handleNotFound(RuntimeException ex) {
@ExceptionHandler({
InvalidFileNameException.class,
IllegalArgumentException.class,
IllegalStateException.class,
InvalidMeetingStateException.class
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
public ResponseEntity<ErrorResponseDto> handleBadRequest(RuntimeException ex) {
Expand Down Expand Up @@ -91,7 +93,8 @@ public ResponseEntity<ErrorResponseDto> handleFileTooLarge(FileTooLargeException
DuplicateEmailException.class,
FileKeyConflictException.class,
DuplicateMeetingAgendaItemException.class,
DuplicateMeetingAgendaDocumentException.class
DuplicateMeetingAgendaDocumentException.class,
ProtocolAlreadyExistsException.class
})
public ResponseEntity<ErrorResponseDto> handleConflict(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice(basePackages = { "backendlab.team4you.casefile.ui","backendlab.team4you.meeting"})
@ControllerAdvice(basePackages = {
"backendlab.team4you.casefile.ui",
"backendlab.team4you.meeting",
"backendlab.team4you.protocol"
})
public class GlobalViewExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalViewExceptionHandler.class);
Expand All @@ -34,6 +38,8 @@ public String handleDuplicateEmail(DuplicateEmailException ex, Model model) {
CaseFileNotFoundException.class,
MeetingNotFoundException.class,
MeetingAgendaDocumentNotFoundException.class,
ProtocolNotFoundException.class,
ProtocolParagraphNotFoundException.class,
})
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(RuntimeException ex, Model model) {
Expand Down Expand Up @@ -63,4 +69,11 @@ public String handleUnexpected(Exception ex, Model model) {
model.addAttribute("errorMessage", "Något gick fel. Försök igen.");
return "error";
}

@ExceptionHandler(ProtocolAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public String handleProtocolAlreadyExists(ProtocolAlreadyExistsException ex, Model model) {
model.addAttribute("errorMessage", "Ett protokoll finns redan för det här sammanträdet.");
return "error";
}
Comment thread
MartinStenhagen marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package backendlab.team4you.exceptions;

public class MeetingNotFoundException extends RuntimeException {
public MeetingNotFoundException(String message) {
super(message);
private final Long meetingId;

public MeetingNotFoundException(Long meetingId) {
super("Sammanträdet hittades inte.");
this.meetingId = meetingId;
}

public Long getMeetingId() {
return meetingId;
}
Comment thread
MartinStenhagen marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backendlab.team4you.exceptions;

public class ProtocolAlreadyExistsException extends RuntimeException {

public ProtocolAlreadyExistsException(Long meetingId) {
super("Protocol already exists for meeting. meetingId=" + meetingId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backendlab.team4you.exceptions;

public class ProtocolNotFoundException extends RuntimeException {

public ProtocolNotFoundException(Long protocolId) {
super("Protocol not found. protocolId=" + protocolId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package backendlab.team4you.exceptions;

public class ProtocolParagraphNotFoundException extends RuntimeException {
private final Long paragraphId;

public ProtocolParagraphNotFoundException(Long paragraphId) {
super("Protokollparagrafen hittades inte.");
this.paragraphId = paragraphId;
}

public Long getParagraphId() {
return paragraphId;
}
Comment thread
MartinStenhagen marked this conversation as resolved.
}
56 changes: 30 additions & 26 deletions src/main/java/backendlab/team4you/meeting/MeetingController.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ public MeetingController(
public String meetingsPage(
@RequestParam(required = false) Long registryId,
@RequestParam(required = false) Long selectedMeetingId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
populateMeetingsPage(model, registryId, selectedMeetingId);
return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@PostMapping
Expand Down Expand Up @@ -69,7 +70,7 @@ public String createMeeting(
notes
);

model.addAttribute("successMessage", "sammanträdet skapades.");
model.addAttribute("successMessage", "Sammanträdet skapades.");
populateMeetingsPage(model, registryId, meeting.getId());

} catch (InvalidMeetingStateException | MeetingNotFoundException | RegistryNotFoundException exception) {
Expand All @@ -81,11 +82,7 @@ public String createMeeting(
populateMeetingsPage(model, safeRegistryId, null);
}

if (htmx != null) {
return "fragments/admin-meetings :: content";
}

return "admin/meetings";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/update")
Expand All @@ -97,6 +94,7 @@ public String updateMeeting(
@RequestParam(required = false) String location,
@RequestParam(required = false) String notes,
@RequestParam MeetingStatus status,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -115,7 +113,7 @@ public String updateMeeting(
status
);

model.addAttribute("successMessage", "sammanträdet uppdaterades.");
model.addAttribute("successMessage", "Sammanträdet uppdaterades.");
populateMeetingsPage(model, updatedMeeting.getRegistry().getId(), updatedMeeting.getId());

} catch (InvalidMeetingStateException | MeetingNotFoundException | RegistryNotFoundException exception) {
Expand All @@ -129,12 +127,13 @@ public String updateMeeting(
populateMeetingsPage(model, registryId, registryId == null ? null : meetingId);
}

return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/delete")
public String deleteMeeting(
@PathVariable Long meetingId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -143,15 +142,15 @@ public String deleteMeeting(

meetingService.deleteMeeting(meetingId);

model.addAttribute("successMessage", "sammanträdet togs bort.");
model.addAttribute("successMessage", "Sammanträdet togs bort.");
populateMeetingsPage(model, registryId, null);

} catch (Exception exception) {
model.addAttribute("errorMessage", exception.getMessage());
populateMeetingsPage(model, null, null);
}
Comment on lines +148 to +151

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Catch‑all catch (Exception ...) masks unexpected failures.

Catching Exception here (and in moveAgendaItemUp Line 216, moveAgendaItemDown Line 238, addAgendaDocument Line 290, removeAgendaDocument Line 313) means any NullPointerException, DataAccessException, DateTimeParseException, etc., will be silently rendered as a Swedish error message in the UI. That hides real bugs from observability and prevents GlobalViewExceptionHandler from mapping them to the appropriate HTTP status.

Prefer catching the concrete domain exceptions the service is documented to throw (e.g. MeetingNotFoundException, MeetingAgendaItemNotFoundException, MeetingAgendaDocumentNotFoundException, InvalidMeetingStateException, FileInUseException) and let the rest bubble up to the global handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/meeting/MeetingController.java` around
lines 149 - 152, Replace the catch-all "catch (Exception exception)" in
MeetingController (the block shown and similar blocks in moveAgendaItemUp,
moveAgendaItemDown, addAgendaDocument, removeAgendaDocument) with explicit
catches for the domain exceptions your services declare (e.g.
MeetingNotFoundException, MeetingAgendaItemNotFoundException,
MeetingAgendaDocumentNotFoundException, InvalidMeetingStateException,
FileInUseException); in each specific catch set the model attribute
"errorMessage" from the exception and call populateMeetingsPage(model, null,
null), and do not swallow other exceptions—either let them propagate (no catch)
or rethrow them so the GlobalViewExceptionHandler can handle them.


return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@GetMapping("/{meetingId}")
Expand All @@ -167,7 +166,7 @@ public String showMeeting(
return "fragments/admin-meetings :: content";
}

return "admin/meetings";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/agenda-items")
Expand All @@ -192,17 +191,14 @@ public String addAgendaItem(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

if (htmx != null) {
return "fragments/admin-meetings :: content";
}

return "admin/meetings";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/agenda-items/{agendaItemId}/move-up")
public String moveAgendaItemUp(
@PathVariable Long meetingId,
@PathVariable Long agendaItemId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -218,13 +214,14 @@ public String moveAgendaItemUp(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/agenda-items/{agendaItemId}/move-down")
public String moveAgendaItemDown(
@PathVariable Long meetingId,
@PathVariable Long agendaItemId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -240,7 +237,7 @@ public String moveAgendaItemDown(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@DeleteMapping("/{meetingId}/agenda-items/{agendaItemId}")
Expand All @@ -265,18 +262,15 @@ public String removeAgendaItem(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

if (htmx != null) {
return "fragments/admin-meetings :: content";
}

return "admin/meetings";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/agenda-items/{agendaItemId}/documents")
public String addAgendaDocument(
@PathVariable Long meetingId,
@PathVariable Long agendaItemId,
@RequestParam Long caseFileId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -292,14 +286,15 @@ public String addAgendaDocument(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

@PostMapping("/{meetingId}/agenda-items/{agendaItemId}/documents/{documentId}/remove")
public String removeAgendaDocument(
@PathVariable Long meetingId,
@PathVariable Long agendaItemId,
@PathVariable Long documentId,
@RequestHeader(value = "HX-Request", required = false) String htmx,
Model model
) {
try {
Expand All @@ -315,7 +310,7 @@ public String removeAgendaDocument(
populateMeetingsPageAfterMeetingAction(model, null, null);
}

return "fragments/admin-meetings :: content";
return meetingsView(htmx);
}

private void populateMeetingsPageAfterMeetingAction(Model model, Long registryId, Long meetingId) {
Expand Down Expand Up @@ -371,4 +366,13 @@ private void populateMeetingsPage(Model model, Long registryId, Long selectedMee
model.addAttribute("documentsByAgendaItemId", documentsByAgendaItemId);
model.addAttribute("availableFilesByAgendaItemId", availableFilesByAgendaItemId);
}

private String meetingsView(String htmx) {
if (htmx != null) {
return "fragments/admin-meetings :: content";
}

return "admin/meetings";
}

}
14 changes: 14 additions & 0 deletions src/main/java/backendlab/team4you/meeting/MeetingRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import backendlab.team4you.registry.Registry;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

Expand All @@ -10,4 +11,17 @@ public interface MeetingRepository extends JpaRepository<Meeting, Long> {
List<Meeting> findByRegistryOrderByStartsAtAsc(Registry registry);
List<Meeting> findByRegistryOrderByStartsAtDesc(Registry registry);
List<Meeting> findAllByOrderByStartsAtDesc();
Comment on lines +11 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=java -C2 '\bfindByRegistryOrderByStartsAtAsc\s*\('
rg -nP --type=java -C2 '\bfindByRegistryOrderByStartsAtDesc\s*\('
rg -nP --type=java -C2 '\bfindAllByOrderByStartsAtDesc\s*\('

Repository: ithsjava25/project-backend-team4you

Length of output: 2768


Remove the unused findByRegistryOrderByStartsAtAsc method.

The method at line 11 has no callers in the codebase. Only findByRegistryOrderByStartsAtDesc (line 12, used in MeetingService) and findAllByOrderByStartsAtDesc (line 13, used in MeetingService) are actually called. Drop the unused method to keep the repository interface clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/backendlab/team4you/meeting/MeetingRepository.java` around
lines 11 - 13, Remove the unused repository method
findByRegistryOrderByStartsAtAsc from the MeetingRepository interface; locate
the method declaration in MeetingRepository (the one named
findByRegistryOrderByStartsAtAsc) and delete it so only the used methods
findByRegistryOrderByStartsAtDesc and findAllByOrderByStartsAtDesc remain; no
other changes to MeetingService are required since it already calls the
descending variants.


@Query("""
select m
from Meeting m
where m.status = backendlab.team4you.meeting.MeetingStatus.COMPLETED
and not exists (
select p.id
from Protocol p
where p.meeting = m
)
order by m.startsAt desc
""")
List<Meeting> findCompletedMeetingsWithoutProtocol();
}
Loading