feat(ui): redesign job workspace sections
This commit is contained in:
@@ -27,7 +27,7 @@ import {
|
||||
ApplicationAnalysis, ApplicationMatch, ApplicationTimeline,
|
||||
} from "../components/ApplicationIntelligence";
|
||||
import {
|
||||
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection,
|
||||
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, SubmittedPackagesSection,
|
||||
} from "../components/ApplicationAssets";
|
||||
import { ApplicationInterviewPrep } from "../components/InterviewPrep";
|
||||
import EditJobDialog from "../components/EditJobDialog";
|
||||
@@ -40,7 +40,7 @@ import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useConfirm } from "../confirm";
|
||||
import {
|
||||
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi,
|
||||
checklistSystemTranslationToken, workspaceSection,
|
||||
checklistSystemTranslationToken, workspaceSection, workspaceSubsection,
|
||||
} from "../applicationWorkspace";
|
||||
|
||||
// Phase 5 Milestone 1 — the dedicated Application Workspace.
|
||||
@@ -54,7 +54,7 @@ export default function ApplicationWorkspacePage() {
|
||||
|
||||
type ApplicationWorkspaceProps = {
|
||||
jobIdOverride?: number;
|
||||
sectionOverride?: WorkspaceSectionKey;
|
||||
sectionOverride?: WorkspaceSectionKey | "analysis" | "cv" | "cover-letter";
|
||||
onSectionChange?: (section: WorkspaceSectionKey) => void;
|
||||
onClose?: () => void;
|
||||
fullPageHref?: string;
|
||||
@@ -74,8 +74,13 @@ export function ApplicationWorkspace({
|
||||
const { t } = useI18n();
|
||||
const { confirm } = useConfirm();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const section = sectionOverride ?? workspaceSection(params.get("section"));
|
||||
const section = workspaceSection(sectionOverride ?? params.get("section"));
|
||||
const [overlaySubsection, setOverlaySubsection] = useState<{ section: WorkspaceSectionKey; value: string } | null>(null);
|
||||
const subsection = onSectionChange && overlaySubsection?.section === section
|
||||
? overlaySubsection.value : workspaceSubsection(sectionOverride ?? params.get("section"), params.get("sub"));
|
||||
const sectionId = useId();
|
||||
const secondaryId = useId();
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [overview, setOverview] = useState<WorkspaceOverview | null>(null);
|
||||
const [error, setError] = useState<ViewResourceError | null>(null);
|
||||
@@ -140,15 +145,25 @@ export function ApplicationWorkspace({
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const go = (next: string) => {
|
||||
useEffect(() => {
|
||||
// Direct entry gets a named target; keyboard tab activation keeps tab focus.
|
||||
if (!loading && section !== "overview" && document.activeElement === document.body) panelRef.current?.focus({ preventScroll: true });
|
||||
}, [loading, section, subsection]);
|
||||
|
||||
const go = (next: string, sub?: string) => {
|
||||
if (next === "career-profile") {
|
||||
navigate("/career?section=profile");
|
||||
return;
|
||||
}
|
||||
if (onSectionChange) onSectionChange(workspaceSection(next));
|
||||
if (onSectionChange) {
|
||||
setOverlaySubsection({ section: workspaceSection(next), value: workspaceSubsection(next, sub ?? null) });
|
||||
onSectionChange(workspaceSection(next));
|
||||
}
|
||||
else setParams((current) => {
|
||||
const updated = new URLSearchParams(current);
|
||||
updated.set("section", next);
|
||||
if (sub) updated.set("sub", sub);
|
||||
else updated.delete("sub");
|
||||
return updated;
|
||||
}, { state: location.state });
|
||||
};
|
||||
@@ -161,6 +176,15 @@ export function ApplicationWorkspace({
|
||||
{ replace: true, state: focusJobId ? { focusJobId } : undefined },
|
||||
);
|
||||
});
|
||||
const subsections = section === "application" ? [
|
||||
{ key: "cv", label: t("workspaceCv") },
|
||||
{ key: "letter", label: t("workspaceCoverLetter") },
|
||||
{ key: "answers", label: t("workspaceAnswers") },
|
||||
{ key: "submission", label: t("workspaceSubmission") },
|
||||
] : [
|
||||
{ key: "activity", label: t("workspaceActivityHistory") },
|
||||
{ key: "correspondence", label: t("workspaceCommunication") },
|
||||
];
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -213,10 +237,10 @@ export function ApplicationWorkspace({
|
||||
</Tabs>
|
||||
</Paper>
|
||||
|
||||
<Box role="tabpanel" id={`${sectionId}-panel`} aria-labelledby={`${sectionId}-${section}`} tabIndex={0} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
<Box ref={panelRef} role="tabpanel" id={`${sectionId}-panel`} aria-labelledby={`${sectionId}-${section}`} tabIndex={0} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
{section === "overview" && jobId > 0 && <ApplicationStatusSuggestion jobId={jobId} onApplied={load} />}
|
||||
{section === "overview" && <OverviewSection overview={overview} onGo={go} onReload={load} onEdit={() => setEditOpen(true)} />}
|
||||
{section === "analysis" && jobId > 0 && (
|
||||
{section === "match" && jobId > 0 && (
|
||||
<Stack spacing={2}>
|
||||
<ApplicationAnalysis jobId={jobId} />
|
||||
<ApplicationMatch jobId={jobId} />
|
||||
@@ -224,19 +248,39 @@ export function ApplicationWorkspace({
|
||||
</Stack>
|
||||
)}
|
||||
{section === "interview" && jobId > 0 && <ApplicationInterviewPrep jobId={jobId} />}
|
||||
{section === "cv" && jobId > 0 && <ApplicationCvSection jobId={jobId} />}
|
||||
{section === "cover-letter" && jobId > 0 && (
|
||||
<>
|
||||
<ApplicationCoverLetterSection jobId={jobId} onDirtyChange={setCoverLetterDirty} />
|
||||
<ApplicationPackageDraftsSection
|
||||
{(section === "application" || section === "timeline") && <>
|
||||
<Tabs value={subsection} onChange={(_, sub: string) => go(section, sub)} variant="scrollable" scrollButtons="auto" aria-label={t(section === "application" ? "workspaceApplicationSections" : "workspaceTimelineSections")}>
|
||||
{subsections.map((sub) => <Tab key={sub.key} value={sub.key} id={`${secondaryId}-${sub.key}`} aria-controls={`${secondaryId}-panel`} label={sub.label} />)}
|
||||
</Tabs>
|
||||
<Box role="tabpanel" id={`${secondaryId}-panel`} aria-labelledby={`${secondaryId}-${subsection}`} tabIndex={0} sx={{ display: "grid", gap: 2, minWidth: 0 }}>
|
||||
{section === "application" && subsection === "cv" && jobId > 0 && <ApplicationCvSection jobId={jobId} includePackages={false} />}
|
||||
{section === "application" && subsection === "letter" && jobId > 0 && <ApplicationCoverLetterSection jobId={jobId} onDirtyChange={setCoverLetterDirty} />}
|
||||
{section === "application" && subsection === "answers" && jobId > 0 && <ApplicationPackageDraftsSection
|
||||
jobId={jobId}
|
||||
initialApplicationAnswer={overview?.applicationAnswerDraft ?? ""}
|
||||
initialRecruiterMessage={overview?.recruiterMessageDraft ?? ""}
|
||||
onSaved={load}
|
||||
onDirtyChange={setPackageDraftsDirty}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>}
|
||||
{section === "application" && subsection === "submission" && jobId > 0 && <SubmittedPackagesSection jobId={jobId} />}
|
||||
{section === "timeline" && subsection === "activity" && jobId > 0 && <ApplicationTimeline jobId={jobId} />}
|
||||
{section === "timeline" && subsection === "correspondence" && jobId > 0 && <PageSection surface title={t("workspaceCommunication")} level="h3"><Correspondence jobId={jobId} jobContext={{ companyName: overview?.company, jobTitle: overview?.jobTitle }} /></PageSection>}
|
||||
</Box>
|
||||
</>}
|
||||
{section === "documents" && jobId > 0 && <>
|
||||
<PageSection surface title={t("workspaceCurrentDocuments")} description={t("workspaceCurrentDocumentsHelp")} actions={<>
|
||||
<Button onClick={() => go("application", "cv")}>{t("workspaceCv")}</Button>
|
||||
<Button onClick={() => go("application", "letter")}>{t("workspaceCoverLetter")}</Button>
|
||||
</>}><Attachments jobId={jobId} /></PageSection>
|
||||
<SubmittedPackagesSection jobId={jobId} allowCapture={false} />
|
||||
</>}
|
||||
{section === "tasks" && jobId > 0 && <>
|
||||
<PageSection surface title={t("workspaceNotes")} actions={<Button startIcon={<EditOutlinedIcon />} onClick={() => setEditOpen(true)}>{t("workspaceEdit")}</Button>}>
|
||||
<Typography sx={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{overview?.notes || t("workspaceNoNotes")}</Typography>
|
||||
</PageSection>
|
||||
<PageSection title={t("workspaceChecklist")}><ApplicationChecklist jobId={jobId} onChanged={load} /></PageSection>
|
||||
<PageSection surface title={t("calendarTitle")}><ApplicationCalendarActions jobId={jobId} followUpAt={overview?.followUpAt ?? null} deadline={overview?.deadline ?? null} /></PageSection>
|
||||
</>}
|
||||
</Box>
|
||||
<EditJobDialog
|
||||
open={editOpen}
|
||||
@@ -391,17 +435,17 @@ function OverviewSection({ overview, onGo, onReload, onEdit }: {
|
||||
)}
|
||||
</PageSection>
|
||||
|
||||
<OverviewDetails jobId={overview.id} overview={overview} onReload={onReload} onEdit={onEdit} />
|
||||
<OverviewDetails overview={overview} onEdit={onEdit} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number; overview: WorkspaceOverview; onReload: () => void; onEdit: () => void }) {
|
||||
function OverviewDetails({ overview, onEdit }: { overview: WorkspaceOverview; onEdit: () => void }) {
|
||||
const { t } = useI18n();
|
||||
const [params] = useSearchParams();
|
||||
const prefix = useId();
|
||||
const triggers = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const targets: Record<string, string> = { "job-details": "details", checklist: "tasks", documents: "documents", portfolio: "documents", timeline: "timeline", communication: "communication" };
|
||||
const targets: Record<string, string> = { "job-details": "details" };
|
||||
const key = params.get("section") ?? "";
|
||||
const target = Object.prototype.hasOwnProperty.call(targets, key) ? targets[key] : undefined;
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
@@ -412,11 +456,6 @@ function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number;
|
||||
}, [target]);
|
||||
const panels = [
|
||||
{ id: "details", title: t("workspaceJobDetails"), content: <JobDetailsSection overview={overview} onEdit={onEdit} /> },
|
||||
{ id: "tasks", title: t("workspaceChecklist"), content: <ApplicationChecklist jobId={jobId} onChanged={onReload} /> },
|
||||
{ id: "calendar", title: t("calendarTitle"), content: <ApplicationCalendarActions jobId={jobId} followUpAt={overview.followUpAt} deadline={overview.deadline} /> },
|
||||
{ id: "timeline", title: t("workspaceActivityHistory"), content: <ApplicationTimeline jobId={jobId} /> },
|
||||
{ id: "documents", title: t("workspaceDocuments"), content: <Attachments jobId={jobId} /> },
|
||||
{ id: "communication", title: t("workspaceCommunication"), content: <Correspondence jobId={jobId} jobContext={{ companyName: overview.company, jobTitle: overview.jobTitle }} /> },
|
||||
];
|
||||
return (
|
||||
<Box>
|
||||
@@ -435,9 +474,11 @@ function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number;
|
||||
function workspaceSectionLabel(t: (key: any) => string, section: WorkspaceSectionKey): string {
|
||||
const keys: Record<WorkspaceSectionKey, any> = {
|
||||
overview: "workspaceOverview",
|
||||
analysis: "workspaceAnalysis",
|
||||
cv: "workspaceCv",
|
||||
"cover-letter": "workspaceCoverLetter",
|
||||
match: "workspaceMatch",
|
||||
application: "workspaceApplication",
|
||||
documents: "workspaceDocuments",
|
||||
timeline: "workspaceTimeline",
|
||||
tasks: "workspaceTasks",
|
||||
interview: "workspaceInterviewPrep",
|
||||
};
|
||||
return t(keys[section]);
|
||||
|
||||
Reference in New Issue
Block a user