/** * Table-based job list view. */ import React from "react"; import { ArrowDown, ArrowUp, ArrowUpDown, CheckCircle2, Copy, Download, ExternalLink, MoreHorizontal, RefreshCcw, XCircle, } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { cn } from "@/lib/utils"; import { copyTextToClipboard, formatJobForLlmContext } from "@client/lib/jobCopy"; import type { Job } from "../../shared/types"; import { StatusBadge } from "./StatusBadge"; export type JobSortKey = | "title" | "employer" | "source" | "location" | "status" | "score" | "discoveredAt"; export type JobSortDirection = "asc" | "desc"; export interface JobSort { key: JobSortKey; direction: JobSortDirection; } export interface JobTableProps { jobs: Job[]; sort: JobSort; onSortChange: (sort: JobSort) => void; selectedJobIds: Set; onSelectedJobIdsChange: (ids: Set) => void; onApply: (id: string) => void | Promise; onReject: (id: string) => void | Promise; onProcess: (id: string) => void | Promise; processingJobId: string | null; highlightedJobId?: string | null; onHighlightChange?: (jobId: string | null) => void; } const sourceLabel: Record = { gradcracker: "Gradcracker", indeed: "Indeed", linkedin: "LinkedIn", }; const defaultSortDirection: Record = { title: "asc", employer: "asc", source: "asc", location: "asc", status: "asc", score: "desc", discoveredAt: "desc", }; const formatDate = (dateStr: string | null) => { if (!dateStr) return null; try { return new Date(dateStr).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", }); } catch { return dateStr; } }; const safeFilenamePart = (value: string) => value.replace(/[^a-z0-9]/gi, "_"); const SortButton: React.FC<{ label: string; sortKey: JobSortKey; sort: JobSort; onSortChange: (sort: JobSort) => void; className?: string; }> = ({ label, sortKey, sort, onSortChange, className }) => { const isActive = sort.key === sortKey; const Icon = isActive ? (sort.direction === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown; return ( ); }; export const JobTable: React.FC = ({ jobs, sort, onSortChange, selectedJobIds, onSelectedJobIdsChange, onApply, onReject, onProcess, processingJobId, highlightedJobId, onHighlightChange, }) => { const selectedCount = jobs.reduce((count, job) => count + (selectedJobIds.has(job.id) ? 1 : 0), 0); const allSelected = jobs.length > 0 && selectedCount === jobs.length; const someSelected = selectedCount > 0 && selectedCount < jobs.length; const handleCopyInfo = async (job: Job) => { try { await copyTextToClipboard(formatJobForLlmContext(job)); toast.success("Copied job info", { description: "LLM-ready context copied to clipboard." }); } catch { toast.error("Could not copy job info"); } }; return ( { const next = new Set(selectedJobIds); if (checked) { for (const job of jobs) next.add(job.id); } else { for (const job of jobs) next.delete(job.id); } onSelectedJobIdsChange(next); }} /> Actions {jobs.map((job) => { const jobLink = job.applicationLink || job.jobUrl; const hasPdf = !!job.pdfPath; const pdfHref = `/pdfs/resume_${job.id}.pdf`; const canApply = job.status === "ready"; const canProcess = ["discovered", "ready"].includes(job.status); const canReject = ["discovered", "ready"].includes(job.status); const isProcessing = processingJobId === job.id; const isSelected = selectedJobIds.has(job.id); const isHighlighted = highlightedJobId === job.id; return ( { const next = new Set(selectedJobIds); if (checked) next.add(job.id); else next.delete(job.id); onSelectedJobIdsChange(next); }} /> {job.employer} {sourceLabel[job.source]} {job.location || "—"} {job.suitabilityScore ?? "—"} {formatDate(job.discoveredAt)} View Job void handleCopyInfo(job)}> Copy info {onHighlightChange && ( onHighlightChange(isHighlighted ? null : job.id)} > {isHighlighted ? "Unhighlight" : "Highlight"} )} {hasPdf && ( <> View PDF Download PDF )} {(canProcess || canReject || canApply) && } {canProcess && ( onProcess(job.id)} disabled={isProcessing} > {isProcessing ? "Processing..." : job.status === "ready" ? "Regenerate PDF" : "Generate Resume"} )} {canReject && ( onReject(job.id)} > Skip )} {canApply && ( onApply(job.id)} > Mark Applied )} ); })}
); };