import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Box, Button, Checkbox, Chip, LinearProgress, Menu, MenuItem, Paper, Stack, Typography, } from "@mui/material"; import { alpha, useTheme } from "@mui/material/styles"; import TuneIcon from "@mui/icons-material/Tune"; import TrendingUpIcon from "@mui/icons-material/TrendingUp"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import BusinessOutlinedIcon from "@mui/icons-material/BusinessOutlined"; import AutoGraphIcon from "@mui/icons-material/AutoGraph"; import { api } from "../api"; import { getUserKeyFromToken } from "../themePrefs"; import { useI18n } from "../i18n/I18nProvider"; import { buildWorkflowPath, getWorkflowAction } from "../jobWorkflowSignals"; import { JobApplication } from "../types"; interface JobStats { total: number; active: number; deleted: number; byStatus: Record; appliedLast30Days: number; averageDaysSinceApplied: number; } type ReminderJob = JobApplication; type AnalyticsPoint = { month: string; applied: number; responses: number }; type TagPoint = { tag: string; count: number }; type OverviewAnalytics = { funnel: { label: string; count: number }[]; responseRateBySource: { label: string; total: number; responses: number; rate: number }[]; topCompanies: { companyId: number; company: string; count: number; responses: number; responseRate: number }[]; medianDaysToFirstResponse?: number | null; totalResponses: number; totalActive: number; }; type TagTrendResponse = { months: string[]; series: { tag: string; counts: number[] }[] }; type Prefs = { cards: boolean; activity: boolean; funnel: boolean; companies: boolean; skills: boolean; }; function prefsKey() { return `dashboardPrefs:${getUserKeyFromToken()}`; } function loadPrefs(): Prefs { try { const raw = window.localStorage.getItem(prefsKey()); if (!raw) return { cards: true, activity: true, funnel: true, companies: true, skills: true }; return { cards: true, activity: true, funnel: true, companies: true, skills: true, ...JSON.parse(raw) }; } catch { return { cards: true, activity: true, funnel: true, companies: true, skills: true }; } } function savePrefs(next: Prefs) { window.localStorage.setItem(prefsKey(), JSON.stringify(next)); } function clamp(n: number, a: number, b: number) { return Math.max(a, Math.min(b, n)); } function buildLinePath(values: number[], width: number, height: number) { if (!values.length) return ""; const min = Math.min(...values); const max = Math.max(...values); const step = width / Math.max(1, values.length - 1); const yFor = (value: number) => { const t = max === min ? 0.5 : (value - min) / (max - min); return height - t * height; }; return values .map((value, index) => `${index === 0 ? "M" : "L"} ${Math.round(index * step)} ${Math.round(yFor(value))}`) .join(" "); } function MiniSpark({ values, color }: { values: number[]; color: string }) { const width = 180; const height = 52; const path = buildLinePath(values, width, height); return ( ); } function SectionCard({ children, sx = {} }: { children: React.ReactNode; sx?: any }) { return ( {children} ); } export default function DashboardView() { const theme = useTheme(); const navigate = useNavigate(); const { t } = useI18n(); const [stats, setStats] = useState(null); const [overview, setOverview] = useState(null); const [tagTrends, setTagTrends] = useState(null); const [analytics, setAnalytics] = useState([]); const [tags, setTags] = useState([]); const [months, setMonths] = useState<6 | 12 | 24>(12); const [reminderJobs, setReminderJobs] = useState([]); const [prefs, setPrefs] = useState(() => loadPrefs()); const [prefsAnchor, setPrefsAnchor] = useState(null); useEffect(() => { api.get("/jobapplications/stats").then((r) => setStats(r.data)); api.get("/jobapplications/analytics-overview").then((r) => setOverview(r.data)).catch(() => setOverview(null)); api.get("/jobapplications/reminders", { params: { upcomingDays: 14 } }).then((r) => setReminderJobs(Array.isArray(r.data) ? r.data : [])).catch(() => setReminderJobs([])); }, []); useEffect(() => { const params = { months }; api.get("/jobapplications/analytics", { params }).then((r) => setAnalytics(r.data ?? [])).catch(() => setAnalytics([])); api.get("/jobapplications/tags", { params: { limit: 10, ...params } }).then((r) => setTags(r.data ?? [])).catch(() => setTags([])); api.get("/jobapplications/tag-trends", { params: { months, limit: 5 } }).then((r) => setTagTrends(r.data)).catch(() => setTagTrends(null)); }, [months]); const appliedValues = analytics.map((x) => x.applied); const responseValues = analytics.map((x) => x.responses); const chartWidth = 860; const chartHeight = 250; const appliedPath = buildLinePath(appliedValues, chartWidth, chartHeight); const responsePath = buildLinePath(responseValues, chartWidth, chartHeight); const tagColors = [theme.palette.primary.main, theme.palette.success.main, theme.palette.warning.main, theme.palette.info.main, theme.palette.error.main]; const funnelMax = overview?.funnel?.length ? Math.max(...overview.funnel.map((item) => item.count)) : 0; const topSource = overview?.responseRateBySource?.[0]; const missingCvCount = reminderJobs.filter((job) => job.workflowSignal?.hasPackageGap).length; const metricCards = [ { label: t("dashboardActiveApplications"), value: stats?.active ?? 0, sub: t("dashboardCurrentlyInProgress"), icon: , tone: theme.palette.primary.main, spark: appliedValues, }, { label: t("dashboardApplied30Days"), value: stats?.appliedLast30Days ?? 0, sub: t("dashboardNewApplications"), icon: , tone: theme.palette.success.main, spark: appliedValues.slice(-6), }, { label: t("dashboardMedianFirstResponse"), value: overview?.medianDaysToFirstResponse ?? "—", sub: t("dashboardDaysUntilFirstReply"), icon: , tone: theme.palette.info.main, spark: responseValues, }, { label: t("dashboardResponsesLogged"), value: overview?.totalResponses ?? 0, sub: t("dashboardAcrossActiveJobs"), icon: , tone: theme.palette.warning.main, spark: responseValues.slice(-6), }, ]; const togglePref = (key: keyof Prefs) => { const next = { ...prefs, [key]: !prefs[key] }; setPrefs(next); savePrefs(next); }; const totalApplied = appliedValues.reduce((sum, value) => sum + value, 0); const totalResponses = responseValues.reduce((sum, value) => sum + value, 0); const responseRate = totalApplied > 0 ? Math.round((totalResponses / totalApplied) * 100) : 0; const priorityJobs = reminderJobs.slice(0, 5); const getReminderAction = (job: ReminderJob) => getWorkflowAction(job, { packageWork: t("jobTablePackageWork"), followUp: t("jobTableFollowUp"), interviewPrep: t("jobTableInterviewStage"), readiness: t("jobTableReadiness"), }); const openReminderJob = (job: ReminderJob) => { navigate(buildWorkflowPath(job)); }; return ( {t("dashboardHeroLabel")} {t("dashboardOverviewTitle")} {t("dashboardOverviewBody")} {([6, 12, 24] as const).map((m) => ( ))} setPrefsAnchor(null)}> {[ ["cards", t("dashboardSummaryCards")], ["activity", t("dashboardActivityChart")], ["funnel", t("dashboardConversionFunnel")], ["companies", t("dashboardTopCompanies")], ["skills", t("dashboardSkillsInsights")], ].map(([key, label]) => ( togglePref(key as keyof Prefs)}> {label} ))} {prefs.cards ? ( {metricCards.map((card) => ( {card.label} {card.value} {card.sub} {card.icon} ))} ) : null} {prefs.activity ? ( {t("dashboardApplicationActivity")} {t("dashboardMonthlyApplicationsResponses")} {[0.2, 0.4, 0.6, 0.8].map((tick) => ( ))} {responsePath ? : null} {appliedPath ? : null} {analytics.map((point) => ( {point.month.slice(5)} ))} ) : null} {t("dashboardConversionFunnelTitle")} {t("dashboardResponseSources")} {(overview?.funnel ?? []).map((item) => { const width = funnelMax ? clamp((item.count / funnelMax) * 100, 0, 100) : 0; return ( {item.label} {item.count} ); })} {topSource?.label ?? t("dashboardResponseSources")} {topSource ? `${topSource.rate}%` : "—"} {topSource ? t("dashboardResponseConversion", { responses: topSource.responses, total: topSource.total }) : t("dashboardNoSourceData")} {t("remindersTitle")} {t("remindersSubtitle")} {priorityJobs.length === 0 ? ( {t("remindersNothing")} ) : ( {priorityJobs.map((job) => { const action = getReminderAction(job); return ( {job.company?.name ?? t("jobTableCompany")} • {job.jobTitle} {action?.detail ?? job.workflowSignal?.reason ?? job.followUpReason ?? t("remindersFollowUpLabel")} )})} )} {prefs.companies ? ( {t("dashboardTopCompaniesByActivity")} {(overview?.topCompanies ?? []).map((item, index) => ( {item.company} {t("dashboardCompanyJobsResponses", { jobs: item.count, responses: item.responses })} = 50 ? "success" : item.responseRate >= 25 ? "warning" : "default"} variant="outlined" /> ))} ) : null} {prefs.skills ? ( {t("dashboardTopSkills")} {tags.length === 0 ? ( {t("dashboardNoTagsYet")} ) : ( {tags.slice(0, 8).map((tag, index) => { const max = Math.max(...tags.map((item) => item.count), 1); const width = (tag.count / max) * 100; return ( {tag.tag} {tag.count} ); })} )} {t("dashboardSkillTrends")} {!tagTrends || tagTrends.series.length === 0 ? ( {t("dashboardNoTagTrendData")} ) : ( {tagTrends.series.map((series, index) => ( {series.tag} {series.counts.reduce((sum, value) => sum + value, 0)} total {series.counts.map((count, i) => ( 0 ? alpha(tagColors[index % tagColors.length], 0.22 + Math.min(0.6, count / 10)) : alpha(theme.palette.text.primary, 0.05), }} title={`${tagTrends.months[i]}: ${count}`} /> ))} ))} )} ) : null} ); }