From 9407cb9dc4414b9e7e77615ba9428d86efe59b8d Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 18:44:26 +0200 Subject: [PATCH] feat(dashboard): add contributors to TranscriptionQueueItemDTO with 5-cap and hasMore flag Co-Authored-By: Claude Sonnet 4.6 --- .../dto/TranscriptionQueueItemDTO.java | 11 ++-- .../service/TranscriptionQueueService.java | 41 ++++++++------- .../TranscriptionQueueControllerTest.java | 6 ++- .../TranscriptionQueueServiceTest.java | 52 +++++++++++++++++++ 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java index 833c047b..5f3d3de5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/TranscriptionQueueItemDTO.java @@ -1,20 +1,19 @@ package org.raddatz.familienarchiv.dto; import io.swagger.v3.oas.annotations.media.Schema; +import org.raddatz.familienarchiv.dashboard.ActivityActorDTO; import java.time.LocalDate; +import java.util.List; import java.util.UUID; -/** - * A single row in one of the three Mission Control Strip queues. - * Annotation/block counts drive the per-document mini progress bar - * in the Transkription column and the percentage label in Lesefertig. - */ public record TranscriptionQueueItemDTO( @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String title, LocalDate documentDate, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int annotationCount, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int textedBlockCount, - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int reviewedBlockCount + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int reviewedBlockCount, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) List contributors, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean hasMoreContributors ) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java index bc0118fa..622b8bcf 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionQueueService.java @@ -1,6 +1,8 @@ package org.raddatz.familienarchiv.service; import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.dashboard.ActivityActorDTO; +import org.raddatz.familienarchiv.dashboard.AuditLogQueryService; import org.raddatz.familienarchiv.dto.TranscriptionQueueItemDTO; import org.raddatz.familienarchiv.dto.TranscriptionWeeklyStatsDTO; import org.raddatz.familienarchiv.repository.DocumentRepository; @@ -8,38 +10,29 @@ import org.raddatz.familienarchiv.repository.TranscriptionQueueProjection; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Map; +import java.util.UUID; -/** - * Serves the three Mission Control Strip queues (Segmentierung / Transkription / Lesefertig) - * and the weekly activity pulse used by the column headers. - */ @Service @RequiredArgsConstructor public class TranscriptionQueueService { private static final int DEFAULT_QUEUE_SIZE = 5; + private static final int MAX_CONTRIBUTORS = 5; private final DocumentRepository documentRepository; + private final AuditLogQueryService auditLogQueryService; public List getSegmentationQueue() { - return documentRepository.findSegmentationQueue(DEFAULT_QUEUE_SIZE) - .stream() - .map(this::toDTO) - .toList(); + return enrichWithContributors(documentRepository.findSegmentationQueue(DEFAULT_QUEUE_SIZE)); } public List getTranscriptionQueue() { - return documentRepository.findTranscriptionQueue(DEFAULT_QUEUE_SIZE) - .stream() - .map(this::toDTO) - .toList(); + return enrichWithContributors(documentRepository.findTranscriptionQueue(DEFAULT_QUEUE_SIZE)); } public List getReadyToReadQueue() { - return documentRepository.findReadyToReadQueue(DEFAULT_QUEUE_SIZE) - .stream() - .map(this::toDTO) - .toList(); + return enrichWithContributors(documentRepository.findReadyToReadQueue(DEFAULT_QUEUE_SIZE)); } public TranscriptionWeeklyStatsDTO getWeeklyStats() { @@ -50,14 +43,26 @@ public class TranscriptionQueueService { ); } - private TranscriptionQueueItemDTO toDTO(TranscriptionQueueProjection p) { + private List enrichWithContributors(List projections) { + List ids = projections.stream().map(TranscriptionQueueProjection::getId).toList(); + Map> contributorMap = auditLogQueryService.findContributorsPerDocument(ids); + return projections.stream() + .map(p -> toDTO(p, contributorMap.getOrDefault(p.getId(), List.of()))) + .toList(); + } + + private TranscriptionQueueItemDTO toDTO(TranscriptionQueueProjection p, List allContributors) { + boolean hasMore = allContributors.size() > MAX_CONTRIBUTORS; + List capped = hasMore ? allContributors.subList(0, MAX_CONTRIBUTORS) : allContributors; return new TranscriptionQueueItemDTO( p.getId(), p.getTitle(), p.getDocumentDate(), p.getAnnotationCount(), p.getTextedBlockCount(), - p.getReviewedBlockCount() + p.getReviewedBlockCount(), + capped, + hasMore ); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/TranscriptionQueueControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/TranscriptionQueueControllerTest.java index 8b6f9fab..de325050 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/TranscriptionQueueControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/TranscriptionQueueControllerTest.java @@ -17,6 +17,8 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.raddatz.familienarchiv.dashboard.ActivityActorDTO; + import java.time.LocalDate; import java.util.List; import java.util.UUID; @@ -41,7 +43,9 @@ class TranscriptionQueueControllerTest { UUID.fromString("00000000-0000-0000-0000-000000000001"), "Testbrief", LocalDate.of(1920, 6, 15), - 3, 1, 0 + 3, 1, 0, + List.of(new ActivityActorDTO("TR", "#a6dad8", "Test Raddatz")), + false ); private static final TranscriptionWeeklyStatsDTO STATS = new TranscriptionWeeklyStatsDTO(2L, 5L); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionQueueServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionQueueServiceTest.java index 0a76114b..851a40f1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionQueueServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionQueueServiceTest.java @@ -1,10 +1,13 @@ package org.raddatz.familienarchiv.service; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.raddatz.familienarchiv.dashboard.ActivityActorDTO; +import org.raddatz.familienarchiv.dashboard.AuditLogQueryService; import org.raddatz.familienarchiv.dto.TranscriptionQueueItemDTO; import org.raddatz.familienarchiv.dto.TranscriptionWeeklyStatsDTO; import org.raddatz.familienarchiv.repository.DocumentRepository; @@ -13,17 +16,25 @@ import org.raddatz.familienarchiv.repository.TranscriptionWeeklyStatsProjection; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class TranscriptionQueueServiceTest { @Mock DocumentRepository documentRepository; + @Mock AuditLogQueryService auditLogQueryService; @InjectMocks TranscriptionQueueService service; + @BeforeEach + void stubContributors() { + lenient().when(auditLogQueryService.findContributorsPerDocument(any())).thenReturn(Map.of()); + } + // ─── getSegmentationQueue ───────────────────────────────────────────────── @Test @@ -108,6 +119,47 @@ class TranscriptionQueueServiceTest { assertThat(result.transcriptionCount()).isEqualTo(0L); } + // ─── contributors ──────────────────────────────────────────────────────── + + @Test + void getSegmentationQueue_includesContributors_whenAuditDataPresent() { + UUID docId = UUID.randomUUID(); + TranscriptionQueueProjection proj = mockQueueProjection(docId, "Brief", null, 0, 0, 0); + when(documentRepository.findSegmentationQueue(5)).thenReturn(List.of(proj)); + + ActivityActorDTO actor = new ActivityActorDTO("MR", "#a6dad8", "Max Raddatz"); + when(auditLogQueryService.findContributorsPerDocument(List.of(docId))) + .thenReturn(Map.of(docId, List.of(actor))); + + List result = service.getSegmentationQueue(); + + assertThat(result.get(0).contributors()).containsExactly(actor); + assertThat(result.get(0).hasMoreContributors()).isFalse(); + } + + @Test + void getSegmentationQueue_capsContributorsAtFive_andSetsHasMoreFlag() { + UUID docId = UUID.randomUUID(); + TranscriptionQueueProjection proj = mockQueueProjection(docId, "Brief", null, 0, 0, 0); + when(documentRepository.findSegmentationQueue(5)).thenReturn(List.of(proj)); + + List sixActors = List.of( + new ActivityActorDTO("A1", "#111", "Alice One"), + new ActivityActorDTO("A2", "#222", "Alice Two"), + new ActivityActorDTO("A3", "#333", "Alice Three"), + new ActivityActorDTO("A4", "#444", "Alice Four"), + new ActivityActorDTO("A5", "#555", "Alice Five"), + new ActivityActorDTO("A6", "#666", "Alice Six") + ); + when(auditLogQueryService.findContributorsPerDocument(List.of(docId))) + .thenReturn(Map.of(docId, sixActors)); + + List result = service.getSegmentationQueue(); + + assertThat(result.get(0).contributors()).hasSize(5); + assertThat(result.get(0).hasMoreContributors()).isTrue(); + } + // ─── helpers ───────────────────────────────────────────────────────────── private TranscriptionQueueProjection mockQueueProjection(