Optimize workspace and daily-loop surfaces

This commit is contained in:
2026-04-11 12:03:49 +02:00
parent 27fd70a2d7
commit 33ac4b963b
7 changed files with 474 additions and 224 deletions
@@ -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]);
}