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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user