feat(PROJ-39): eDiscovery Export + Feature-Specs PROJ-40–43
- Neuer POST /api/export/ediscovery Handler (internal/api/ediscovery.go) - Route in server.go registriert - Feature-Specs PROJ-39 bis PROJ-43 angelegt - INDEX.md aktualisiert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+84
-8
@@ -3,7 +3,7 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { searchEmails, exportMailsZIP, uploadMailFilesUser, getUploadProgressUser, type SearchHit, type UploadJob } from "@/lib/api";
|
||||
import { searchEmails, exportMailsZIP, exportEDiscovery, uploadMailFilesUser, getUploadProgressUser, type SearchHit, type UploadJob } from "@/lib/api";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -68,6 +68,9 @@ export default function SearchPage() {
|
||||
const [exportOpen, setExportOpen] = useState(false);
|
||||
const [exportAttachments, setExportAttachments] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [ediscoveryOpen, setEdiscoveryOpen] = useState(false);
|
||||
const [ediscoveryCaseName, setEdiscoveryCaseName] = useState("");
|
||||
const [ediscoveryLoading, setEdiscoveryLoading] = useState(false);
|
||||
|
||||
// Upload state
|
||||
const [uploadOpen, setUploadOpen] = useState(false);
|
||||
@@ -141,6 +144,31 @@ export default function SearchPage() {
|
||||
doSearch(1);
|
||||
}
|
||||
|
||||
async function handleEDiscoveryExport() {
|
||||
setEdiscoveryLoading(true);
|
||||
try {
|
||||
const { blob, filename } = await exportEDiscovery({
|
||||
case_name: ediscoveryCaseName || undefined,
|
||||
q: query || undefined,
|
||||
from: fromFilter || undefined,
|
||||
to: toFilter || undefined,
|
||||
date_from: dateFrom || undefined,
|
||||
date_to: dateTo || undefined,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setEdiscoveryOpen(false);
|
||||
} catch (e) {
|
||||
alert(`eDiscovery Export fehlgeschlagen: ${e instanceof Error ? e.message : e}`);
|
||||
} finally {
|
||||
setEdiscoveryLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportZIP() {
|
||||
setExporting(true);
|
||||
try {
|
||||
@@ -346,13 +374,18 @@ export default function SearchPage() {
|
||||
: `${total} E-Mail${total !== 1 ? "s" : ""} im Archiv`}
|
||||
</div>
|
||||
|
||||
{selected.size > 0 && (
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{selected.size} ausgewählt</span>
|
||||
<Button size="sm" onClick={() => setExportOpen(true)}>Als ZIP exportieren</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setSelected(new Set())}>Auswahl aufheben</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-2 flex items-center gap-2 flex-wrap">
|
||||
{selected.size > 0 && (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">{selected.size} ausgewählt</span>
|
||||
<Button size="sm" onClick={() => setExportOpen(true)}>Als ZIP exportieren</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setSelected(new Set())}>Auswahl aufheben</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={() => setEdiscoveryOpen(true)}>
|
||||
eDiscovery Export
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
@@ -485,6 +518,49 @@ export default function SearchPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* eDiscovery Export Dialog */}
|
||||
<Dialog open={ediscoveryOpen} onOpenChange={setEdiscoveryOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>eDiscovery Export</DialogTitle>
|
||||
<DialogDescription>
|
||||
Exportiert alle Mails der aktuellen Suche als ZIP mit Metadaten-CSV und README.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="case-name">Case-Name (optional)</Label>
|
||||
<input
|
||||
id="case-name"
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="z.B. Ermittlung-2026-Q1"
|
||||
value={ediscoveryCaseName}
|
||||
onChange={(e) => setEdiscoveryCaseName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground space-y-1">
|
||||
<div><span className="font-medium">Aktive Filter:</span></div>
|
||||
{query && <div>Suche: <span className="font-mono">{query}</span></div>}
|
||||
{fromFilter && <div>Von: <span className="font-mono">{fromFilter}</span></div>}
|
||||
{toFilter && <div>An: <span className="font-mono">{toFilter}</span></div>}
|
||||
{dateFrom && <div>Von Datum: {dateFrom}</div>}
|
||||
{dateTo && <div>Bis Datum: {dateTo}</div>}
|
||||
{!query && !fromFilter && !toFilter && !dateFrom && !dateTo && (
|
||||
<div className="italic">Keine Filter — alle archivierten Mails werden exportiert</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEdiscoveryOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button onClick={handleEDiscoveryExport} disabled={ediscoveryLoading}>
|
||||
{ediscoveryLoading ? "Wird exportiert..." : "ZIP herunterladen"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Upload Dialog */}
|
||||
<Dialog open={uploadOpen} onOpenChange={(open) => { if (!open) handleUploadClose(); else setUploadOpen(true); }}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -301,6 +301,29 @@ export async function getPop3Progress(id: number): Promise<Pop3Account> {
|
||||
return request<Pop3Account>(`/api/pop3/${id}/progress`);
|
||||
}
|
||||
|
||||
// ── eDiscovery Export ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function exportEDiscovery(params: {
|
||||
case_name?: string;
|
||||
q?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
has_attachment?: boolean;
|
||||
}): Promise<{ blob: Blob; filename: string }> {
|
||||
const res = await fetch(`${API_BASE}/api/export/ediscovery`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (!res.ok) throw new Error(`eDiscovery Export fehlgeschlagen: ${res.status}`);
|
||||
const cd = res.headers.get("Content-Disposition") || "";
|
||||
const filename = cd.match(/filename="([^"]+)"/)?.[1] || "archivmail-export.zip";
|
||||
return { blob: await res.blob(), filename };
|
||||
}
|
||||
|
||||
// ── Thread ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getThread(threadID: string): Promise<ThreadResponse> {
|
||||
|
||||
Reference in New Issue
Block a user