import React, { useCallback, useEffect, useMemo, useState } from "react"; import { BlockerFunction, useBeforeUnload, useBlocker, useLocation, useNavigate, useParams, useSearchParams, } from "react-router-dom"; import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Chip, Divider, IconButton, Paper, Skeleton, Stack, Tab, Tabs, 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 ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { getApiErrorMessage } from "../api"; import Attachments from "../components/Attachments"; import Correspondence from "../components/Correspondence"; import ApplicationChecklist from "../components/ApplicationChecklist"; import ApplicationCalendarActions from "../components/ApplicationCalendarActions"; import { ApplicationAnalysis, ApplicationMatch, ApplicationTimeline, } from "../components/ApplicationIntelligence"; import { ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, } from "../components/ApplicationAssets"; import { ApplicationInterviewPrep } from "../components/InterviewPrep"; import EditJobDialog from "../components/EditJobDialog"; import { ApplicationStatusSuggestion, ApplicationStrategySnapshot } from "../components/ApplicationWorkflowAssist"; import { PIPELINE_STATUSES, normalizeStatus, statusLabel, statusTone } from "../pipeline"; import { useI18n } from "../i18n/I18nProvider"; import { useConfirm } from "../confirm"; import { WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, checklistSystemTranslationToken, 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 (for example Attachments and Correspondence). // 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 { t } = useI18n(); const { confirm } = useConfirm(); 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 [coverLetterDirty, setCoverLetterDirty] = useState(false); const [packageDraftsDirty, setPackageDraftsDirty] = useState(false); const hasUnsavedChanges = coverLetterDirty || packageDraftsDirty; const shouldBlock = useCallback( ({ currentLocation, nextLocation }) => hasUnsavedChanges && ( currentLocation.pathname !== nextLocation.pathname || currentLocation.search !== nextLocation.search ), [hasUnsavedChanges], ); const blocker = useBlocker(shouldBlock); useBeforeUnload(useCallback((event) => { if (!hasUnsavedChanges) return; event.preventDefault(); event.returnValue = ""; }, [hasUnsavedChanges])); useEffect(() => { if (blocker.state !== "blocked") return; const blockedNavigation = blocker; let active = true; void confirm({ title: t("workspaceUnsavedTitle"), message: t("workspaceUnsavedMessage"), confirmLabel: t("workspaceDiscardLeave"), cancelLabel: t("workspaceKeepEditing"), destructive: true, }).then((approved) => { if (!active) return; if (approved) blockedNavigation.proceed(); else blockedNavigation.reset(); }); return () => { active = false; }; }, [blocker, confirm, t]); const load = useCallback(async () => { if (!Number.isInteger(jobId) || jobId <= 0) { setOverview(null); setError(t("workspaceInvalidLink")); return; } try { setError(null); setOverview(await applicationWorkspaceApi.overview(jobId)); } catch (err) { setError(getApiErrorMessage(err, t("workspaceLoadFailed"))); } }, [jobId, t]); useEffect(() => { load(); }, [load]); const go = (next: WorkspaceSectionKey) => { if (onSectionChange) onSectionChange(next); else setParams({ section: next }, { replace: true, state: location.state }); }; const close = onClose ?? (() => { const state = location.state as { from?: unknown; focusJobId?: unknown } | null; const from = state?.from; const focusJobId = typeof state?.focusJobId === "number" ? state.focusJobId : undefined; navigate( typeof from === "string" && from.startsWith("/") && !from.startsWith("//") ? from : "/jobs", { replace: true, state: focusJobId ? { focusJobId } : undefined }, ); }); if (error) { return ( {error} ); } return ( {t("workspace")} {fullPageHref ? ( ) : null} setEditOpen(true)} /> go(value)} variant="scrollable" scrollButtons="auto" aria-label={t("workspaceSections")} sx={{ px: { xs: 0.5, sm: 1.5 }, borderTop: 1, borderColor: "divider", minHeight: 46 }} > {WORKSPACE_SECTIONS.map((s) => )} {section === "overview" && jobId > 0 && } {section === "overview" && setEditOpen(true)} />} {section === "analysis" && jobId > 0 && ( )} {section === "interview" && jobId > 0 && } {section === "cv" && jobId > 0 && } {section === "cover-letter" && jobId > 0 && ( <> )} 0 ? jobId : null} onClose={() => setEditOpen(false)} onSaved={() => { setEditOpen(false); void load(); }} /> ); } function WorkspaceHeader({ overview, onEdit }: { overview: WorkspaceOverview | null; onEdit: () => void }) { const { t } = useI18n(); if (!overview) return ; return ( {overview.jobTitle} {[overview.company, overview.location, overview.salary].filter(Boolean).join(" · ") || "—"} {overview.source ? : null} {overview.jobUrl && ( )} ); } function ApplicationProgress({ status }: { status: string }) { const { t } = useI18n(); const normalized = normalizeStatus(status); const currentOrder = normalized === "Other" ? Number.MAX_SAFE_INTEGER : PIPELINE_STATUSES.indexOf(normalized) + 1; const isTerminal = normalized === "Rejected" || normalized === "Ghosted" || normalized === "Withdrawn"; const stages = isTerminal ? [...PIPELINE_STATUSES.filter((stage) => !["Offer", "Rejected", "Ghosted", "Withdrawn"].includes(stage)), normalized] : PIPELINE_STATUSES.filter((stage) => !["Rejected", "Ghosted", "Withdrawn"].includes(stage)); return ( {t("workspaceProgress")} {stages.map((stage, index) => { const order = PIPELINE_STATUSES.indexOf(stage) + 1; const current = stage === normalized; const complete = !current && order < currentOrder; return ( {index > 0 && } `0 0 0 4px ${theme.palette.primary.main}26` : "none" }} /> {statusLabel(t, stage)} ); })} ); } function OverviewSection({ overview, onGo, onReload, onEdit }: { overview: WorkspaceOverview | null; onGo: (s: WorkspaceSectionKey) => void; onReload: () => void; onEdit: () => void; }) { const { language, t } = useI18n(); const stats = useMemo(() => overview ? [ { icon: , label: t("workspaceCv"), value: overview.cv.variantName ?? (overview.cv.hasTailoredCvText ? t("workspaceTailoredText") : t("workspaceNotPrepared")), ok: !!overview.cv.variantId || overview.cv.hasTailoredCvText, go: "cv" as const }, { icon: , label: t("jobDetailsCoverLetter"), value: overview.hasCoverLetter ? t("workspaceReady") : t("workspaceNotWritten"), ok: overview.hasCoverLetter, go: "cover-letter" as const }, { icon: , label: t("workspaceDocuments"), value: overview.documentCount ? t("workspaceAttachedCount", { count: overview.documentCount }) : t("assetsNone"), ok: overview.documentCount > 0, go: "overview" as const }, { icon: , label: t("workspaceAiSuggestions"), value: overview.aiInteractionCount ? t("workspaceSavedCount", { count: overview.aiInteractionCount }) : t("workspaceNoneYet"), ok: overview.aiInteractionCount > 0, go: "analysis" as const }, { icon: , label: t("workspaceChecklist"), value: overview.checklistProgress ? t("workspaceDoneCount", { completed: overview.checklistProgress.completed, total: overview.checklistProgress.total }) : "—", ok: (overview.checklistProgress?.percent ?? 0) === 100, go: "overview" as const }, ] : [], [overview, t]); if (!overview) { return {[0, 1].map((i) => )}; } const nextStepToken = checklistSystemTranslationToken(overview.nextStep?.key); const nextStepLabel = overview.nextStep ? nextStepToken ? t(`checklistItem_${nextStepToken}` as any) : overview.nextStep.label : null; const nextStepReason = overview.nextStep ? nextStepToken ? t(`checklistItem_${nextStepToken}Description` as any) : overview.nextStep.reason : null; return ( {overview.nextStep ? ( {t("workspaceNextAction")} {nextStepLabel} {nextStepReason} ) : ( {t("workspaceNothingOutstanding")} )} {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} ))} {t("workspaceRecentActivity")} {overview.recentActivity.length === 0 ? ( {t("workspaceNoActivity")} ) : ( {overview.recentActivity.map((a, i) => ( {a.type}{a.detail ? ` — ${a.detail}` : ""} {new Date(a.at).toLocaleDateString(language === "nb" ? "nb-NO" : "en")} ))} )} ); } function OverviewDetails({ jobId, overview, onReload, onEdit }: { jobId: number; overview: WorkspaceOverview; onReload: () => void; onEdit: () => void }) { const { t } = useI18n(); const panels = [ { id: "details", title: t("workspaceJobDetails"), content: }, { id: "tasks", title: t("workspaceChecklist"), content: }, { id: "calendar", title: t("calendarTitle"), content: }, { id: "timeline", title: t("workspaceActivityHistory"), content: }, { id: "documents", title: t("workspaceDocuments"), content: }, { id: "communication", title: t("workspaceCommunication"), content: }, ]; return ( {panels.map((panel) => ( } aria-controls={`${panel.id}-content`} id={`${panel.id}-header`}> {panel.title} {panel.content} ))} ); } function workspaceSectionLabel(t: (key: any) => string, section: WorkspaceSectionKey): string { const keys: Record = { overview: "workspaceOverview", analysis: "workspaceAnalysis", cv: "workspaceCv", "cover-letter": "workspaceCoverLetter", interview: "workspaceInterviewPrep", }; return t(keys[section]); } function JobDetailsSection({ overview, onEdit }: { overview: WorkspaceOverview | null; onEdit: () => void }) { const { language, t } = useI18n(); if (!overview) return ; const rows: [string, string][] = [ [t("company"), overview.company ?? "—"], [t("location"), overview.location ?? "—"], [t("workspaceCountry"), overview.countryCode ?? "—"], [t("workspaceSource"), overview.source ?? "—"], [t("jobDetailsSalary"), overview.salary ?? "—"], [t("addJobModalStatus"), statusLabel(t, overview.status)], [t("workspaceDiscovered"), overview.savedAt ? new Date(overview.savedAt).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—"], [t("workspaceApplied"), overview.dateApplied ? new Date(overview.dateApplied).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—"], [t("jobDetailsDeadline"), overview.deadline ? new Date(overview.deadline).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—"], [t("intelligenceCategoryFollowUp"), overview.followUpAt ? new Date(overview.followUpAt).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : t("workspaceNotScheduled")], [t("workspaceNextAction"), overview.nextAction ?? "—"], ]; return ( {t("workspaceApplicationInfo")} {rows.map(([k, v]) => ( {k} {v} ))} {overview.tags.length > 0 ? ( {overview.tags.map((tag) => )} ) : null} {overview.notes ? ( {t("workspaceNotes")} {overview.notes} ) : null} {t("workspaceJobDescription")} {!overview.hasJobDescription ? ( {t("workspaceAddAdvert")}}> {t("workspaceNoJobDescription")} ) : ( {overview.translatedDescription ? ( {t("workspaceTranslatedAdvert")} {overview.translatedDescription} ) : null} {overview.description ? ( {overview.translatedDescription ? {t("workspaceOriginalAdvert")}{overview.descriptionLanguage ? ` · ${overview.descriptionLanguage.toUpperCase()}` : ""} : null} {overview.description} ) : null} )} ); }