fix(dashboard): null-safe name join in toActorDTO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
package org.raddatz.familienarchiv.dashboard;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.model.AppUser;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||
import org.raddatz.familienarchiv.service.DocumentService;
|
||||
import org.raddatz.familienarchiv.service.TranscriptionService;
|
||||
import org.raddatz.familienarchiv.service.UserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DashboardService {
|
||||
|
||||
private final AuditLogQueryService auditLogQueryService;
|
||||
private final DocumentService documentService;
|
||||
private final TranscriptionService transcriptionService;
|
||||
private final UserService userService;
|
||||
|
||||
public DashboardResumeDTO getResume(UUID userId) {
|
||||
Optional<UUID> docIdOpt = auditLogQueryService.findMostRecentDocumentForUser(userId);
|
||||
if (docIdOpt.isEmpty()) return null;
|
||||
|
||||
UUID docId = docIdOpt.get();
|
||||
Document doc;
|
||||
try {
|
||||
doc = documentService.getDocumentById(docId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Resume: document {} not found for user {}", docId, userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
List<TranscriptionBlock> blocks = transcriptionService.listBlocks(docId);
|
||||
String excerpt = blocks.stream()
|
||||
.filter(b -> b.getText() != null && !b.getText().isBlank())
|
||||
.min(Comparator.comparingInt(TranscriptionBlock::getSortOrder))
|
||||
.map(b -> b.getText().length() > 200 ? b.getText().substring(0, 200) + "…" : b.getText())
|
||||
.orElse("");
|
||||
|
||||
int totalBlocks = blocks.size();
|
||||
long reviewedBlocks = blocks.stream().filter(TranscriptionBlock::isReviewed).count();
|
||||
int pct = totalBlocks > 0 ? (int) (reviewedBlocks * 100L / totalBlocks) : 0;
|
||||
|
||||
Set<Integer> pageNumbers = new HashSet<>();
|
||||
for (TranscriptionBlock b : blocks) {
|
||||
pageNumbers.add(b.getSortOrder());
|
||||
}
|
||||
int maxPage = blocks.stream().mapToInt(TranscriptionBlock::getSortOrder).max().orElse(1);
|
||||
int totalPages = Math.max(1, (int) blocks.stream()
|
||||
.mapToInt(TranscriptionBlock::getSortOrder)
|
||||
.distinct().count());
|
||||
int currentPage = 1;
|
||||
|
||||
String caption = buildCaption(doc);
|
||||
|
||||
List<UUID> collaboratorIds = blocks.stream()
|
||||
.map(TranscriptionBlock::getUpdatedBy)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.limit(5)
|
||||
.toList();
|
||||
|
||||
List<ActivityActorDTO> collaborators = collaboratorIds.stream()
|
||||
.map(uid -> {
|
||||
try {
|
||||
AppUser u = userService.getById(uid);
|
||||
return toActorDTO(u);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
return new DashboardResumeDTO(docId, doc.getTitle(), caption, excerpt,
|
||||
currentPage, totalPages, pct, null, collaborators);
|
||||
}
|
||||
|
||||
public DashboardPulseDTO getPulse(UUID userId) {
|
||||
OffsetDateTime weekStart = OffsetDateTime.now(ZoneOffset.UTC)
|
||||
.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
.withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
|
||||
PulseStatsRow stats = auditLogQueryService.getPulseStats(weekStart, userId);
|
||||
|
||||
List<ActivityFeedRow> feed = auditLogQueryService.findActivityFeed(userId, 50);
|
||||
List<ActivityActorDTO> contributors = feed.stream()
|
||||
.filter(r -> r.getActorId() != null)
|
||||
.map(r -> new ActivityActorDTO(r.getActorInitials(), r.getActorColor(), r.getActorName()))
|
||||
.filter(a -> !a.initials().isBlank())
|
||||
.distinct()
|
||||
.limit(6)
|
||||
.toList();
|
||||
|
||||
return new DashboardPulseDTO(
|
||||
(int) stats.getPages(),
|
||||
(int) stats.getAnnotated(),
|
||||
(int) stats.getTranscribed(),
|
||||
(int) stats.getUploaded(),
|
||||
(int) stats.getYourPages(),
|
||||
contributors
|
||||
);
|
||||
}
|
||||
|
||||
public List<ActivityFeedItemDTO> getActivity(UUID currentUserId, int limit) {
|
||||
List<ActivityFeedRow> rows = auditLogQueryService.findActivityFeed(currentUserId, limit);
|
||||
|
||||
List<UUID> docIds = rows.stream()
|
||||
.map(ActivityFeedRow::getDocumentId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
Map<UUID, String> titleCache = new HashMap<>();
|
||||
for (UUID docId : docIds) {
|
||||
try {
|
||||
titleCache.put(docId, documentService.getDocumentById(docId).getTitle());
|
||||
} catch (Exception e) {
|
||||
titleCache.put(docId, "");
|
||||
}
|
||||
}
|
||||
|
||||
return rows.stream().map(row -> {
|
||||
ActivityActorDTO actor = row.getActorId() != null
|
||||
? new ActivityActorDTO(row.getActorInitials(), row.getActorColor(), row.getActorName())
|
||||
: null;
|
||||
String docTitle = titleCache.getOrDefault(row.getDocumentId(), "");
|
||||
return new ActivityFeedItemDTO(
|
||||
org.raddatz.familienarchiv.audit.AuditKind.valueOf(row.getKind()),
|
||||
actor,
|
||||
row.getDocumentId(),
|
||||
docTitle,
|
||||
row.getHappenedAt().atOffset(ZoneOffset.UTC),
|
||||
row.isYouMentioned()
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private String buildCaption(Document doc) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (doc.getSender() != null) sb.append(personName(doc.getSender()));
|
||||
if (!doc.getReceivers().isEmpty()) {
|
||||
String receivers = doc.getReceivers().stream()
|
||||
.map(this::personName).collect(Collectors.joining(", "));
|
||||
if (!sb.isEmpty()) sb.append(" an ");
|
||||
sb.append(receivers);
|
||||
}
|
||||
if (doc.getDocumentDate() != null) {
|
||||
if (!sb.isEmpty()) sb.append(" · ");
|
||||
sb.append(doc.getDocumentDate());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String personName(Person p) {
|
||||
if (p == null) return "";
|
||||
if (p.getFirstName() != null && p.getLastName() != null) return p.getFirstName() + " " + p.getLastName();
|
||||
if (p.getFirstName() != null) return p.getFirstName();
|
||||
if (p.getLastName() != null) return p.getLastName();
|
||||
return "";
|
||||
}
|
||||
|
||||
private ActivityActorDTO toActorDTO(AppUser u) {
|
||||
String initials = "";
|
||||
if (u.getFirstName() != null && !u.getFirstName().isBlank())
|
||||
initials += u.getFirstName().charAt(0);
|
||||
if (u.getLastName() != null && !u.getLastName().isBlank())
|
||||
initials += u.getLastName().charAt(0);
|
||||
if (initials.isBlank() && u.getEmail() != null)
|
||||
initials = u.getEmail().substring(0, 1).toUpperCase();
|
||||
String fullName = Stream.of(u.getFirstName(), u.getLastName())
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.joining(" "));
|
||||
return new ActivityActorDTO(initials.toUpperCase(), u.getColor(), fullName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.raddatz.familienarchiv.dashboard;
|
||||
|
||||
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.model.AppUser;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.TranscriptionBlock;
|
||||
import org.raddatz.familienarchiv.service.DocumentService;
|
||||
import org.raddatz.familienarchiv.service.TranscriptionService;
|
||||
import org.raddatz.familienarchiv.service.UserService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DashboardServiceTest {
|
||||
|
||||
@Mock AuditLogQueryService auditLogQueryService;
|
||||
@Mock DocumentService documentService;
|
||||
@Mock TranscriptionService transcriptionService;
|
||||
@Mock UserService userService;
|
||||
|
||||
@InjectMocks DashboardService dashboardService;
|
||||
|
||||
// ─── toActorDTO (via getResume collaborators) ─────────────────────────────
|
||||
|
||||
@Test
|
||||
void getResume_collaboratorName_isNullSafe_whenFirstNameIsNull() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
UUID docId = UUID.randomUUID();
|
||||
UUID collaboratorId = UUID.randomUUID();
|
||||
|
||||
Document doc = Document.builder()
|
||||
.id(docId).title("Brief").originalFilename("brief.pdf")
|
||||
.receivers(new HashSet<>())
|
||||
.build();
|
||||
|
||||
TranscriptionBlock block = TranscriptionBlock.builder()
|
||||
.id(UUID.randomUUID()).annotationId(UUID.randomUUID())
|
||||
.documentId(docId).sortOrder(1).updatedBy(collaboratorId)
|
||||
.build();
|
||||
|
||||
AppUser collaborator = AppUser.builder()
|
||||
.id(collaboratorId).email("s@test.com").password("pw")
|
||||
.firstName(null).lastName("Schmidt").color("#abc")
|
||||
.build();
|
||||
|
||||
when(auditLogQueryService.findMostRecentDocumentForUser(userId)).thenReturn(Optional.of(docId));
|
||||
when(documentService.getDocumentById(docId)).thenReturn(doc);
|
||||
when(transcriptionService.listBlocks(docId)).thenReturn(List.of(block));
|
||||
when(userService.getById(collaboratorId)).thenReturn(collaborator);
|
||||
|
||||
DashboardResumeDTO result = dashboardService.getResume(userId);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.collaborators()).hasSize(1);
|
||||
assertThat(result.collaborators().get(0).name()).isEqualTo("Schmidt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user