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
+42 -10
View File
@@ -41,6 +41,7 @@ import ViewColumnIcon from "@mui/icons-material/ViewColumn";
import SearchIcon from "@mui/icons-material/Search";
import { api } from "../api";
import ViewStateNotice from "./ViewStateNotice";
import { useCompanies } from "../hooks/useCompanies";
import { useDebouncedValue } from "../hooks/useDebouncedValue";
import JobDetailsDialog from "./JobDetailsDialog";
@@ -50,6 +51,7 @@ import SavedViewsMenu, { SavedViewParams } from "./SavedViewsMenu";
import { useDialogActions } from "../dialogs";
import { useI18n } from "../i18n/I18nProvider";
import { JobApplication } from "../types";
import { useViewResource } from "../hooks/useViewResource";
import { getWorkflowAction, needsInterviewPrep, needsWorkflowWork } from "../jobWorkflowSignals";
interface PagedResult<T> {
@@ -127,7 +129,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
const debouncedLocation = useDebouncedValue(locationFilter, 250);
const [needsFollowUpOnly, setNeedsFollowUpOnly] = useState(false);
const [readinessFilter, setReadinessFilter] = useState<"all" | "needs-work" | "interview">("all");
const { companies } = useCompanies();
const { companies, error: companiesError, reload: reloadCompanies } = useCompanies();
const [companyFilterId, setCompanyFilterId] = useState<number | "All">("All");
const [detailsJobId, setDetailsJobId] = useState<number | null>(null);
const [detailsInitialTab, setDetailsInitialTab] = useState(0);
@@ -153,13 +155,25 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
needsFollowUp: needsFollowUpOnly ? true : undefined,
}), [page, pageSize, debouncedSearch, statusFilter, companyFilterId, debouncedLocation, includeDeleted, mode, sortBy, sortDir, needsFollowUpOnly]);
const jobsResource = useViewResource(
async () => {
const r = await api.get<PagedResult<JobApplication>>("/jobapplications", { params });
return r.data;
},
{
initialData: { items: [], total: 0, page: 1, pageSize },
errorMessage: "Unable to load jobs right now.",
deps: [params, refreshToken, reloadToken, pageSize],
},
);
useEffect(() => {
api.get<PagedResult<JobApplication>>("/jobapplications", { params }).then((r) => {
setJobs(r.data.items);
setTotal(r.data.total);
setJobs(jobsResource.data.items);
setTotal(jobsResource.data.total);
if (!jobsResource.error) {
setSelectedIds([]);
});
}, [params, refreshToken, reloadToken]);
}
}, [jobsResource.data, jobsResource.error]);
useEffect(() => {
const paramsSearch = new URLSearchParams(location.search);
@@ -460,6 +474,22 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
))}
</Menu>
<ViewStateNotice
error={jobsResource.error}
title={mode === "trash" ? "Unable to load trash" : "Unable to load jobs"}
description={mode === "trash" ? "The deleted-jobs view cannot reach the API right now." : "The jobs list cannot reach the API right now."}
onRetry={jobsResource.reload}
/>
{companiesError ? (
<ViewStateNotice
error={companiesError}
title="Unable to load company filters"
description="Company filter data is unavailable right now."
onRetry={reloadCompanies}
compact
/>
) : null}
<Paper sx={{ mt: 2, overflow: "hidden" }}>
{isMobile ? (
<Stack spacing={1.25} sx={{ p: 1.25 }}>
@@ -467,7 +497,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
<FormControlLabel control={<Checkbox checked={selectedAllOnPage} indeterminate={selectedIds.length > 0 && !selectedAllOnPage} onChange={(e) => toggleSelectAll(e.target.checked)} />} label={t("jobTableSelectAll")} sx={{ mr: 0 }} />
</Box>
{filteredJobs.map((job) => {
{jobsResource.loading ? <Typography sx={{ py: 2, textAlign: "center" }}>{t("loading")}</Typography> : null}
{!jobsResource.loading && !jobsResource.error && filteredJobs.map((job) => {
const toneName = statusTone(job.status);
const primaryAction = getPrimaryAction(job);
const actionSignals = getActionSignals(job);
@@ -596,7 +627,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
</Paper>
);
})}
{filteredJobs.length === 0 ? <Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography> : null}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? <Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography> : null}
</Stack>
) : (
<Box sx={{ overflowX: "auto" }}>
@@ -615,7 +646,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
</TableRow>
</TableHead>
<TableBody>
{filteredJobs.map((job) => {
{jobsResource.loading ? <TableRow><TableCell colSpan={visibleDesktopColumns}><Typography sx={{ py: 2, textAlign: "center" }}>{t("loading")}</Typography></TableCell></TableRow> : null}
{!jobsResource.loading && !jobsResource.error && filteredJobs.map((job) => {
const open = expanded.includes(job.id);
const toneName = statusTone(job.status);
const tone = toneName === "error" ? theme.palette.error.main : toneName === "warning" ? theme.palette.warning.main : toneName === "success" ? theme.palette.success.main : toneName === "info" ? theme.palette.info.main : theme.palette.primary.main;
@@ -690,7 +722,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
</React.Fragment>
);
})}
{filteredJobs.length === 0 ? <TableRow><TableCell colSpan={visibleDesktopColumns}><Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography></TableCell></TableRow> : null}
{filteredJobs.length === 0 && !jobsResource.loading && !jobsResource.error ? <TableRow><TableCell colSpan={visibleDesktopColumns}><Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography></TableCell></TableRow> : null}
</TableBody>
</Table>
</Box>