Files
archivmail/src/app/search/page.tsx
T
sysops a55faf74b1 chore: Frontend-Seiten in Komponenten/Hooks aufteilen
admin/page.tsx (1019→446), search/page.tsx (871→304), imap/page.tsx
(738→142), settings/page.tsx (641→56). Reine Umstrukturierung,
keine Verhaltensänderung, Build verifiziert.
2026-06-22 11:29:43 +02:00

293 lines
9.1 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import { useSearch, DEFAULT_PAGE_SIZE } from "@/hooks/useSearch";
import { useSavedSearches } from "@/hooks/useSavedSearches";
import { useMailUpload } from "@/hooks/useMailUpload";
import { exportMailsZIP, exportEDiscovery } from "@/lib/api";
import { Navbar } from "@/components/navbar";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SearchFilterBar } from "@/components/search/SearchFilterBar";
import { SearchResultsTable } from "@/components/search/SearchResultsTable";
import {
ExportZipDialog,
EDiscoveryDialog,
UploadDialog,
} from "@/components/search/ExportDialogs";
export default function SearchPage() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
const pageSize = user?.list_page_size ?? DEFAULT_PAGE_SIZE;
const search = useSearch(user, pageSize);
const {
query,
setQuery,
fromFilter,
setFromFilter,
toFilter,
setToFilter,
dateFrom,
setDateFrom,
dateTo,
setDateTo,
sort,
setSort,
hasAttachment,
setHasAttachment,
results,
setResults,
total,
setTotal,
page,
setPage,
searching,
setSearching,
searched,
setSearched,
doSearch,
} = search;
// Selection state
const [selected, setSelected] = useState<Set<string>>(new Set());
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);
const upload = useMailUpload(() => doSearch(1));
const saved = useSavedSearches({
user,
pageSize,
query,
fromFilter,
toFilter,
dateFrom,
dateTo,
hasAttachment,
setQuery,
setFromFilter,
setToFilter,
setDateFrom,
setDateTo,
setHasAttachment,
setResults,
setTotal,
setPage,
setSearching,
setSearched,
});
// Clear selection when results change
useEffect(() => {
setSelected(new Set());
}, [results]);
// Superadmin has no mail access — redirect to admin dashboard
useEffect(() => {
if (user?.role === "superadmin") {
router.replace("/admin");
}
}, [user, router]);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
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 {
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);
}
}
const hasActiveSearch = !!(query || fromFilter || toFilter || dateFrom || dateTo || hasAttachment);
const totalPages = Math.ceil(total / pageSize);
return (
<div className="min-h-screen">
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
<main className="mx-auto max-w-7xl px-4 py-6">
{(authLoading || !user) && (
<div className="space-y-4">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-64 w-full" />
</div>
)}
{!authLoading && user && (<>
<div className="flex gap-6">
{/* Main content */}
<div className="flex-1 min-w-0">
<SearchFilterBar
query={query}
setQuery={setQuery}
fromFilter={fromFilter}
setFromFilter={setFromFilter}
toFilter={toFilter}
setToFilter={setToFilter}
dateFrom={dateFrom}
setDateFrom={setDateFrom}
dateTo={dateTo}
setDateTo={setDateTo}
sort={sort}
setSort={setSort}
hasAttachment={hasAttachment}
setHasAttachment={setHasAttachment}
searching={searching}
onSubmit={handleSubmit}
onOpenUpload={() => upload.setUploadOpen(true)}
hasActiveSearch={hasActiveSearch}
savePopoverOpen={saved.savePopoverOpen}
setSavePopoverOpen={saved.setSavePopoverOpen}
saveName={saved.saveName}
setSaveName={saved.setSaveName}
saving={saved.saving}
onSaveSearch={saved.handleSaveSearch}
savedListOpen={saved.savedListOpen}
setSavedListOpen={saved.setSavedListOpen}
savedLoading={saved.savedLoading}
savedSearches={saved.savedSearches}
onApplySavedSearch={saved.handleApplySavedSearch}
onDeleteSavedSearch={saved.handleDeleteSavedSearch}
/>
<div className="mt-6">
{searching ? (
<Card>
<CardContent className="p-4 space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
) : searched && results.length === 0 ? (
<Card>
<CardContent className="p-8 text-center text-muted-foreground">
Keine E-Mails gefunden.
</CardContent>
</Card>
) : results.length > 0 ? (
<>
<div className="mb-2 text-sm text-muted-foreground">
{query || fromFilter || toFilter || dateFrom || dateTo
? `${total} Ergebnis${total !== 1 ? "se" : ""} gefunden`
: `${total} E-Mail${total !== 1 ? "s" : ""} im Archiv`}
</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>
<SearchResultsTable
results={results}
selected={selected}
setSelected={setSelected}
total={total}
page={page}
totalPages={totalPages}
onPageChange={(p) => doSearch(p)}
/>
</>
) : null}
</div>
</div>{/* end flex-1 */}
</div>{/* end flex gap-6 */}
<ExportZipDialog
open={exportOpen}
onOpenChange={setExportOpen}
selectedCount={selected.size}
exportAttachments={exportAttachments}
setExportAttachments={setExportAttachments}
exporting={exporting}
onExport={handleExportZIP}
/>
<EDiscoveryDialog
open={ediscoveryOpen}
onOpenChange={setEdiscoveryOpen}
caseName={ediscoveryCaseName}
setCaseName={setEdiscoveryCaseName}
loading={ediscoveryLoading}
onExport={handleEDiscoveryExport}
query={query}
fromFilter={fromFilter}
toFilter={toFilter}
dateFrom={dateFrom}
dateTo={dateTo}
/>
<UploadDialog
open={upload.uploadOpen}
onOpenChange={(open) => { if (!open) upload.handleUploadClose(); else upload.setUploadOpen(true); }}
dragging={upload.uploadDragging}
setDragging={upload.setUploadDragging}
job={upload.uploadJob}
error={upload.uploadError}
loading={upload.uploadLoading}
onUploadFiles={upload.handleUploadFiles}
onClose={upload.handleUploadClose}
/>
</>)}
</main>
</div>
);
}