/** * ReadyPanel - Optimized "shipping lane" view for Ready jobs. * * Designed for a single, fast, repeatable workflow: verify → download → apply → mark applied. * The PDF is the primary artifact, represented abstractly through an Application Kit summary. * * Now includes inline tailoring mode for editing and regenerating PDFs without switching tabs. */ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { CheckCircle2, ChevronUp, Download, ExternalLink, FileText, Loader2, MoreHorizontal, RefreshCcw, Undo2, Copy, Edit2, XCircle, Briefcase, Building2, FolderKanban, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { cn, copyTextToClipboard, formatJobForWebhook } from "@/lib/utils"; import * as api from "../api"; import { FitAssessment, JobHeader, TailoredSummary } from "."; import { TailorMode } from "./discovered-panel/TailorMode"; import { useProfile } from "../hooks/useProfile"; import type { Job, ResumeProjectCatalogItem } from "../../shared/types"; type PanelMode = "ready" | "tailor"; interface ReadyPanelProps { job: Job | null; onJobUpdated: () => void | Promise; onJobMoved: (jobId: string) => void; } const safeFilenamePart = (value: string | null | undefined) => (value || "Unknown").replace(/[^\w\s-]/g, "").replace(/\s+/g, "_"); export const ReadyPanel: React.FC = ({ job, onJobUpdated, onJobMoved, }) => { const [mode, setMode] = useState("ready"); const [isMarkingApplied, setIsMarkingApplied] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); const [catalog, setCatalog] = useState([]); const [recentlyApplied, setRecentlyApplied] = useState<{ jobId: string; jobTitle: string; employer: string; timeoutId: ReturnType; } | null>(null); const { personName } = useProfile(); // Load project catalog once useEffect(() => { api.getProfileProjects().then(setCatalog).catch(console.error); }, []); // Reset mode when job changes useEffect(() => { setMode("ready"); }, [job?.id]); // Compute derived values const pdfHref = job ? `/pdfs/resume_${job.id}.pdf?v=${encodeURIComponent(job.updatedAt)}` : "#"; const jobLink = job ? job.applicationLink || job.jobUrl : "#"; const selectedProjectIds = useMemo(() => { return job?.selectedProjectIds?.split(",").filter(Boolean) ?? []; }, [job?.selectedProjectIds]); const selectedProjectNames = useMemo(() => { if (!catalog.length || !selectedProjectIds.length) return []; return selectedProjectIds .map(id => catalog.find(p => p.id === id)?.name) .filter(Boolean) as string[]; }, [catalog, selectedProjectIds]); // Handle mark as applied with undo capability const handleMarkApplied = useCallback(async () => { if (!job) return; try { setIsMarkingApplied(true); await api.markAsApplied(job.id); // Store for undo const timeoutId = setTimeout(() => { setRecentlyApplied(null); }, 8000); setRecentlyApplied({ jobId: job.id, jobTitle: job.title, employer: job.employer, timeoutId, }); // Notify parent to move to next job onJobMoved(job.id); await onJobUpdated(); toast.success("Marked as applied", { description: `${job.title} at ${job.employer}`, action: { label: "Undo", onClick: () => handleUndoApplied(job.id), }, duration: 6000, }); } catch (error) { const message = error instanceof Error ? error.message : "Failed to mark as applied"; toast.error(message); } finally { setIsMarkingApplied(false); } }, [job, onJobMoved, onJobUpdated]); const handleUndoApplied = useCallback( async (jobId: string) => { try { // Revert to ready status await api.updateJob(jobId, { status: "ready" }); toast.success("Reverted to Ready"); if (recentlyApplied?.timeoutId) { clearTimeout(recentlyApplied.timeoutId); } setRecentlyApplied(null); await onJobUpdated(); } catch (error) { const message = error instanceof Error ? error.message : "Failed to undo"; toast.error(message); } }, [onJobUpdated, recentlyApplied], ); const handleRegenerate = useCallback(async () => { if (!job) return; try { setIsRegenerating(true); await api.generateJobPdf(job.id); toast.success("PDF regenerated"); await onJobUpdated(); } catch (error) { const message = error instanceof Error ? error.message : "Failed to regenerate PDF"; toast.error(message); } finally { setIsRegenerating(false); } }, [job, onJobUpdated]); const handleSkip = useCallback(async () => { if (!job) return; try { await api.skipJob(job.id); toast.message("Job skipped"); onJobMoved(job.id); await onJobUpdated(); } catch (error) { const message = error instanceof Error ? error.message : "Failed to skip"; toast.error(message); } }, [job, onJobMoved, onJobUpdated]); const handleCopyInfo = useCallback(async () => { if (!job) return; try { await copyTextToClipboard(formatJobForWebhook(job)); toast.success("Copied job info", { description: "Webhook payload copied to clipboard.", }); } catch { toast.error("Could not copy job info"); } }, [job]); // Handler for regenerating PDF after tailoring edits const handleTailorFinalize = useCallback(async () => { if (!job) return; try { setIsRegenerating(true); await api.generateJobPdf(job.id); toast.success("PDF regenerated"); await onJobUpdated(); setMode("ready"); } catch (error) { const message = error instanceof Error ? error.message : "Failed to regenerate PDF"; toast.error(message); } finally { setIsRegenerating(false); } }, [job, onJobUpdated]); // Empty state if (!job) { return (
No job selected

Select a Ready job to view its application kit and take action.

); } // Tailor mode - reuse the same TailorMode component with 'ready' variant if (mode === "tailor") { return ( setMode("ready")} onFinalize={handleTailorFinalize} isFinalizing={isRegenerating} variant="ready" /> ); } return (
{ await api.checkSponsor(job.id); await onJobUpdated(); }} /> {/* ───────────────────────────────────────────────────────────────────── PRIMARY ACTION CLUSTER All actions in one line: View, Save, Open, and Mark Applied ───────────────────────────────────────────────────────────────────── */}
{/* Show PDF - to verify quickly without download */} {/* Download PDF - primary artifact action */} {/* Open job - to verify before applying */} {/* Primary CTA: Mark Applied */}
{/* ───────────────────────────────────────────────────────────────────── APPLICATION KIT SUMMARY Abstract representation of what the PDF contains - verify at a glance ───────────────────────────────────────────────────────────────────── */}
{/* Job identity - confirm this is the right role */}
{/* Project selection - expandable accordion */}
{selectedProjectIds.length} {selectedProjectIds.length === 1 ? "project" : "projects"} selected
    {selectedProjectNames.map((name, i) => (
  • {name}
  • ))} {selectedProjectNames.length === 0 && (
  • No projects selected
  • )}
{/* ───────────────────────────────────────────────────────────────────── SECONDARY ACTIONS Fix/More menu - all non-critical actions demoted here ───────────────────────────────────────────────────────────────────── */}
{/* Fix/Edit actions */} setMode("tailor")}> Edit tailoring {isRegenerating ? "Regenerating..." : "Regenerate PDF"} {/* Utility actions */} Copy job info {/* Destructive actions */} Skip this job
{/* ───────────────────────────────────────────────────────────────────── UNDO BAR (conditional) Lightweight undo option after marking applied ───────────────────────────────────────────────────────────────────── */} {recentlyApplied && (
{recentlyApplied.jobTitle} marked applied
)}
); };