- Rename LeadVault → Leadspeicher throughout (sidebar, topbar, page) - SidePanel: full lead detail view with contact, source, tags (read-only), Google Maps link for address - Tags: kontaktiert stored as tag (toggleable), favorit tag toggle - Remove Status column, StatusBadge dropdown, Priority feature - Remove Aktualisieren button from Leadspeicher - Bulk actions: remove status dropdown - Export: LeadVault Excel-only, clean columns, freeze row + autofilter - Export dropdown: click-based (fix overflow-hidden clipping) - ExportButtons: remove CSV, Excel only everywhere - Maps page: post-search Anymailfinder enrichment button - ProgressCard: "Suche läuft..." instead of "Warte auf Anymailfinder-Server..." - Quick SERP renamed to "Schnell neue Suche" - Results page: Excel export, always-enabled download button - Anymailfinder: fix bulk field names, array-of-arrays format - Apify: fix countryCode lowercase - API: sourceTerm search, contacted/favorite tag filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
202 lines
8.5 KiB
TypeScript
202 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { EmptyState } from "@/components/shared/EmptyState";
|
|
import { StatusBadge } from "@/components/shared/ProgressCard";
|
|
import { toast } from "sonner";
|
|
import { BarChart3, Building2, Linkedin, Search, MapPin, Download, Trash2, RefreshCw } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Job {
|
|
id: string;
|
|
type: string;
|
|
status: string;
|
|
totalLeads: number;
|
|
emailsFound: number;
|
|
createdAt: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface Stats {
|
|
totalLeads: number;
|
|
totalEmails: number;
|
|
avgHitRate: number;
|
|
totalJobs: number;
|
|
}
|
|
|
|
const TYPE_CONFIG: Record<string, { icon: typeof Building2; label: string; color: string }> = {
|
|
airscale: { icon: Building2, label: "AirScale", color: "text-blue-400" },
|
|
linkedin: { icon: Linkedin, label: "LinkedIn", color: "text-blue-500" },
|
|
"linkedin-enrich": { icon: Linkedin, label: "LinkedIn Enrich", color: "text-blue-400" },
|
|
serp: { icon: Search, label: "SERP", color: "text-purple-400" },
|
|
maps: { icon: MapPin, label: "Google Maps", color: "text-green-400" },
|
|
};
|
|
|
|
export default function ResultsPage() {
|
|
const [jobs, setJobs] = useState<Job[]>([]);
|
|
const [stats, setStats] = useState<Stats>({ totalLeads: 0, totalEmails: 0, avgHitRate: 0, totalJobs: 0 });
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadJobs();
|
|
}, []);
|
|
|
|
const loadJobs = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/jobs");
|
|
const data = await res.json() as { jobs: Job[]; stats: Stats };
|
|
setJobs(data.jobs || []);
|
|
setStats(data.stats || { totalLeads: 0, totalEmails: 0, avgHitRate: 0, totalJobs: 0 });
|
|
} catch {
|
|
toast.error("Job-Verlauf konnte nicht geladen werden");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const deleteJob = async (id: string) => {
|
|
try {
|
|
await fetch(`/api/jobs/${id}`, { method: "DELETE" });
|
|
setJobs(prev => prev.filter(j => j.id !== id));
|
|
toast.success("Job gelöscht");
|
|
} catch {
|
|
toast.error("Job konnte nicht gelöscht werden");
|
|
}
|
|
};
|
|
|
|
const downloadJob = (id: string) => {
|
|
window.open(`/api/export/${id}`, "_blank");
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-6xl">
|
|
{/* Header */}
|
|
<div className="relative rounded-2xl bg-gradient-to-r from-green-600/10 to-blue-600/10 border border-[#1e1e2e] p-6 overflow-hidden">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 to-transparent" />
|
|
<div className="relative">
|
|
<h1 className="text-2xl font-bold text-white">Ergebnisse & Verlauf</h1>
|
|
<p className="text-gray-400 mt-1 text-sm">Alle vergangenen Anreicherungsjobs und ihre Ergebnisse.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats cards */}
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{[
|
|
{ label: "Jobs gesamt", value: stats.totalJobs, color: "text-white" },
|
|
{ label: "Leads gesamt", value: stats.totalLeads.toLocaleString(), color: "text-blue-400" },
|
|
{ label: "E-Mails gefunden", value: stats.totalEmails.toLocaleString(), color: "text-green-400" },
|
|
{ label: "Ø Trefferquote", value: `${stats.avgHitRate}%`, color: "text-purple-400" },
|
|
].map(stat => (
|
|
<Card key={stat.label} className="bg-[#111118] border-[#1e1e2e] p-5">
|
|
<p className={`text-2xl font-bold ${stat.color}`}>{stat.value}</p>
|
|
<p className="text-xs text-gray-500 mt-1">{stat.label}</p>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Jobs table */}
|
|
<Card className="bg-[#111118] border-[#1e1e2e] overflow-hidden">
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[#1e1e2e]">
|
|
<h2 className="font-semibold text-white">Job-Verlauf</h2>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={loadJobs}
|
|
className="border-[#2e2e3e] text-gray-300 hover:bg-[#1a1a28]"
|
|
>
|
|
<RefreshCw className="w-3.5 h-3.5 mr-1.5" /> Aktualisieren
|
|
</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="p-6 space-y-3">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<Skeleton key={i} className="h-12 w-full bg-[#1e1e2e]" />
|
|
))}
|
|
</div>
|
|
) : jobs.length === 0 ? (
|
|
<EmptyState
|
|
icon={BarChart3}
|
|
title="Noch keine Jobs"
|
|
description="Starte einen Anreicherungsjob über einen der Pipeline-Tabs, um Ergebnisse zu sehen."
|
|
/>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[#1e1e2e] bg-[#0d0d18]">
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Typ</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Job-ID</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Gestartet</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Status</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Leads</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">E-Mails</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{jobs.map((job, idx) => {
|
|
const cfg = TYPE_CONFIG[job.type] || { icon: BarChart3, label: job.type, color: "text-gray-400" };
|
|
const Icon = cfg.icon;
|
|
return (
|
|
<tr
|
|
key={job.id}
|
|
className={cn(
|
|
"border-b border-[#1e1e2e]/50",
|
|
idx % 2 === 0 ? "bg-[#111118]" : "bg-[#0f0f1a]"
|
|
)}
|
|
>
|
|
<td className="px-4 py-3">
|
|
<div className={`flex items-center gap-2 ${cfg.color}`}>
|
|
<Icon className="w-4 h-4" />
|
|
<span className="text-xs font-medium">{cfg.label}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-xs text-gray-500">{job.id.slice(0, 12)}...</td>
|
|
<td className="px-4 py-3 text-xs text-gray-400">
|
|
{new Date(job.createdAt).toLocaleDateString("de-DE", {
|
|
day: "2-digit", month: "2-digit", year: "numeric",
|
|
hour: "2-digit", minute: "2-digit",
|
|
})}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<StatusBadge status={job.status === "complete" ? "complete" : job.status} />
|
|
</td>
|
|
<td className="px-4 py-3 text-white font-medium">{job.totalLeads.toLocaleString()}</td>
|
|
<td className="px-4 py-3 text-green-400 font-medium">{job.emailsFound.toLocaleString()}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-1.5">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => downloadJob(job.id)}
|
|
className="h-7 px-2 text-gray-400 hover:text-white hover:bg-[#1a1a28]"
|
|
>
|
|
<Download className="w-3.5 h-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => deleteJob(job.id)}
|
|
className="h-7 px-2 text-gray-400 hover:text-red-400 hover:bg-red-500/5"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|