import { prisma } from "@/lib/db"; import Link from "next/link"; export const dynamic = "force-dynamic"; const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"] as const; const STAGE_CONFIG: Record< string, { label: string; description: string; color: string; badge: string; border: string; bgActive: string } > = { prospect: { label: "Prospect", description: "Saved roles", color: "bg-zinc-400 dark:bg-zinc-500", badge: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300", border: "border-zinc-200/90 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600", bgActive: "bg-zinc-50/90 dark:bg-zinc-800/50", }, reviewing: { label: "Reviewing", description: "Evaluating fit", color: "bg-amber-500", badge: "bg-amber-50 text-amber-700 dark:bg-amber-950/70 dark:text-amber-300 border border-amber-200/60 dark:border-amber-900/60", border: "border-amber-200/80 dark:border-amber-900/50 hover:border-amber-400 dark:hover:border-amber-600", bgActive: "bg-amber-50/60 dark:bg-amber-950/30", }, tailoring: { label: "Tailoring", description: "Customizing CV", color: "bg-purple-500", badge: "bg-purple-50 text-purple-700 dark:bg-purple-950/70 dark:text-purple-300 border border-purple-200/60 dark:border-purple-900/60", border: "border-purple-200/80 dark:border-purple-900/50 hover:border-purple-400 dark:hover:border-purple-600", bgActive: "bg-purple-50/60 dark:bg-purple-950/30", }, ready: { label: "Ready", description: "Ready to submit", color: "bg-blue-500", badge: "bg-blue-50 text-blue-700 dark:bg-blue-950/70 dark:text-blue-300 border border-blue-200/60 dark:border-blue-900/60", border: "border-blue-200/80 dark:border-blue-900/50 hover:border-blue-400 dark:hover:border-blue-600", bgActive: "bg-blue-50/60 dark:bg-blue-950/30", }, applied: { label: "Applied", description: "Submitted", color: "bg-indigo-500", badge: "bg-indigo-50 text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-900/60", border: "border-indigo-200/80 dark:border-indigo-900/50 hover:border-indigo-400 dark:hover:border-indigo-600", bgActive: "bg-indigo-50/60 dark:bg-indigo-950/30", }, interviewing: { label: "Interviewing", description: "In discussions", color: "bg-teal-500", badge: "bg-teal-50 text-teal-700 dark:bg-teal-950/70 dark:text-teal-300 border border-teal-200/60 dark:border-teal-900/60", border: "border-teal-200/80 dark:border-teal-900/50 hover:border-teal-400 dark:hover:border-teal-600", bgActive: "bg-teal-50/60 dark:bg-teal-950/30", }, offer: { label: "Offer", description: "Offer received", color: "bg-emerald-500", badge: "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/70 dark:text-emerald-300 border border-emerald-200/60 dark:border-emerald-900/60", border: "border-emerald-200/80 dark:border-emerald-900/50 hover:border-emerald-400 dark:hover:border-emerald-600", bgActive: "bg-emerald-50/60 dark:bg-emerald-950/30", }, rejected: { label: "Rejected", description: "Not selected", color: "bg-rose-500", badge: "bg-rose-50 text-rose-700 dark:bg-rose-950/70 dark:text-rose-300 border border-rose-200/60 dark:border-rose-900/60", border: "border-rose-200/80 dark:border-rose-900/50 hover:border-rose-400 dark:hover:border-rose-600", bgActive: "bg-rose-50/60 dark:bg-rose-950/30", }, dead: { label: "Dead", description: "Inactive / Stale", color: "bg-zinc-400", badge: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 border border-zinc-200/60 dark:border-zinc-700/60", border: "border-zinc-200/80 dark:border-zinc-800 hover:border-zinc-400 dark:hover:border-zinc-600", bgActive: "bg-zinc-50/70 dark:bg-zinc-800/40", }, }; function getCompanyColor(name: string) { const colors = [ "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950/80 dark:text-indigo-300 dark:border-indigo-800", "bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-950/80 dark:text-violet-300 dark:border-violet-800", "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950/80 dark:text-blue-300 dark:border-blue-800", "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-950/80 dark:text-emerald-300 dark:border-emerald-800", "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-950/80 dark:text-amber-300 dark:border-amber-800", "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-950/80 dark:text-rose-300 dark:border-rose-800", "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-950/80 dark:text-teal-300 dark:border-teal-800", "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-950/80 dark:text-cyan-300 dark:border-cyan-800", ]; let hash = 0; for (let i = 0; i < (name || "").length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } function getInitials(name: string) { if (!name) return "?"; const parts = name.trim().split(/\s+/); if (parts.length >= 2) { return (parts[0][0] + parts[1][0]).toUpperCase(); } return name.slice(0, 2).toUpperCase(); } export default async function Dashboard() { const jobs = await prisma.job.findMany({ orderBy: { updatedAt: "desc" }, take: 6, }); const totalJobsCount = await prisma.job.count(); const allJobs = await prisma.job.findMany({ select: { status: true, phEligibility: true } }); const apps = await prisma.application.count(); const resumes = await prisma.resume.count(); const byStatus = Object.fromEntries(STATUSES.map((s) => [s, allJobs.filter((j) => j.status === s).length])); const phYes = allJobs.filter((j) => j.phEligibility === "yes").length; const activeCount = (byStatus.reviewing ?? 0) + (byStatus.tailoring ?? 0) + (byStatus.ready ?? 0) + (byStatus.applied ?? 0) + (byStatus.interviewing ?? 0) + (byStatus.offer ?? 0); const activePercent = totalJobsCount > 0 ? Math.round((activeCount / totalJobsCount) * 100) : 0; const statCards = [ { label: "Total Jobs Tracked", value: totalJobsCount, sub: "Pipeline opportunities", gradient: "from-indigo-500/[0.08] via-indigo-500/[0.02] to-transparent dark:from-indigo-500/[0.14] dark:to-transparent", iconBorder: "border-indigo-200/80 dark:border-indigo-800/60 bg-indigo-50/80 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-400", icon: ( ), }, { label: "PH-Eligible Roles", value: phYes, sub: `${totalJobsCount > 0 ? Math.round((phYes / totalJobsCount) * 100) : 0}% of all tracked positions`, gradient: "from-emerald-500/[0.08] via-emerald-500/[0.02] to-transparent dark:from-emerald-500/[0.14] dark:to-transparent", iconBorder: "border-emerald-200/80 dark:border-emerald-800/60 bg-emerald-50/80 dark:bg-emerald-950/60 text-emerald-600 dark:text-emerald-400", icon: ( ), }, { label: "Tailored Resumes", value: resumes, sub: "AI-customized CV documents", gradient: "from-purple-500/[0.08] via-purple-500/[0.02] to-transparent dark:from-purple-500/[0.14] dark:to-transparent", iconBorder: "border-purple-200/80 dark:border-purple-800/60 bg-purple-50/80 dark:bg-purple-950/60 text-purple-600 dark:text-purple-400", icon: ( ), }, { label: "Applications Sent", value: apps, sub: "Logged submissions", gradient: "from-blue-500/[0.08] via-blue-500/[0.02] to-transparent dark:from-blue-500/[0.14] dark:to-transparent", iconBorder: "border-blue-200/80 dark:border-blue-800/60 bg-blue-50/80 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400", icon: ( ), }, ]; return (
Monitor active hiring stages, manage candidate positioning, and tailor resumes with AI across your job search pipeline.
{card.sub}
Horizontal funnel flow across active stages
AI-powered parser
Paste a listing URL or raw job description. JobFlow AI extracts titles, compensation, PH eligibility, and requirements automatically.
+ Add New PositionLatest pipeline updates
No jobs tracked yet.
Add your first job →