feat(documents): add DocumentService.getDensity (#385)

Maps the repository's Object[] rows into a DocumentDensityResult and pairs
them with the archive-wide min/max meta_date range. Read-only, no
@Transactional needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-07 21:50:05 +02:00
parent c90b42d045
commit fbf4725e97
2 changed files with 65 additions and 0 deletions

View File

@@ -125,6 +125,19 @@ public class DocumentService {
return titles;
}
/**
* Per-month document counts for the timeline density widget (issue #385).
* Returns only months that have at least one document — the frontend fills
* gaps using the {@code minDate}/{@code maxDate} bounds.
*/
public DocumentDensityResult getDensity(LocalDate from, LocalDate to) {
List<MonthBucket> buckets = documentRepository.findDensityByMonth(from, to).stream()
.map(row -> new MonthBucket((String) row[0], ((Number) row[1]).intValue()))
.toList();
DocumentDateRangeProjection range = documentRepository.findMinMaxDocumentDate();
return new DocumentDensityResult(buckets, range.getMinDate(), range.getMaxDate());
}
/**
* Lädt eine Datei hoch.
* - Prüft, ob ein Eintrag (aus Excel) schon existiert.

View File

@@ -2321,4 +2321,56 @@ class DocumentServiceTest {
assertThat(documentService.save(doc)).isEqualTo(doc);
verify(documentRepository).save(doc);
}
// ─── getDensity ────────────────────────────────────────────────────────────
@Test
void getDensity_returnsEmptyResult_whenArchiveIsEmpty() {
when(documentRepository.findDensityByMonth(null, null)).thenReturn(List.of());
DocumentDateRangeProjection emptyRange = mock(DocumentDateRangeProjection.class);
when(emptyRange.getMinDate()).thenReturn(null);
when(emptyRange.getMaxDate()).thenReturn(null);
when(documentRepository.findMinMaxDocumentDate()).thenReturn(emptyRange);
DocumentDensityResult result = documentService.getDensity(null, null);
assertThat(result.buckets()).isEmpty();
assertThat(result.minDate()).isNull();
assertThat(result.maxDate()).isNull();
}
@Test
void getDensity_mapsRepositoryRowsToMonthBuckets() {
List<Object[]> rows = List.of(
new Object[]{"1915-08", 2L},
new Object[]{"1915-09", 1L}
);
when(documentRepository.findDensityByMonth(null, null)).thenReturn(rows);
DocumentDateRangeProjection range = mock(DocumentDateRangeProjection.class);
when(range.getMinDate()).thenReturn(LocalDate.of(1915, 8, 3));
when(range.getMaxDate()).thenReturn(LocalDate.of(1915, 9, 1));
when(documentRepository.findMinMaxDocumentDate()).thenReturn(range);
DocumentDensityResult result = documentService.getDensity(null, null);
assertThat(result.buckets()).extracting(MonthBucket::month)
.containsExactly("1915-08", "1915-09");
assertThat(result.buckets()).extracting(MonthBucket::count)
.containsExactly(2, 1);
assertThat(result.minDate()).isEqualTo(LocalDate.of(1915, 8, 3));
assertThat(result.maxDate()).isEqualTo(LocalDate.of(1915, 9, 1));
}
@Test
void getDensity_passesFromAndToBoundsToRepository() {
when(documentRepository.findDensityByMonth(any(), any())).thenReturn(List.of());
DocumentDateRangeProjection range = mock(DocumentDateRangeProjection.class);
when(documentRepository.findMinMaxDocumentDate()).thenReturn(range);
LocalDate from = LocalDate.of(1914, 1, 1);
LocalDate to = LocalDate.of(1918, 12, 31);
documentService.getDensity(from, to);
verify(documentRepository).findDensityByMonth(from, to);
}
}