Compare commits
2 Commits
dc487e2f97
...
2171c3702a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2171c3702a | ||
|
|
6976daa910 |
@@ -11,6 +11,7 @@ import java.util.UUID;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import org.raddatz.familienarchiv.dto.DocumentUpdateDTO;
|
import org.raddatz.familienarchiv.dto.DocumentUpdateDTO;
|
||||||
import org.raddatz.familienarchiv.dto.DocumentVersionSummary;
|
import org.raddatz.familienarchiv.dto.DocumentVersionSummary;
|
||||||
import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO;
|
import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO;
|
||||||
@@ -167,7 +168,7 @@ public class DocumentController {
|
|||||||
|
|
||||||
@GetMapping("/incomplete")
|
@GetMapping("/incomplete")
|
||||||
public List<IncompleteDocumentDTO> getIncomplete(
|
public List<IncompleteDocumentDTO> getIncomplete(
|
||||||
@RequestParam(defaultValue = "10") int size) {
|
@Parameter(description = "Maximum number of results") @RequestParam(defaultValue = "10") int size) {
|
||||||
return documentService.findIncompleteDocuments(size);
|
return documentService.findIncompleteDocuments(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,12 @@ public class DocumentController {
|
|||||||
.orElse(ResponseEntity.noContent().build());
|
.orElse(ResponseEntity.noContent().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/recent-activity")
|
||||||
|
public ResponseEntity<List<Document>> getRecentActivity(
|
||||||
|
@RequestParam(defaultValue = "5") int size) {
|
||||||
|
return ResponseEntity.ok(documentService.getRecentActivity(size));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/search")
|
@GetMapping("/search")
|
||||||
public ResponseEntity<List<Document>> search(
|
public ResponseEntity<List<Document>> search(
|
||||||
@RequestParam(required = false) String q,
|
@RequestParam(required = false) String q,
|
||||||
@@ -186,7 +193,7 @@ public class DocumentController {
|
|||||||
@RequestParam(required = false) UUID senderId,
|
@RequestParam(required = false) UUID senderId,
|
||||||
@RequestParam(required = false) UUID receiverId,
|
@RequestParam(required = false) UUID receiverId,
|
||||||
@RequestParam(required = false, name = "tag") List<String> tags,
|
@RequestParam(required = false, name = "tag") List<String> tags,
|
||||||
@RequestParam(required = false) DocumentStatus status) {
|
@Parameter(description = "Filter by document status") @RequestParam(required = false) DocumentStatus status) {
|
||||||
return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, status));
|
return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,12 @@ public class DocumentService {
|
|||||||
return documentRepository.save(doc);
|
return documentRepository.save(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0. Zuletzt aktive Dokumente (sortiert nach updatedAt DESC)
|
||||||
|
public List<Document> getRecentActivity(int size) {
|
||||||
|
return documentRepository.findAll(Sort.by(Sort.Direction.DESC, "updatedAt"))
|
||||||
|
.stream().limit(size).toList();
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Allgemeine Suche (für das Suchfeld im Frontend)
|
// 1. Allgemeine Suche (für das Suchfeld im Frontend)
|
||||||
public List<Document> searchDocuments(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List<String> tags, DocumentStatus status) {
|
public List<Document> searchDocuments(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List<String> tags, DocumentStatus status) {
|
||||||
Specification<Document> spec = Specification.where(hasText(text))
|
Specification<Document> spec = Specification.where(hasText(text))
|
||||||
|
|||||||
@@ -406,6 +406,27 @@ class DocumentControllerTest {
|
|||||||
.andExpect(status().isNoContent());
|
.andExpect(status().isNoContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── GET /api/documents/recent-activity ──────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRecentActivity_returns401_whenUnauthenticated() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/documents/recent-activity"))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void getRecentActivity_returnsOkWithDocuments() throws Exception {
|
||||||
|
Document doc1 = Document.builder().id(UUID.randomUUID()).title("Alpha").originalFilename("a.pdf").build();
|
||||||
|
Document doc2 = Document.builder().id(UUID.randomUUID()).title("Beta").originalFilename("b.pdf").build();
|
||||||
|
when(documentService.getRecentActivity(5)).thenReturn(List.of(doc1, doc2));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/documents/recent-activity").param("size", "5"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$[0].title").value("Alpha"))
|
||||||
|
.andExpect(jsonPath("$[1].title").value("Beta"));
|
||||||
|
}
|
||||||
|
|
||||||
// ─── GET /api/documents/{id}/versions ────────────────────────────────────
|
// ─── GET /api/documents/{id}/versions ────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1213,4 +1213,26 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Sort.class));
|
verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Sort.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── getRecentActivity ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRecentActivity_returnsMostRecentlyUpdatedDocuments() {
|
||||||
|
java.time.LocalDateTime oldest = java.time.LocalDateTime.of(2024, 1, 1, 0, 0);
|
||||||
|
java.time.LocalDateTime middle = java.time.LocalDateTime.of(2024, 6, 1, 0, 0);
|
||||||
|
java.time.LocalDateTime newest = java.time.LocalDateTime.of(2024, 12, 1, 0, 0);
|
||||||
|
|
||||||
|
Document doc1 = Document.builder().id(UUID.randomUUID()).title("Oldest").build();
|
||||||
|
Document doc2 = Document.builder().id(UUID.randomUUID()).title("Middle").build();
|
||||||
|
Document doc3 = Document.builder().id(UUID.randomUUID()).title("Newest").build();
|
||||||
|
|
||||||
|
// findAll(Sort) returns documents already sorted DESC by updatedAt
|
||||||
|
when(documentRepository.findAll(Sort.by(Sort.Direction.DESC, "updatedAt")))
|
||||||
|
.thenReturn(List.of(doc3, doc2, doc1));
|
||||||
|
|
||||||
|
List<Document> result = documentService.getRecentActivity(2);
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
assertThat(result).containsExactly(doc3, doc2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
"dashboard_notification_replied": "hat geantwortet",
|
"dashboard_notification_replied": "hat geantwortet",
|
||||||
"dashboard_needs_metadata_heading": "Metadaten fehlen",
|
"dashboard_needs_metadata_heading": "Metadaten fehlen",
|
||||||
"dashboard_needs_metadata_show_all": "Alle anzeigen",
|
"dashboard_needs_metadata_show_all": "Alle anzeigen",
|
||||||
"dashboard_recent_heading": "Zuletzt hinzugefügt",
|
"dashboard_recent_heading": "Zuletzt aktiv",
|
||||||
"dashboard_resume_label": "Zuletzt geöffnet:",
|
"dashboard_resume_label": "Zuletzt geöffnet:",
|
||||||
"dashboard_resume_fallback": "Unbekanntes Dokument"
|
"dashboard_resume_fallback": "Unbekanntes Dokument"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
"dashboard_notification_replied": "replied",
|
"dashboard_notification_replied": "replied",
|
||||||
"dashboard_needs_metadata_heading": "Missing Metadata",
|
"dashboard_needs_metadata_heading": "Missing Metadata",
|
||||||
"dashboard_needs_metadata_show_all": "Show all",
|
"dashboard_needs_metadata_show_all": "Show all",
|
||||||
"dashboard_recent_heading": "Recently Added",
|
"dashboard_recent_heading": "Recent Activity",
|
||||||
"dashboard_resume_label": "Last opened:",
|
"dashboard_resume_label": "Last opened:",
|
||||||
"dashboard_resume_fallback": "Unknown document"
|
"dashboard_resume_fallback": "Unknown document"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
"dashboard_notification_replied": "respondió",
|
"dashboard_notification_replied": "respondió",
|
||||||
"dashboard_needs_metadata_heading": "Metadatos incompletos",
|
"dashboard_needs_metadata_heading": "Metadatos incompletos",
|
||||||
"dashboard_needs_metadata_show_all": "Ver todos",
|
"dashboard_needs_metadata_show_all": "Ver todos",
|
||||||
"dashboard_recent_heading": "Añadidos recientemente",
|
"dashboard_recent_heading": "Actividad reciente",
|
||||||
"dashboard_resume_label": "Último abierto:",
|
"dashboard_resume_label": "Último abierto:",
|
||||||
"dashboard_resume_fallback": "Documento desconocido"
|
"dashboard_resume_fallback": "Documento desconocido"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getLocale } from '$lib/paraglide/runtime.js';
|
|||||||
type Document = {
|
type Document = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
createdAt?: string;
|
updatedAt?: string;
|
||||||
sender?: { id: string; firstName: string; lastName: string };
|
sender?: { id: string; firstName: string; lastName: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,12 +37,12 @@ function formatDate(dateStr: string): string {
|
|||||||
>
|
>
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</a>
|
</a>
|
||||||
{#if doc.createdAt}
|
{#if doc.updatedAt}
|
||||||
<span
|
<span
|
||||||
data-testid="doc-date-{doc.id}"
|
data-testid="doc-date-{doc.id}"
|
||||||
class="ml-2 shrink-0 font-sans text-xs text-gray-400"
|
class="ml-2 shrink-0 font-sans text-xs text-gray-400"
|
||||||
>
|
>
|
||||||
{formatDate(doc.createdAt)}
|
{formatDate(doc.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ afterEach(cleanup);
|
|||||||
type Document = {
|
type Document = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
createdAt?: string;
|
updatedAt?: string;
|
||||||
sender?: { id: string; firstName: string; lastName: string };
|
sender?: { id: string; firstName: string; lastName: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeDoc(id: string, title: string, createdAt?: string): Document {
|
function makeDoc(id: string, title: string, updatedAt?: string): Document {
|
||||||
return { id, title, createdAt };
|
return { id, title, updatedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('DashboardRecentDocuments', () => {
|
describe('DashboardRecentDocuments', () => {
|
||||||
|
|||||||
@@ -628,6 +628,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/documents/recent-activity": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["getRecentActivity"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/documents/incomplete": {
|
"/api/documents/incomplete": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -2328,6 +2344,28 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
getRecentActivity: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description OK */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"*/*": components["schemas"]["Document"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
getIncomplete: {
|
getIncomplete: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export async function load({ url, fetch }) {
|
|||||||
const [mentionsResult, incompleteResult, recentResult] = await Promise.allSettled([
|
const [mentionsResult, incompleteResult, recentResult] = await Promise.allSettled([
|
||||||
api.GET('/api/notifications', { params: { query: { size: 5 } } }),
|
api.GET('/api/notifications', { params: { query: { size: 5 } } }),
|
||||||
api.GET('/api/documents/incomplete', { params: { query: { size: 5 } } }),
|
api.GET('/api/documents/incomplete', { params: { query: { size: 5 } } }),
|
||||||
api.GET('/api/documents/search', { params: { query: {} } })
|
api.GET('/api/documents/recent-activity', { params: { query: { size: 5 } } })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (mentionsResult.status === 'fulfilled' && mentionsResult.value.response.ok) {
|
if (mentionsResult.status === 'fulfilled' && mentionsResult.value.response.ok) {
|
||||||
@@ -70,7 +70,7 @@ export async function load({ url, fetch }) {
|
|||||||
incompleteDocs = incompleteResult.value.data ?? [];
|
incompleteDocs = incompleteResult.value.data ?? [];
|
||||||
}
|
}
|
||||||
if (recentResult.status === 'fulfilled' && recentResult.value.response.ok) {
|
if (recentResult.status === 'fulfilled' && recentResult.value.response.ok) {
|
||||||
recentDocs = (recentResult.value.data ?? []).slice(0, 5);
|
recentDocs = recentResult.value.data ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user