refactor, security updates, cv extraction upgrades
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
@@ -22,10 +22,12 @@ import BusinessOutlinedIcon from "@mui/icons-material/BusinessOutlined";
|
||||
import AutoGraphIcon from "@mui/icons-material/AutoGraph";
|
||||
|
||||
import { api } from "../api";
|
||||
import ViewStateNotice from "./ViewStateNotice";
|
||||
import { getUserKeyFromToken } from "../themePrefs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { buildWorkflowPath, getWorkflowAction } from "../jobWorkflowSignals";
|
||||
import { JobApplication } from "../types";
|
||||
import { useViewResource } from "../hooks/useViewResource";
|
||||
|
||||
interface JobStats {
|
||||
total: number;
|
||||
@@ -130,28 +132,58 @@ export default function DashboardView() {
|
||||
const isMobile = useMediaQuery("(max-width:767.95px)");
|
||||
const navigate = useNavigate();
|
||||
const { t } = useI18n();
|
||||
const [stats, setStats] = useState<JobStats | null>(null);
|
||||
const [overview, setOverview] = useState<OverviewAnalytics | null>(null);
|
||||
const [tagTrends, setTagTrends] = useState<TagTrendResponse | null>(null);
|
||||
const [analytics, setAnalytics] = useState<AnalyticsPoint[]>([]);
|
||||
const [tags, setTags] = useState<TagPoint[]>([]);
|
||||
const [months, setMonths] = useState<6 | 12 | 24>(12);
|
||||
const [reminderJobs, setReminderJobs] = useState<ReminderJob[]>([]);
|
||||
const [prefs, setPrefs] = useState<Prefs>(() => loadPrefs());
|
||||
const [prefsAnchor, setPrefsAnchor] = useState<HTMLElement | null>(null);
|
||||
const summaryResource = useViewResource(
|
||||
async () => {
|
||||
const [statsResponse, overviewResponse, remindersResponse] = await Promise.all([
|
||||
api.get<JobStats>("/jobapplications/stats"),
|
||||
api.get<OverviewAnalytics>("/jobapplications/analytics-overview"),
|
||||
api.get<ReminderJob[]>("/jobapplications/reminders", { params: { upcomingDays: 14 } }),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
api.get<JobStats>("/jobapplications/stats").then((r) => setStats(r.data));
|
||||
api.get<OverviewAnalytics>("/jobapplications/analytics-overview").then((r) => setOverview(r.data)).catch(() => setOverview(null));
|
||||
api.get<ReminderJob[]>("/jobapplications/reminders", { params: { upcomingDays: 14 } }).then((r) => setReminderJobs(Array.isArray(r.data) ? r.data : [])).catch(() => setReminderJobs([]));
|
||||
}, []);
|
||||
return {
|
||||
stats: statsResponse.data,
|
||||
overview: overviewResponse.data,
|
||||
reminderJobs: Array.isArray(remindersResponse.data) ? remindersResponse.data : [],
|
||||
};
|
||||
},
|
||||
{
|
||||
initialData: { stats: null as JobStats | null, overview: null as OverviewAnalytics | null, reminderJobs: [] as ReminderJob[] },
|
||||
errorMessage: "Unable to load dashboard summary data right now.",
|
||||
deps: [],
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const params = { months };
|
||||
api.get<AnalyticsPoint[]>("/jobapplications/analytics", { params }).then((r) => setAnalytics(r.data ?? [])).catch(() => setAnalytics([]));
|
||||
api.get<TagPoint[]>("/jobapplications/tags", { params: { limit: 10, ...params } }).then((r) => setTags(r.data ?? [])).catch(() => setTags([]));
|
||||
api.get<TagTrendResponse>("/jobapplications/tag-trends", { params: { months, limit: 5 } }).then((r) => setTagTrends(r.data)).catch(() => setTagTrends(null));
|
||||
}, [months]);
|
||||
const trendsResource = useViewResource(
|
||||
async () => {
|
||||
const params = { months };
|
||||
const [analyticsResponse, tagsResponse, trendsResponse] = await Promise.all([
|
||||
api.get<AnalyticsPoint[]>("/jobapplications/analytics", { params }),
|
||||
api.get<TagPoint[]>("/jobapplications/tags", { params: { limit: 10, ...params } }),
|
||||
api.get<TagTrendResponse>("/jobapplications/tag-trends", { params: { months, limit: 5 } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
analytics: analyticsResponse.data ?? [],
|
||||
tags: tagsResponse.data ?? [],
|
||||
tagTrends: trendsResponse.data,
|
||||
};
|
||||
},
|
||||
{
|
||||
initialData: { analytics: [] as AnalyticsPoint[], tags: [] as TagPoint[], tagTrends: null as TagTrendResponse | null },
|
||||
errorMessage: "Unable to load dashboard trends right now.",
|
||||
deps: [months],
|
||||
},
|
||||
);
|
||||
|
||||
const stats = summaryResource.data.stats;
|
||||
const overview = summaryResource.data.overview;
|
||||
const reminderJobs = summaryResource.data.reminderJobs;
|
||||
const analytics = trendsResource.data.analytics;
|
||||
const tags = trendsResource.data.tags;
|
||||
const tagTrends = trendsResource.data.tagTrends;
|
||||
|
||||
const appliedValues = analytics.map((x) => x.applied);
|
||||
const responseValues = analytics.map((x) => x.responses);
|
||||
@@ -299,7 +331,23 @@ export default function DashboardView() {
|
||||
</Box>
|
||||
</SectionCard>
|
||||
|
||||
{prefs.cards ? (
|
||||
<ViewStateNotice
|
||||
loading={summaryResource.loading}
|
||||
error={summaryResource.error}
|
||||
title="Unable to load dashboard summary"
|
||||
description="The dashboard summary is unavailable right now."
|
||||
onRetry={summaryResource.reload}
|
||||
/>
|
||||
<ViewStateNotice
|
||||
loading={trendsResource.loading}
|
||||
error={trendsResource.error}
|
||||
title="Unable to load dashboard trends"
|
||||
description="Charts and trend panels could not reach the API."
|
||||
onRetry={trendsResource.reload}
|
||||
compact
|
||||
/>
|
||||
|
||||
{!summaryResource.loading && !summaryResource.error && prefs.cards ? (
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(2, 1fr)", xl: "repeat(4, 1fr)" }, gap: 2, mt: 2 }}>
|
||||
{metricCards.map((card) => (
|
||||
<SectionCard key={card.label}>
|
||||
@@ -322,7 +370,7 @@ export default function DashboardView() {
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", xl: "minmax(0, 1.8fr) minmax(320px, 0.9fr)" }, gap: 2, mt: 2 }}>
|
||||
{prefs.activity ? (
|
||||
{!summaryResource.loading && !summaryResource.error && prefs.activity ? (
|
||||
<SectionCard>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "flex-start" }}>
|
||||
<Box>
|
||||
@@ -364,7 +412,8 @@ export default function DashboardView() {
|
||||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
<SectionCard>
|
||||
{!summaryResource.loading && !summaryResource.error ? (
|
||||
<SectionCard>
|
||||
<Typography variant="h6" sx={{ fontWeight: 950 }}>{t("dashboardConversionFunnelTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("dashboardResponseSources")}</Typography>
|
||||
<Stack spacing={1.2}>
|
||||
@@ -402,9 +451,11 @@ export default function DashboardView() {
|
||||
</Typography>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", xl: "1.15fr 0.85fr" }, gap: 2, mt: 2 }}>
|
||||
{!summaryResource.loading && !summaryResource.error ? (
|
||||
<SectionCard>
|
||||
<Typography variant="h6" sx={{ fontWeight: 950, mb: 1 }}>{t("remindersTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("remindersSubtitle")}</Typography>
|
||||
@@ -432,8 +483,9 @@ export default function DashboardView() {
|
||||
<Button variant="text" onClick={() => navigate('/reminders')}>{t("reminders")}</Button>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
{prefs.companies ? (
|
||||
{!summaryResource.loading && !summaryResource.error && prefs.companies ? (
|
||||
<SectionCard>
|
||||
<Typography variant="h6" sx={{ fontWeight: 950, mb: 1 }}>{t("dashboardTopCompaniesByActivity")}</Typography>
|
||||
<Stack spacing={1.25}>
|
||||
@@ -452,7 +504,7 @@ export default function DashboardView() {
|
||||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
{prefs.skills ? (
|
||||
{!trendsResource.loading && !trendsResource.error && prefs.skills ? (
|
||||
<SectionCard>
|
||||
<Typography variant="h6" sx={{ fontWeight: 950, mb: 1 }}>{t("dashboardTopSkills")}</Typography>
|
||||
{tags.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user