refactor, security updates, cv extraction upgrades

This commit is contained in:
2026-04-11 01:34:32 +02:00
parent 806b200ac5
commit 27fd70a2d7
59 changed files with 6817 additions and 1561 deletions
+75 -23
View File
@@ -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 ? (