"use client"; 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 { Navbar } from "@/components/navbar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Progress } from "@/components/ui/progress"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Skeleton } from "@/components/ui/skeleton"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; const PAGE_SIZE = 25; function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } export default function SearchPage() { const { user, loading: authLoading } = useAuth(); const router = useRouter(); const [query, setQuery] = useState(""); const [fromFilter, setFromFilter] = useState(""); const [toFilter, setToFilter] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [sort, setSort] = useState("date_desc"); const [hasAttachment, setHasAttachment] = useState(undefined); const [results, setResults] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [searching, setSearching] = useState(false); const [searched, setSearched] = useState(false); // Selection state const [selected, setSelected] = useState>(new Set()); const [exportOpen, setExportOpen] = useState(false); const [exportAttachments, setExportAttachments] = useState(false); const [exporting, setExporting] = useState(false); // Upload state const [uploadOpen, setUploadOpen] = useState(false); const [uploadDragging, setUploadDragging] = useState(false); const [uploadJob, setUploadJob] = useState(null); const [uploadError, setUploadError] = useState(""); const [uploadLoading, setUploadLoading] = useState(false); const uploadPollRef = useRef | null>(null); // Clear selection when results change useEffect(() => { setSelected(new Set()); }, [results]); const doSearch = useCallback( async (p: number) => { setSearching(true); try { const res = await searchEmails({ q: query || undefined, from: fromFilter || undefined, to: toFilter || undefined, date_from: dateFrom || undefined, date_to: dateTo || undefined, sort: sort !== "date_desc" ? sort : undefined, has_attachment: hasAttachment, page: p, page_size: PAGE_SIZE, }); setResults(res.hits || []); setTotal(res.total); setPage(p); setSearched(true); } catch { setResults([]); setTotal(0); } finally { setSearching(false); } }, [query, fromFilter, toFilter, dateFrom, dateTo, sort, hasAttachment] ); // Alle Mails beim Öffnen der Seite laden — direkt, ohne useCallback-Closure useEffect(() => { if (!user) return; setSearching(true); searchEmails({ page: 1, page_size: PAGE_SIZE }) .then((res) => { setResults(res.hits || []); setTotal(res.total); setPage(1); setSearched(true); }) .catch(() => { setResults([]); setTotal(0); }) .finally(() => setSearching(false)); }, [user]); function handleSubmit(e: React.FormEvent) { e.preventDefault(); doSearch(1); } async function handleExportZIP() { setExporting(true); try { const { blob } = await exportMailsZIP(Array.from(selected), exportAttachments); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "archivmail-export.zip"; a.click(); URL.revokeObjectURL(url); setExportOpen(false); setSelected(new Set()); } catch (e) { alert(`Export fehlgeschlagen: ${e instanceof Error ? e.message : e}`); } finally { setExporting(false); } } async function handleUploadFiles(files: FileList | File[]) { const allowed = Array.from(files).filter((f) => { const n = f.name.toLowerCase(); return n.endsWith(".eml") || n.endsWith(".mbox"); }); if (allowed.length === 0) { setUploadError("Nur .eml und .mbox Dateien erlaubt."); return; } setUploadError(""); setUploadJob(null); setUploadLoading(true); try { const { job_id } = await uploadMailFilesUser(allowed); uploadPollRef.current = setInterval(async () => { try { const job = await getUploadProgressUser(job_id); setUploadJob(job); if (job.status !== "running") { clearInterval(uploadPollRef.current!); uploadPollRef.current = null; setUploadLoading(false); // Refresh search results after successful import if (job.status === "done") doSearch(1); } } catch { clearInterval(uploadPollRef.current!); uploadPollRef.current = null; setUploadLoading(false); } }, 1500); } catch (e) { setUploadError(e instanceof Error ? e.message : "Upload fehlgeschlagen."); setUploadLoading(false); } } function handleUploadClose() { if (uploadPollRef.current) { clearInterval(uploadPollRef.current); uploadPollRef.current = null; } setUploadOpen(false); setUploadJob(null); setUploadError(""); setUploadLoading(false); } const totalPages = Math.ceil(total / PAGE_SIZE); const allSelected = results.length > 0 && results.every((h) => selected.has(h.id)); return (
{(authLoading || !user) && (
)} {!authLoading && user && (<>
setQuery(e.target.value)} className="flex-1" aria-label="Suchbegriff" />
setFromFilter(e.target.value)} aria-label="Absender filtern" />
setToFilter(e.target.value)} aria-label="Empfänger filtern" />
setDateFrom(e.target.value)} aria-label="Datum von" />
setDateTo(e.target.value)} aria-label="Datum bis" />
setHasAttachment(checked ? true : undefined) } />
{searching ? ( {Array.from({ length: 5 }).map((_, i) => ( ))} ) : searched && results.length === 0 ? ( Keine E-Mails gefunden. ) : results.length > 0 ? ( <>
{query || fromFilter || toFilter || dateFrom || dateTo ? `${total} Ergebnis${total !== 1 ? "se" : ""} gefunden` : `${total} E-Mail${total !== 1 ? "s" : ""} im Archiv`}
{selected.size > 0 && (
{selected.size} ausgewählt
)} { if (checked) setSelected(new Set(results.map((h) => h.id))); else setSelected(new Set()); }} aria-label="Alle auswählen" /> Datum Von Betreff An 📎 Größe {results.map((hit) => ( router.push(`/mail/${hit.id}`)} role="link" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") router.push(`/mail/${hit.id}`); }} aria-label={`E-Mail von ${hit.from || "unbekannt"}: ${hit.subject || "Kein Betreff"}`} > e.stopPropagation()}> { setSelected((prev) => { const next = new Set(prev); if (checked) next.add(hit.id); else next.delete(hit.id); return next; }); }} aria-label="Mail auswählen" /> {hit.date ? new Date(hit.date).toLocaleDateString("de-DE") : "-"} {hit.from || "-"} {hit.subject || "(kein Betreff)"} {hit.to || "-"} {hit.has_attachments ? "📎" : ""} {hit.size ? formatBytes(hit.size) : ""} ))}
{totalPages > 1 && (
Seite {page} von {totalPages}
)} ) : null}
E-Mails exportieren {selected.size} E-Mail{selected.size !== 1 ? "s" : ""} als ZIP herunterladen
{/* Upload Dialog */} { if (!open) handleUploadClose(); else setUploadOpen(true); }}> E-Mails importieren EML- oder MBOX-Dateien in das Archiv hochladen {!uploadJob && (
{ e.preventDefault(); setUploadDragging(true); }} onDragLeave={() => setUploadDragging(false)} onDrop={(e) => { e.preventDefault(); setUploadDragging(false); if (e.dataTransfer.files.length > 0) handleUploadFiles(e.dataTransfer.files); }} >

Dateien hierher ziehen oder auswählen

.eml · .mbox

)} {uploadError && (

{uploadError}

)} {uploadJob && (
0 ? Math.round(((uploadJob.imported + uploadJob.skipped + uploadJob.errors) / uploadJob.total) * 100) : 0} />
{uploadJob.imported}
Importiert
{uploadJob.skipped}
Duplikate
{uploadJob.errors}
Fehler
{uploadJob.status === "running" && (

Verarbeite {uploadJob.imported + uploadJob.skipped + uploadJob.errors} / {uploadJob.total} …

)} {uploadJob.status === "done" && (

Abgeschlossen

)}
)}
)}
); }