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.
293 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|