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 (
} onClick={close}>{t("workspaceBack")}
{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}
}
onClick={() => overview.nextStep?.section && onGo(overview.nextStep.section as WorkspaceSectionKey)}>
{nextStepLabel}
) : (
{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")}
} onClick={onEdit}>{t("workspaceEdit")}
{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}
)}
);
}