/** * 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 type { Job, ResumeProjectCatalogItem } from "@shared/types.js"; import { CheckCircle2, ChevronUp, Copy, Download, Edit2, ExternalLink, FileText, FolderKanban, Loader2, RefreshCcw, Undo2, XCircle, } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { trackProductEvent } from "@/lib/analytics"; import { cn, copyTextToClipboard, formatJobForWebhook, safeFilenamePart, } from "@/lib/utils"; import * as api from "../api"; import { useMarkAsAppliedMutation, useSkipJobMutation, } from "../hooks/queries/useJobMutations"; import { useCoverLetterGeneration } from "../hooks/useCoverLetterGeneration"; import { useProfile } from "../hooks/useProfile"; import { useRescoreJob } from "../hooks/useRescoreJob"; import { FitAssessment, JobHeader, JobNotes, TailoredSummary } from "."; import { CoverLetterDisplay } from "./CoverLetterDisplay"; import { TailorMode } from "./discovered-panel/TailorMode"; import { GhostwriterDrawer } from "./ghostwriter/GhostwriterDrawer"; import { JobDetailsEditDrawer } from "./JobDetailsEditDrawer"; import { KbdHint } from "./KbdHint"; import { OpenJobListingButton } from "./OpenJobListingButton"; import { ReadySummaryAccordion } from "./ReadySummaryAccordion"; import { buildReadyPanelGoogleDorks } from "./ready-panel-google-dorks"; type PanelMode = "ready" | "tailor"; interface ReadyPanelProps { job: Job | null; onJobUpdated: () => void | Promise; onJobMoved: (jobId: string) => void; onTailoringDirtyChange?: (isDirty: boolean) => void; } export const ReadyPanel: React.FC = ({ job, onJobUpdated, onJobMoved, onTailoringDirtyChange, }) => { const [mode, setMode] = useState("ready"); const [isMarkingApplied, setIsMarkingApplied] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); const [isEditDetailsOpen, setIsEditDetailsOpen] = useState(false); const { isRescoring, rescoreJob } = useRescoreJob(onJobUpdated); const [catalog, setCatalog] = useState([]); const [recentlyApplied, setRecentlyApplied] = useState<{ jobId: string; jobTitle: string; employer: string; timeoutId: ReturnType; } | null>(null); const previousJobIdRef = useRef(null); const markAsAppliedMutation = useMarkAsAppliedMutation(); const skipJobMutation = useSkipJobMutation(); const { generateForJob, inFlight: coverLetterGenerating } = useCoverLetterGeneration(onJobUpdated); const { personName } = useProfile(); // Load project catalog once useEffect(() => { api.getResumeProjectsCatalog().then(setCatalog).catch(console.error); }, []); // Reset mode when job changes useEffect(() => { const currentJobId = job?.id ?? null; if (previousJobIdRef.current === currentJobId) return; previousJobIdRef.current = currentJobId; setMode("ready"); setIsEditDetailsOpen(false); onTailoringDirtyChange?.(false); }, [job?.id, onTailoringDirtyChange]); useEffect(() => { if (mode !== "tailor") { onTailoringDirtyChange?.(false); } }, [mode, onTailoringDirtyChange]); useEffect(() => { return () => onTailoringDirtyChange?.(false); }, [onTailoringDirtyChange]); // 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 googleDorks = useMemo( () => (job ? buildReadyPanelGoogleDorks(job) : []), [job], ); const handleUndoApplied = useCallback( async (jobId: string) => { try { // Revert to ready status await api.updateJob(jobId, { status: "ready" }); trackProductEvent("jobs_job_action_completed", { action: "move_to_ready", result: "success", from_status: "applied", to_status: "ready", }); toast.success("Reverted to Ready"); if (recentlyApplied?.timeoutId) { clearTimeout(recentlyApplied.timeoutId); } setRecentlyApplied(null); await onJobUpdated(); } catch (error) { trackProductEvent("jobs_job_action_completed", { action: "move_to_ready", result: "error", from_status: "applied", to_status: "ready", }); const message = error instanceof Error ? error.message : "Failed to undo"; toast.error(message); } }, [onJobUpdated, recentlyApplied], ); // Handle mark as applied with undo capability const handleMarkApplied = useCallback(async () => { if (!job) return; try { setIsMarkingApplied(true); await markAsAppliedMutation.mutateAsync(job.id); trackProductEvent("jobs_job_action_completed", { action: "mark_applied", result: "success", from_status: job.status, to_status: "applied", }); // 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) { trackProductEvent("jobs_job_action_completed", { action: "mark_applied", result: "error", from_status: job.status, to_status: "applied", }); const message = error instanceof Error ? error.message : "Failed to mark as applied"; toast.error(message); } finally { setIsMarkingApplied(false); } }, [job, markAsAppliedMutation, onJobMoved, onJobUpdated, handleUndoApplied]); const handleRegenerate = useCallback(async () => { if (!job) return; try { setIsRegenerating(true); await api.generateJobPdf(job.id); trackProductEvent("jobs_job_action_completed", { action: "generate_pdf", result: "success", from_status: job.status, }); toast.success("PDF regenerated"); await onJobUpdated(); } catch (error) { trackProductEvent("jobs_job_action_completed", { action: "generate_pdf", result: "error", from_status: job.status, }); const message = error instanceof Error ? error.message : "Failed to regenerate PDF"; toast.error(message); } finally { setIsRegenerating(false); } }, [job, onJobUpdated]); const handleRescore = useCallback( () => rescoreJob(job?.id), [job?.id, rescoreJob], ); const handleSkip = useCallback(async () => { if (!job) return; try { await skipJobMutation.mutateAsync(job.id); trackProductEvent("jobs_job_action_completed", { action: "skip", result: "success", from_status: job.status, to_status: "skipped", }); toast.message("Job skipped"); onJobMoved(job.id); await onJobUpdated(); } catch (error) { trackProductEvent("jobs_job_action_completed", { action: "skip", result: "error", from_status: job.status, to_status: "skipped", }); const message = error instanceof Error ? error.message : "Failed to skip"; toast.error(message); } }, [job, onJobMoved, onJobUpdated, skipJobMutation]); 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); trackProductEvent("jobs_job_action_completed", { action: "generate_pdf", result: "success", from_status: job.status, }); toast.success("PDF regenerated"); await onJobUpdated(); setMode("ready"); } catch (error) { trackProductEvent("jobs_job_action_completed", { action: "generate_pdf", result: "error", from_status: job.status, }); 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" onDirtyChange={onTailoringDirtyChange} /> ); } return (
{ try { await api.checkSponsor(job.id); trackProductEvent("jobs_job_action_completed", { action: "check_sponsor", result: "success", from_status: job.status, }); await onJobUpdated(); } catch (error) { trackProductEvent("jobs_job_action_completed", { action: "check_sponsor", result: "error", from_status: job.status, }); throw error; } }} /> {/* ───────────────────────────────────────────────────────────────────── PRIMARY ACTION CLUSTER All actions in one line: View, Save, Open, and Mark Applied ───────────────────────────────────────────────────────────────────── */}
{/* Download PDF - primary artifact action */} {/* Open job - to verify before applying */} {/* Primary CTA: Mark Applied */}
generateForJob(job.id)} generateInFlight={coverLetterGenerating} /> {googleDorks.length > 0 ? ( {googleDorks.length}{" "} {googleDorks.length === 1 ? "search link" : "search links"} } value="search-dorks" >
{googleDorks.map((dork) => ( {dork.label} ))}
) : null} {/* Project selection - expandable accordion */} {selectedProjectIds.length}{" "} {selectedProjectIds.length === 1 ? "project" : "projects"}{" "} selected } value="projects" >
    {selectedProjectIds.map((id) => { const name = catalog.find((p) => p.id === id)?.name; if (!name) return null; return
  • {name}
  • ; })} {selectedProjectIds.length === 0 && (
  • No projects selected
  • )}
{/* ───────────────────────────────────────────────────────────────────── SECONDARY ACTIONS Fix/More menu - all non-critical actions demoted here ───────────────────────────────────────────────────────────────────── */}
{/* Fix/Edit actions */} setMode("tailor")}> Edit tailoring setIsEditDetailsOpen(true)}> Edit details {isRegenerating ? "Regenerating..." : "Regenerate PDF"} {isRescoring ? "Recalculating..." : "Recalculate match"} {/* Utility actions */} window.open(pdfHref, "_blank", "noopener,noreferrer") } > View PDF Copy job info {/* Destructive actions */} Skip this job
{/* ───────────────────────────────────────────────────────────────────── UNDO BAR (conditional) Lightweight undo option after marking applied ───────────────────────────────────────────────────────────────────── */} {recentlyApplied && (
{recentlyApplied.jobTitle} marked applied
)}
); };