Optimize workspace and daily-loop surfaces
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "../../api";
|
||||
import { AttachmentItem, JobApplication } from "../../types";
|
||||
|
||||
type PackageWorkspaceState = {
|
||||
coverLetter: string;
|
||||
applicationAnswer: string;
|
||||
recruiterMessage: string;
|
||||
};
|
||||
|
||||
const APPLICATION_ANSWER_START = "<<<APPLICATION_ANSWER_DRAFT>>>";
|
||||
const APPLICATION_ANSWER_END = "<<<END_APPLICATION_ANSWER_DRAFT>>>";
|
||||
|
||||
function extractApplicationAnswerDraft(notes?: string | null) {
|
||||
const value = (notes ?? "").trim();
|
||||
if (!value) return "";
|
||||
|
||||
const startIndex = value.indexOf(APPLICATION_ANSWER_START);
|
||||
const endIndex = value.indexOf(APPLICATION_ANSWER_END);
|
||||
if (startIndex >= 0 && endIndex > startIndex) {
|
||||
return value.slice(startIndex + APPLICATION_ANSWER_START.length, endIndex).trim();
|
||||
}
|
||||
|
||||
const legacyMatch = value.match(/Application answer draft:\s*\n([\s\S]*)$/i);
|
||||
return legacyMatch?.[1]?.trim() ?? "";
|
||||
}
|
||||
|
||||
export function useJobWorkspaceBaseData({
|
||||
open,
|
||||
jobId,
|
||||
initialTab,
|
||||
initialFollowUpMode,
|
||||
}: {
|
||||
open: boolean;
|
||||
jobId: number | null;
|
||||
initialTab: number;
|
||||
initialFollowUpMode?: string;
|
||||
}) {
|
||||
const [job, setJob] = useState<JobApplication | null>(null);
|
||||
const [tab, setTab] = useState(0);
|
||||
const [history, setHistory] = useState<{ id: number; type: string; oldValue?: string; newValue?: string; note?: string; at: string }[]>([]);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [jobAttachments, setJobAttachments] = useState<AttachmentItem[]>([]);
|
||||
const [selectedAttachmentIds, setSelectedAttachmentIds] = useState<number[]>([]);
|
||||
const [profileAvatarImageDataUrl, setProfileAvatarImageDataUrl] = useState<string | null>(null);
|
||||
const [packageWorkspace, setPackageWorkspace] = useState<PackageWorkspaceState>({ coverLetter: "", applicationAnswer: "", recruiterMessage: "" });
|
||||
const [savedPackageWorkspace, setSavedPackageWorkspace] = useState<PackageWorkspaceState>({ coverLetter: "", applicationAnswer: "", recruiterMessage: "" });
|
||||
const [packageGeneratedAt, setPackageGeneratedAt] = useState<string | null>(null);
|
||||
const [draftRecipient, setDraftRecipient] = useState("");
|
||||
const [followUpMode, setFollowUpMode] = useState(initialFollowUpMode || "post-apply");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !jobId) return;
|
||||
|
||||
setTab(Math.max(0, Math.min(9, initialTab)));
|
||||
setJob(null);
|
||||
setHistory([]);
|
||||
setIsAdmin(false);
|
||||
setJobAttachments([]);
|
||||
setSelectedAttachmentIds([]);
|
||||
setProfileAvatarImageDataUrl(null);
|
||||
setPackageWorkspace({ coverLetter: "", applicationAnswer: "", recruiterMessage: "" });
|
||||
setSavedPackageWorkspace({ coverLetter: "", applicationAnswer: "", recruiterMessage: "" });
|
||||
setPackageGeneratedAt(null);
|
||||
|
||||
void api.get<JobApplication>(`/jobapplications/${jobId}`).then((r) => {
|
||||
setJob(r.data);
|
||||
const savedWorkspace = {
|
||||
coverLetter: r.data.coverLetterText ?? "",
|
||||
applicationAnswer: extractApplicationAnswerDraft(r.data.notes),
|
||||
recruiterMessage: r.data.recruiterMessageDraft ?? "",
|
||||
};
|
||||
setSavedPackageWorkspace(savedWorkspace);
|
||||
setPackageWorkspace(savedWorkspace);
|
||||
setDraftRecipient(r.data.company?.recruiterEmail ?? "");
|
||||
setFollowUpMode(initialFollowUpMode || (r.data.status?.includes("Interview") ? "post-interview" : r.data.status === "Waiting" ? "waiting-update" : r.data.status === "Offer" ? "offer-checkin" : r.data.status === "Rejected" ? "feedback-request" : "post-apply"));
|
||||
});
|
||||
|
||||
void api.get<AttachmentItem[]>(`/attachments/${jobId}`).then((r) => {
|
||||
const items = Array.isArray(r.data) ? r.data : [];
|
||||
setJobAttachments(items);
|
||||
const defaultIds = items.filter((item) => item.useForAi !== false).slice(0, 3).map((item) => item.id);
|
||||
setSelectedAttachmentIds(defaultIds.length > 0 ? defaultIds : items.slice(0, 3).map((item) => item.id));
|
||||
}).catch(() => {
|
||||
setJobAttachments([]);
|
||||
setSelectedAttachmentIds([]);
|
||||
});
|
||||
|
||||
void api.get(`/auth/me`).then((r) => {
|
||||
setIsAdmin(Boolean(r.data?.roles?.includes("Admin")));
|
||||
setProfileAvatarImageDataUrl(r.data?.avatarImageDataUrl ?? null);
|
||||
}).catch(() => {
|
||||
setIsAdmin(false);
|
||||
setProfileAvatarImageDataUrl(null);
|
||||
});
|
||||
|
||||
void api.get(`/jobapplications/${jobId}/history`).then((r) => setHistory(r.data)).catch(() => setHistory([]));
|
||||
}, [open, jobId, initialTab, initialFollowUpMode]);
|
||||
|
||||
return {
|
||||
job,
|
||||
setJob,
|
||||
tab,
|
||||
setTab,
|
||||
history,
|
||||
setHistory,
|
||||
isAdmin,
|
||||
jobAttachments,
|
||||
setJobAttachments,
|
||||
selectedAttachmentIds,
|
||||
setSelectedAttachmentIds,
|
||||
profileAvatarImageDataUrl,
|
||||
packageWorkspace,
|
||||
setPackageWorkspace,
|
||||
savedPackageWorkspace,
|
||||
setSavedPackageWorkspace,
|
||||
packageGeneratedAt,
|
||||
setPackageGeneratedAt,
|
||||
draftRecipient,
|
||||
setDraftRecipient,
|
||||
followUpMode,
|
||||
setFollowUpMode,
|
||||
};
|
||||
}
|
||||
|
||||
export { extractApplicationAnswerDraft };
|
||||
export type { PackageWorkspaceState };
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
|
||||
export function useWorkspaceTabCache<T>() {
|
||||
const cacheRef = useRef(new Map<string, T>());
|
||||
|
||||
const getCached = useCallback((key: string) => cacheRef.current.get(key), []);
|
||||
const setCached = useCallback((key: string, value: T) => {
|
||||
cacheRef.current.set(key, value);
|
||||
}, []);
|
||||
const clearCached = useCallback((prefix?: string) => {
|
||||
if (!prefix) {
|
||||
cacheRef.current.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Array.from(cacheRef.current.keys())) {
|
||||
if (key.startsWith(prefix)) {
|
||||
cacheRef.current.delete(key);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return useMemo(() => ({ getCached, setCached, clearCached }), [getCached, setCached, clearCached]);
|
||||
}
|
||||
Reference in New Issue
Block a user