import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { Alert, Box, Button, Chip, Divider, IconButton, List, ListItemButton, ListItemText, Paper, Skeleton, Stack, Tooltip, Typography, } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh"; import ChecklistIcon from "@mui/icons-material/Checklist"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import { getApiErrorMessage } from "../api"; import Attachments from "../components/Attachments"; import Correspondence from "../components/Correspondence"; import AiWorkspacePanel from "../components/AiWorkspacePanel"; import ApplicationChecklist from "../components/ApplicationChecklist"; import { ApplicationAnalysis, ApplicationMatch, ApplicationTimeline, } from "../components/ApplicationIntelligence"; import { ApplicationCoverLetterSection, ApplicationCvSection, } from "../components/ApplicationAssets"; import { ApplicationInterviewPrep } from "../components/InterviewPrep"; import EditJobDialog from "../components/EditJobDialog"; import { WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, workspaceSection, } from "../applicationWorkspace"; // Phase 5 Milestone 1 — the dedicated Application Workspace. // // This is a surface, not a new data store: the overview is one aggregate read, and each section // reuses the component that already owns that domain (Attachments, Correspondence, AiWorkspacePanel). // docs/architecture/application-workspace.md. export default function ApplicationWorkspacePage() { return ; } type ApplicationWorkspaceProps = { jobIdOverride?: number; sectionOverride?: WorkspaceSectionKey; onSectionChange?: (section: WorkspaceSectionKey) => void; onClose?: () => void; fullPageHref?: string; }; export function ApplicationWorkspace({ jobIdOverride, sectionOverride, onSectionChange, onClose, fullPageHref, }: ApplicationWorkspaceProps) { const { id } = useParams(); const jobId = jobIdOverride ?? Number(id); const location = useLocation(); const navigate = useNavigate(); const [params, setParams] = useSearchParams(); const section = sectionOverride ?? workspaceSection(params.get("section")); const [overview, setOverview] = useState(null); const [error, setError] = useState(null); const [editOpen, setEditOpen] = useState(false); const load = useCallback(async () => { if (!Number.isInteger(jobId) || jobId <= 0) { setOverview(null); setError("This application link is invalid."); return; } try { setError(null); setOverview(await applicationWorkspaceApi.overview(jobId)); } catch (err) { setError(getApiErrorMessage(err, "Could not open this application.")); } }, [jobId]); useEffect(() => { load(); }, [load]); const go = (next: WorkspaceSectionKey) => { if (onSectionChange) onSectionChange(next); else setParams({ section: next }, { replace: true, state: location.state }); }; const close = onClose ?? (() => { const from = (location.state as { from?: unknown } | null)?.from; navigate(typeof from === "string" && from.startsWith("/") && !from.startsWith("//") ? from : "/jobs", { replace: true }); }); if (error) { return ( {error} ); } return ( Workspace {fullPageHref ? ( ) : null} {WORKSPACE_SECTIONS.map((s) => ( go(s.key)} sx={{ borderRadius: 2 }}> ))} setEditOpen(true)} /> {section === "overview" && } {section === "job-details" && setEditOpen(true)} />} {/* Deterministic answer first, then the AI panel below it — the page never generates on load. */} {section === "analysis" && jobId > 0 && } {section === "match" && jobId > 0 && } {section === "timeline" && jobId > 0 && } {section === "interview" && jobId > 0 && } {(section === "analysis" || section === "match" || section === "interview") && jobId > 0 && ( )} {section === "documents" && jobId > 0 && ( )} {section === "communication" && jobId > 0 && ( )} {section === "checklist" && jobId > 0 && ( )} {section === "cv" && jobId > 0 && } {section === "cover-letter" && jobId > 0 && ( <> {/* Generation stays an explicit user action, below the editor the user owns. */} )} 0 ? jobId : null} onClose={() => setEditOpen(false)} onSaved={() => { setEditOpen(false); void load(); }} /> ); } function WorkspaceHeader({ overview, onEdit }: { overview: WorkspaceOverview | null; onEdit: () => void }) { if (!overview) return ; return ( {overview.jobTitle} {[overview.company, overview.location, overview.salary].filter(Boolean).join(" · ") || "—"} {overview.source ? : null} {overview.jobUrl && ( )} ); } function OverviewSection({ overview, onGo, onReload }: { overview: WorkspaceOverview | null; onGo: (s: WorkspaceSectionKey) => void; onReload: () => void; }) { const stats = useMemo(() => overview ? [ { icon: , label: "CV", value: overview.cv.variantName ?? (overview.cv.hasTailoredCvText ? "Tailored text" : "Not prepared"), ok: !!overview.cv.variantId || overview.cv.hasTailoredCvText, go: "cv" as const }, { icon: , label: "Cover letter", value: overview.hasCoverLetter ? "Ready" : "Not written", ok: overview.hasCoverLetter, go: "cover-letter" as const }, { icon: , label: "Documents", value: overview.documentCount ? `${overview.documentCount} attached` : "None", ok: overview.documentCount > 0, go: "documents" as const }, { icon: , label: "AI suggestions", value: overview.aiInteractionCount ? `${overview.aiInteractionCount} saved` : "None yet", ok: overview.aiInteractionCount > 0, go: "analysis" as const }, { icon: , label: "Checklist", value: overview.checklistProgress ? `${overview.checklistProgress.completed}/${overview.checklistProgress.total} done` : "—", ok: (overview.checklistProgress?.percent ?? 0) === 100, go: "checklist" as const }, ] : [], [overview]); if (!overview) { return {[0, 1].map((i) => )}; } return ( {overview.nextStep ? ( Next recommended action {overview.nextStep.label} {overview.nextStep.reason} ) : ( Nothing outstanding — this application is fully prepared. )} {stats.map((s) => ( onGo(s.go)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onGo(s.go); } }} sx={{ p: 1.5, borderRadius: 3, cursor: "pointer", "&:focus-visible": { boxShadow: 3 } }}> {s.icon} {s.label} {s.value} ))} Recent activity {overview.recentActivity.length === 0 ? ( No activity recorded yet. ) : ( {overview.recentActivity.map((a, i) => ( {a.type}{a.detail ? ` — ${a.detail}` : ""} {new Date(a.at).toLocaleDateString()} ))} )} ); } function JobDetailsSection({ overview, onEdit }: { overview: WorkspaceOverview | null; onEdit: () => void }) { if (!overview) return ; const rows: [string, string][] = [ ["Company", overview.company ?? "—"], ["Location", overview.location ?? "—"], ["Country", overview.countryCode ?? "—"], ["Source", overview.source ?? "—"], ["Salary", overview.salary ?? "—"], ["Status", overview.status], ["Discovered", overview.savedAt ? new Date(overview.savedAt).toLocaleDateString() : "—"], ["Applied", overview.dateApplied ? new Date(overview.dateApplied).toLocaleDateString() : "—"], ["Deadline", overview.deadline ? new Date(overview.deadline).toLocaleDateString() : "—"], ["Follow-up", overview.followUpAt ? new Date(overview.followUpAt).toLocaleDateString() : "Not scheduled"], ["Next action", overview.nextAction ?? "—"], ]; return ( Application information {rows.map(([k, v]) => ( {k} {v} ))} {overview.tags.length > 0 ? ( {overview.tags.map((tag) => )} ) : null} {overview.notes ? ( Notes {overview.notes} ) : null} Job description {!overview.hasJobDescription ? ( Add advert}> No advert text saved. Analysis and matching need the job description. ) : ( {overview.translatedDescription ? ( Translated advert {overview.translatedDescription} ) : null} {overview.description ? ( {overview.translatedDescription ? Original advert{overview.descriptionLanguage ? ` · ${overview.descriptionLanguage.toUpperCase()}` : ""} : null} {overview.description} ) : null} )} ); }