diff --git a/orchestrator/src/client/components/ReadyPanel.tsx b/orchestrator/src/client/components/ReadyPanel.tsx index ee82f8f..10ccc40 100644 --- a/orchestrator/src/client/components/ReadyPanel.tsx +++ b/orchestrator/src/client/components/ReadyPanel.tsx @@ -46,6 +46,7 @@ import * as api from "../api"; import { FitAssessment, JobHeader, TailoredSummary } from "."; import { TailorMode } from "./discovered-panel/TailorMode"; import { useProfile } from "../hooks/useProfile"; +import { useRescoreJob } from "../hooks/useRescoreJob"; import type { Job, ResumeProjectCatalogItem } from "../../shared/types"; type PanelMode = "ready" | "tailor"; @@ -67,7 +68,7 @@ export const ReadyPanel: React.FC = ({ const [mode, setMode] = useState("ready"); const [isMarkingApplied, setIsMarkingApplied] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false); - const [isRescoring, setIsRescoring] = useState(false); + const { isRescoring, rescoreJob } = useRescoreJob(onJobUpdated); const [catalog, setCatalog] = useState([]); const [recentlyApplied, setRecentlyApplied] = useState<{ jobId: string; @@ -182,21 +183,7 @@ export const ReadyPanel: React.FC = ({ } }, [job, onJobUpdated]); - const handleRescore = useCallback(async () => { - if (!job) return; - - try { - setIsRescoring(true); - await api.rescoreJob(job.id); - toast.success("Match recalculated"); - await onJobUpdated(); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to recalculate match"; - toast.error(message); - } finally { - setIsRescoring(false); - } - }, [job, onJobUpdated]); + const handleRescore = useCallback(() => rescoreJob(job?.id), [job?.id, rescoreJob]); const handleSkip = useCallback(async () => { if (!job) return; diff --git a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx index 709bdbd..25e8d24 100644 --- a/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx +++ b/orchestrator/src/client/components/discovered-panel/DiscoveredPanel.tsx @@ -7,6 +7,7 @@ import { DecideMode } from "./DecideMode"; import { EmptyState } from "./EmptyState"; import { ProcessingState } from "./ProcessingState"; import { TailorMode } from "./TailorMode"; +import { useRescoreJob } from "../../hooks/useRescoreJob"; type PanelMode = "decide" | "tailor"; @@ -24,13 +25,12 @@ export const DiscoveredPanel: React.FC = ({ const [mode, setMode] = useState("decide"); const [isSkipping, setIsSkipping] = useState(false); const [isFinalizing, setIsFinalizing] = useState(false); - const [isRescoring, setIsRescoring] = useState(false); + const { isRescoring, rescoreJob } = useRescoreJob(onJobUpdated); useEffect(() => { setMode("decide"); setIsSkipping(false); setIsFinalizing(false); - setIsRescoring(false); }, [job?.id]); const handleSkip = async () => { @@ -71,21 +71,7 @@ export const DiscoveredPanel: React.FC = ({ } }; - const handleRescore = async () => { - if (!job) return; - try { - setIsRescoring(true); - await api.rescoreJob(job.id); - toast.success("Match recalculated"); - await onJobUpdated(); - } catch (error) { - const message = - error instanceof Error ? error.message : "Failed to recalculate match"; - toast.error(message); - } finally { - setIsRescoring(false); - } - }; + const handleRescore = () => rescoreJob(job?.id); if (!job) { return ; diff --git a/orchestrator/src/client/hooks/useRescoreJob.test.ts b/orchestrator/src/client/hooks/useRescoreJob.test.ts new file mode 100644 index 0000000..d711ccc --- /dev/null +++ b/orchestrator/src/client/hooks/useRescoreJob.test.ts @@ -0,0 +1,38 @@ +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { useRescoreJob } from "./useRescoreJob"; +import * as api from "../api"; +import { toast } from "sonner"; + +vi.mock("../api", () => ({ + rescoreJob: vi.fn(), +})); + +vi.mock("sonner", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +describe("useRescoreJob", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("rescoring updates the job and shows a toast", async () => { + const onJobUpdated = vi.fn().mockResolvedValue(undefined); + vi.mocked(api.rescoreJob).mockResolvedValue({} as any); + + const { result } = renderHook(() => useRescoreJob(onJobUpdated)); + + await act(async () => { + await result.current.rescoreJob("job-1"); + }); + + expect(api.rescoreJob).toHaveBeenCalledWith("job-1"); + expect(onJobUpdated).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith("Match recalculated"); + }); +}); diff --git a/orchestrator/src/client/hooks/useRescoreJob.ts b/orchestrator/src/client/hooks/useRescoreJob.ts new file mode 100644 index 0000000..2ebb44d --- /dev/null +++ b/orchestrator/src/client/hooks/useRescoreJob.ts @@ -0,0 +1,29 @@ +import { useCallback, useState } from "react"; +import { toast } from "sonner"; + +import * as api from "../api"; + +export function useRescoreJob(onJobUpdated: () => void | Promise) { + const [isRescoring, setIsRescoring] = useState(false); + + const rescoreJob = useCallback( + async (jobId?: string | null) => { + if (!jobId) return; + + try { + setIsRescoring(true); + await api.rescoreJob(jobId); + toast.success("Match recalculated"); + await onJobUpdated(); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to recalculate match"; + toast.error(message); + } finally { + setIsRescoring(false); + } + }, + [onJobUpdated], + ); + + return { isRescoring, rescoreJob }; +}