tailoring editor confimration

This commit is contained in:
DaKheera47 2026-01-09 15:25:08 +00:00
parent b914026d8b
commit f41609bd45
4 changed files with 140 additions and 26 deletions

View File

@ -95,7 +95,7 @@ export const JobCard: React.FC<JobCardProps> = ({
const canReject = ["discovered", "ready"].includes(job.status);
const jobLink = job.applicationLink || job.jobUrl;
const pdfHref = `/pdfs/resume_${job.id}.pdf`;
const pdfHref = `/pdfs/resume_${job.id}.pdf?v=${encodeURIComponent(job.updatedAt)}`;
const deadline = formatDate(job.deadline);
const discoveredAt = formatDateTime(job.discoveredAt);
const isHighlighted = highlightedJobId === job.id;

View File

@ -221,7 +221,7 @@ export const JobTable: React.FC<JobTableProps> = ({
{jobs.map((job) => {
const jobLink = job.applicationLink || job.jobUrl;
const hasPdf = !!job.pdfPath;
const pdfHref = `/pdfs/resume_${job.id}.pdf`;
const pdfHref = `/pdfs/resume_${job.id}.pdf?v=${encodeURIComponent(job.updatedAt)}`;
const canApply = job.status === "ready";
const canProcess = ["discovered", "ready"].includes(job.status);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Check, Loader2, Sparkles, FileText, AlertTriangle } from "lucide-react";
import { toast } from "sonner";
@ -12,9 +12,18 @@ import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
interface TailoringEditorProps {
job: Job;
onUpdate: () => void | Promise<void>;
onDirtyChange?: (isDirty: boolean) => void;
onRegisterSave?: (save: () => Promise<void>) => void;
onBeforeGenerate?: () => boolean | Promise<boolean>;
}
export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate }) => {
export const TailoringEditor: React.FC<TailoringEditorProps> = ({
job,
onUpdate,
onDirtyChange,
onRegisterSave,
onBeforeGenerate,
}) => {
const [catalog, setCatalog] = useState<ResumeProjectCatalogItem[]>([]);
const [summary, setSummary] = useState(job.tailoredSummary || "");
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@ -22,6 +31,25 @@ export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const savedSelectedIds = useMemo(() => {
const saved = job.selectedProjectIds?.split(",").filter(Boolean) ?? [];
return new Set(saved);
}, [job.selectedProjectIds]);
const hasSelectionDiff = useMemo(() => {
if (selectedIds.size !== savedSelectedIds.size) return true;
for (const id of selectedIds) {
if (!savedSelectedIds.has(id)) return true;
}
return false;
}, [selectedIds, savedSelectedIds]);
const isDirty = summary !== (job.tailoredSummary || "") || hasSelectionDiff;
useEffect(() => {
onDirtyChange?.(isDirty);
}, [isDirty, onDirtyChange]);
useEffect(() => {
// Load project catalog
api.getProfileProjects().then(setCatalog).catch(console.error);
@ -36,6 +64,30 @@ export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate
setSummary(job.tailoredSummary || "");
}, [job.tailoredSummary]);
const saveChanges = useCallback(
async ({ showToast = true }: { showToast?: boolean } = {}) => {
try {
setIsSaving(true);
await api.updateJob(job.id, {
tailoredSummary: summary,
selectedProjectIds: Array.from(selectedIds).join(","),
});
if (showToast) toast.success("Changes saved");
await onUpdate();
} catch (error) {
if (showToast) toast.error("Failed to save changes");
throw error;
} finally {
setIsSaving(false);
}
},
[job.id, onUpdate, selectedIds, summary],
);
useEffect(() => {
onRegisterSave?.(() => saveChanges({ showToast: false }));
}, [onRegisterSave, saveChanges]);
const handleToggleProject = (id: string) => {
const next = new Set(selectedIds);
if (next.has(id)) next.delete(id);
@ -45,17 +97,9 @@ export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate
const handleSave = async () => {
try {
setIsSaving(true);
await api.updateJob(job.id, {
tailoredSummary: summary,
selectedProjectIds: Array.from(selectedIds).join(','),
});
toast.success("Changes saved");
await onUpdate();
} catch (error) {
toast.error("Failed to save changes");
} finally {
setIsSaving(false);
await saveChanges();
} catch {
// Toast handled in saveChanges
}
};
@ -78,12 +122,12 @@ export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate
const handleGeneratePdf = async () => {
try {
const shouldProceed = onBeforeGenerate ? await onBeforeGenerate() : true;
if (shouldProceed === false) return;
setIsGeneratingPdf(true);
// Save current state first to ensure PDF uses latest
await api.updateJob(job.id, {
tailoredSummary: summary,
selectedProjectIds: Array.from(selectedIds).join(','),
});
await saveChanges({ showToast: false });
await api.generateJobPdf(job.id);
toast.success("Resume PDF generated");

View File

@ -2,7 +2,7 @@
* Orchestrator layout with a split list/detail experience.
*/
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ArrowUpDown,
Calendar,
@ -304,6 +304,8 @@ export const OrchestratorPage: React.FC = () => {
const [isEditingDescription, setIsEditingDescription] = useState(false);
const [editedDescription, setEditedDescription] = useState("");
const [isSavingDescription, setIsSavingDescription] = useState(false);
const [hasUnsavedTailoring, setHasUnsavedTailoring] = useState(false);
const saveTailoringRef = useRef<null | (() => Promise<void>)>(null);
const [pipelineSources, setPipelineSources] = useState<JobSource[]>(() => {
try {
const raw = localStorage.getItem(PIPELINE_SOURCES_STORAGE_KEY);
@ -390,11 +392,21 @@ export const OrchestratorPage: React.FC = () => {
const handleProcess = async (jobId: string) => {
try {
setProcessingJobId(jobId);
const job = jobs.find((item) => item.id === jobId);
const force = job?.status === "ready";
await api.processJob(jobId, { force });
toast.success(force ? "Resume regenerated successfully" : "Resume generated successfully");
if (!job) throw new Error("Job not found");
const shouldProceed = await confirmAndSaveEdits({ includeTailoring: true });
if (!shouldProceed) return;
setProcessingJobId(jobId);
if (job.status === "ready") {
await api.generateJobPdf(jobId);
toast.success("Resume regenerated successfully");
} else {
await api.processJob(jobId);
toast.success("Resume generated successfully");
}
await loadJobs();
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to process job";
@ -472,6 +484,11 @@ export const OrchestratorPage: React.FC = () => {
[jobs, selectedJobId],
);
useEffect(() => {
setHasUnsavedTailoring(false);
saveTailoringRef.current = null;
}, [selectedJob?.id]);
const description = useMemo(() => {
if (!selectedJob?.jobDescription) return "No description available.";
const jd = selectedJob.jobDescription;
@ -512,11 +529,56 @@ export const OrchestratorPage: React.FC = () => {
}
};
const hasUnsavedDescription =
!!selectedJob &&
isEditingDescription &&
editedDescription !== (selectedJob.jobDescription || "");
const confirmAndSaveEdits = useCallback(
async ({ includeTailoring = true }: { includeTailoring?: boolean } = {}) => {
const pendingDescription = hasUnsavedDescription;
const pendingTailoring = includeTailoring && hasUnsavedTailoring;
if (!pendingDescription && !pendingTailoring) return true;
const parts = [];
if (pendingDescription) parts.push("job description");
if (pendingTailoring) parts.push("tailoring changes");
const message = `You have unsaved ${parts.join(" and ")}. Save before generating the PDF?`;
if (!window.confirm(message)) return false;
try {
if (pendingDescription && selectedJob) {
await api.updateJob(selectedJob.id, { jobDescription: editedDescription });
}
if (pendingTailoring) {
const saveTailoring = saveTailoringRef.current;
if (!saveTailoring) {
toast.error("Could not save tailoring changes");
return false;
}
await saveTailoring();
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to save changes";
toast.error(errorMessage);
return false;
}
return true;
},
[editedDescription, hasUnsavedDescription, hasUnsavedTailoring, selectedJob],
);
const totalJobs = Object.values(stats).reduce((a, b) => a + b, 0);
const activeResultsCount = activeJobs.length;
const selectedHasPdf = !!selectedJob?.pdfPath;
const selectedJobLink = selectedJob ? selectedJob.applicationLink || selectedJob.jobUrl : "#";
const selectedPdfHref = selectedJob ? `/pdfs/resume_${selectedJob.id}.pdf` : "#";
const selectedPdfHref = selectedJob
? `/pdfs/resume_${selectedJob.id}.pdf?v=${encodeURIComponent(selectedJob.updatedAt)}`
: "#";
const selectedDeadline = selectedJob ? formatDate(selectedJob.deadline) : null;
const selectedDiscoveredAt = selectedJob ? formatDateTime(selectedJob.discoveredAt) : null;
const canApply = selectedJob?.status === "ready";
@ -1024,7 +1086,15 @@ export const OrchestratorPage: React.FC = () => {
</TabsContent>
<TabsContent value="tailoring" className="pt-3">
<TailoringEditor job={selectedJob} onUpdate={loadJobs} />
<TailoringEditor
job={selectedJob}
onUpdate={loadJobs}
onDirtyChange={setHasUnsavedTailoring}
onRegisterSave={(save) => {
saveTailoringRef.current = save;
}}
onBeforeGenerate={() => confirmAndSaveEdits({ includeTailoring: false })}
/>
</TabsContent>
<TabsContent value="description" className="space-y-3 pt-3">