feat(jobs): redesign table and mobile cards with preserved workflows
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import RestoreFromTrashOutlinedIcon from "@mui/icons-material/RestoreFromTrashOutlined";
|
||||
@@ -111,7 +111,7 @@ function queryCompany(search: string): number | "All" {
|
||||
|
||||
function queryStatus(search: string): string {
|
||||
const value = new URLSearchParams(search).get("status");
|
||||
return value && JOB_STATUS_FILTERS.includes(value) ? value : "All";
|
||||
return value || "All";
|
||||
}
|
||||
|
||||
function queryReadiness(search: string): ReadinessFilter {
|
||||
@@ -168,15 +168,15 @@ function isInteractiveTarget(target: EventTarget | null): boolean {
|
||||
}
|
||||
|
||||
export default function JobTable({ refreshToken, pageSize, onPageSizeChange, columns, onColumnsChange, mode = "jobs" }: Props) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery("(max-width:767.95px)");
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { t, language } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const listRouteRef = useRef(`${location.pathname}${location.search}`);
|
||||
const restoredFocusJobIdRef = useRef<number | null>(null);
|
||||
const retainedSelectionAfterReloadRef = useRef<number[] | null>(null);
|
||||
const [jobs, setJobs] = useState<JobApplication[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(() => queryPage(location.search));
|
||||
@@ -197,11 +197,13 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const [editJobId, setEditJobId] = useState<number | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const [statusAnchor, setStatusAnchor] = useState<null | HTMLElement>(null);
|
||||
const [bulkStatusAnchor, setBulkStatusAnchor] = useState<null | HTMLElement>(null);
|
||||
const [statusJobId, setStatusJobId] = useState<number | null>(null);
|
||||
const [sortBy, setSortBy] = useState<JobSortKey>(() => querySort(location.search));
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">(() => new URLSearchParams(location.search).get("sortDir") === "asc" ? "asc" : "desc");
|
||||
|
||||
const updateListRoute = useCallback((updates: Record<string, string | null>) => {
|
||||
setSelectedIds([]);
|
||||
const next = new URLSearchParams(location.search);
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (value) next.set(key, value);
|
||||
@@ -293,6 +295,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
};
|
||||
|
||||
const openJob = useCallback((jobId: number, path?: string) => {
|
||||
// Keep the originating row on the list history entry so browser Back can restore focus.
|
||||
window.history.replaceState({ ...window.history.state, jobListFocusId: jobId }, "");
|
||||
navigate(path ?? `/jobs/${jobId}`, { state: { from: listRouteRef.current, focusJobId: jobId } });
|
||||
}, [navigate]);
|
||||
|
||||
@@ -327,7 +331,8 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
setJobs(jobsResource.data.items);
|
||||
setTotal(jobsResource.data.total);
|
||||
if (!jobsResource.error) {
|
||||
setSelectedIds([]);
|
||||
setSelectedIds(retainedSelectionAfterReloadRef.current?.filter((id) => jobsResource.data.items.some((job) => job.id === id)) ?? []);
|
||||
retainedSelectionAfterReloadRef.current = null;
|
||||
}
|
||||
}, [jobsResource.data, jobsResource.error]);
|
||||
|
||||
@@ -342,7 +347,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const filteredJobs = jobs;
|
||||
|
||||
useEffect(() => {
|
||||
const focusJobId = (location.state as { focusJobId?: unknown } | null)?.focusJobId;
|
||||
const focusJobId = window.history.state?.jobListFocusId ?? (location.state as { focusJobId?: unknown } | null)?.focusJobId;
|
||||
if (typeof focusJobId !== "number" || restoredFocusJobIdRef.current === focusJobId || jobsResource.loading) return;
|
||||
const row = document.querySelector<HTMLElement>(`[data-job-row-id="${focusJobId}"]`);
|
||||
if (!row) return;
|
||||
@@ -401,7 +406,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const setStatusQuick = async (id: number, status: string) => {
|
||||
try {
|
||||
await api.patch(`/jobapplications/${id}/status`, { status });
|
||||
toast(t("jobTableStatusSet", { status }), "success");
|
||||
toast(t("jobTableStatusSet", { status: statusLabel(t, status) }), "success");
|
||||
setReloadToken((token) => token + 1);
|
||||
} catch {
|
||||
toast(t("jobTableStatusUpdateFailed"), "error");
|
||||
@@ -412,17 +417,17 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
if (selectedIds.length === 0) return;
|
||||
const selectedJobs = jobs.filter((job) => selectedIdSet.has(job.id));
|
||||
if (action === "delete" && !(await confirmDelete(selectedJobs))) return;
|
||||
try {
|
||||
await Promise.all(selectedIds.map((id) => {
|
||||
const results = await Promise.allSettled(selectedIds.map((id) => {
|
||||
if (action === "delete") return api.delete(`/jobapplications/${id}`);
|
||||
if (action === "restore") return api.post(`/jobapplications/${id}/restore`);
|
||||
return api.patch(`/jobapplications/${id}/status`, { status: value });
|
||||
}));
|
||||
toast(t("jobTableUpdatedJobs", { count: selectedIds.length }), "success");
|
||||
}));
|
||||
const failedIds = selectedIds.filter((_, index) => results[index].status === "rejected");
|
||||
if (failedIds.length) toast(t("jobTableBulkActionFailed"), "error");
|
||||
else toast(t("jobTableUpdatedJobs", { count: selectedIds.length }), "success");
|
||||
if (failedIds.length !== selectedIds.length) {
|
||||
retainedSelectionAfterReloadRef.current = failedIds;
|
||||
setReloadToken((token) => token + 1);
|
||||
setSelectedIds([]);
|
||||
} catch {
|
||||
toast(t("jobTableBulkActionFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,10 +456,10 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
toneName: statusTone(job.status),
|
||||
tags: parseTags(job.tags),
|
||||
actionSignals: actionSignal ? [actionSignal] : [],
|
||||
appliedDateLabel: job.dateApplied ? new Date(job.dateApplied).toLocaleDateString() : "—",
|
||||
appliedDateLabel: job.dateApplied ? new Date(job.dateApplied).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—",
|
||||
isSelected: selectedIdSet.has(job.id),
|
||||
};
|
||||
}), [buildWorkflowActionSignal, filteredJobs, selectedIdSet]);
|
||||
}), [buildWorkflowActionSignal, filteredJobs, selectedIdSet, language]);
|
||||
|
||||
const statusOptions = ["Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const;
|
||||
const visibleDesktopColumns = 6 + Number(columns.status) + Number(columns.dateApplied) + Number(columns.daysSince) + Number(columns.jobUrl);
|
||||
@@ -472,7 +477,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
return (
|
||||
<Box>
|
||||
{isMobile ? (
|
||||
<Paper sx={{ mt: 2, p: 1.25, borderRadius: 4 }}>
|
||||
<Paper variant="outlined" sx={{ mt: 2, p: 1.25, borderRadius: 2.5, boxShadow: "none" }}>
|
||||
<Stack spacing={1.1}>
|
||||
<TextField
|
||||
label={t("jobTableSearch")}
|
||||
@@ -488,7 +493,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel id="job-table-status-label">{t("jobTableStatus")}</InputLabel>
|
||||
<Select labelId="job-table-status-label" value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
|
||||
{[t("jobTableAll"), t("statusApplied"), t("statusWaiting"), t("statusInterview"), t("statusOffer"), t("statusRejected"), t("statusGhosted")].map((s) => <MenuItem key={s} value={s === t("jobTableAll") ? "All" : s === t("statusApplied") ? "Applied" : s === t("statusWaiting") ? "Waiting" : s === t("statusInterview") ? "Interview" : s === t("statusOffer") ? "Offer" : s === t("statusRejected") ? "Rejected" : "Ghosted"}>{s}</MenuItem>)}
|
||||
<MenuItem value="All">{t("jobTableAll")}</MenuItem>
|
||||
{JOB_STATUS_FILTERS.map((status) => <MenuItem key={status} value={status}>{statusLabel(t, status)}</MenuItem>)}
|
||||
{statusFilter !== "All" && !JOB_STATUS_FILTERS.includes(statusFilter) ? <MenuItem value={statusFilter}>{statusFilter}</MenuItem> : null}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -530,10 +537,10 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
gap: 0.25,
|
||||
px: 0.25,
|
||||
py: 0.5,
|
||||
borderRadius: 3,
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.03),
|
||||
borderRadius: 2,
|
||||
backgroundColor: "action.hover",
|
||||
border: "1px solid",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.1),
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<FormControlLabel control={<Checkbox checked={needsFollowUpOnly} onChange={(e) => changeNeedsFollowUp(e.target.checked)} />} label={t("jobTableNeedsFollowUp")} sx={{ mr: 0, ml: -0.5 }} />
|
||||
@@ -553,7 +560,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
||||
<Paper variant="outlined" sx={{ display: "flex", gap: 1.5, alignItems: "center", mt: 2, p: 1.5, flexWrap: "wrap", borderRadius: 2.5, boxShadow: "none" }}>
|
||||
<TextField
|
||||
label={t("jobTableSearch")}
|
||||
value={search}
|
||||
@@ -567,7 +574,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<FormControl sx={{ width: { xs: "100%", sm: 180 } }} size="small">
|
||||
<InputLabel id="job-table-status-label">{t("jobTableStatus")}</InputLabel>
|
||||
<Select labelId="job-table-status-label" value={statusFilter} label={t("jobTableStatus")} onChange={(e) => changeStatus(e.target.value)}>
|
||||
{[t("jobTableAll"), t("statusApplied"), t("statusWaiting"), t("statusInterview"), t("statusOffer"), t("statusRejected"), t("statusGhosted")].map((s) => <MenuItem key={s} value={s === t("jobTableAll") ? "All" : s === t("statusApplied") ? "Applied" : s === t("statusWaiting") ? "Waiting" : s === t("statusInterview") ? "Interview" : s === t("statusOffer") ? "Offer" : s === t("statusRejected") ? "Rejected" : "Ghosted"}>{s}</MenuItem>)}
|
||||
<MenuItem value="All">{t("jobTableAll")}</MenuItem>
|
||||
{JOB_STATUS_FILTERS.map((status) => <MenuItem key={status} value={status}>{statusLabel(t, status)}</MenuItem>)}
|
||||
{statusFilter !== "All" && !JOB_STATUS_FILTERS.includes(statusFilter) ? <MenuItem value={statusFilter}>{statusFilter}</MenuItem> : null}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -604,18 +613,22 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
{!isMobile ? <Tooltip title={t("jobTableColumns")}><IconButton aria-label={t("jobTableColumns")} onClick={(e) => setColumnsAnchor(e.currentTarget)}><ViewColumnIcon /></IconButton></Tooltip> : null}
|
||||
{densityControl}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{selectedIds.length > 0 ? (
|
||||
<Paper sx={{ mt: 2, p: 1.5, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
|
||||
<Typography sx={{ fontWeight: 800 }}>{t("jobTableSelected", { count: selectedIds.length })}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", width: { xs: "100%", sm: "auto" } }}>
|
||||
<Paper variant="outlined" sx={{ mt: 1.5, p: 1.5, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1.5, flexWrap: "wrap", borderColor: "primary.main", borderRadius: 2.5, boxShadow: "none" }}>
|
||||
<Typography sx={{ fontWeight: 700 }}>{t("jobTableSelected", { count: selectedIds.length })}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", width: { xs: "100%", sm: "auto" }, "& .MuiButton-root": { minHeight: isMobile ? 44 : undefined } }}>
|
||||
{mode === "trash" ? <Button variant="outlined" onClick={() => void runBulkAction("restore")} sx={{ width: { xs: "100%", sm: "auto" } }}>{t("jobTableRestoreSelected")}</Button> : <Button variant="outlined" color="error" onClick={() => void runBulkAction("delete")} sx={{ width: { xs: "100%", sm: "auto" } }}>{t("jobTableDeleteSelected")}</Button>}
|
||||
{mode === "jobs" ? statusOptions.map((status) => <Button key={status} variant="outlined" onClick={() => void runBulkAction("status", status)} sx={{ width: { xs: "100%", sm: "auto" } }}>{status}</Button>) : null}
|
||||
{mode === "jobs" && isMobile ? <Button variant="outlined" onClick={(event) => setBulkStatusAnchor(event.currentTarget)} sx={{ width: "100%" }}>{t("jobTableStatus")}</Button> : null}
|
||||
{mode === "jobs" && !isMobile ? statusOptions.map((status) => <Button key={status} variant="outlined" onClick={() => void runBulkAction("status", status)}>{statusLabel(t, status)}</Button>) : null}
|
||||
</Box>
|
||||
</Paper>
|
||||
) : null}
|
||||
<Menu anchorEl={bulkStatusAnchor} open={Boolean(bulkStatusAnchor)} onClose={() => setBulkStatusAnchor(null)} MenuListProps={{ "aria-label": t("jobTableStatus") }}>
|
||||
{statusOptions.map((status) => <MenuItem key={status} onClick={() => { setBulkStatusAnchor(null); void runBulkAction("status", status); }}>{statusLabel(t, status)}</MenuItem>)}
|
||||
</Menu>
|
||||
|
||||
<Menu anchorEl={columnsAnchor} open={Boolean(columnsAnchor)} onClose={() => setColumnsAnchor(null)}>
|
||||
{([ ["status", t("settingsColumnStatus")], ["dateApplied", t("settingsColumnDateApplied")], ["daysSince", t("settingsColumnDays")], ["jobUrl", t("settingsColumnJobUrl")] ] as const).map(([key, label]) => (
|
||||
@@ -645,11 +658,12 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<Paper
|
||||
data-content-density={density}
|
||||
sx={{
|
||||
mt: 2,
|
||||
mt: 1.5,
|
||||
overflow: "hidden",
|
||||
borderRadius: 4,
|
||||
border: "none",
|
||||
boxShadow: "var(--customShadows-section, none)",
|
||||
borderRadius: 2.5,
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
@@ -677,13 +691,14 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
openJob(job.id);
|
||||
}
|
||||
}}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: "var(--app-list-item-padding)",
|
||||
borderRadius: 3.5,
|
||||
borderRadius: 2,
|
||||
cursor: mode === "jobs" && !job.isDeleted ? "pointer" : "default",
|
||||
backgroundColor: isSelected ? "action.selected" : alpha(theme.palette.primary.main, 0.03),
|
||||
borderColor: alpha(theme.palette.primary.main, 0.08),
|
||||
boxShadow: `0 10px 24px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.18 : 0.06)}`,
|
||||
backgroundColor: isSelected ? "action.selected" : "background.paper",
|
||||
borderColor: "divider",
|
||||
boxShadow: "none",
|
||||
"&:hover": mode === "jobs" && !job.isDeleted ? { backgroundColor: "action.hover" } : undefined,
|
||||
"&:focus-visible": { outline: "3px solid", outlineColor: "primary.main", outlineOffset: 2 },
|
||||
}}
|
||||
@@ -696,12 +711,12 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>
|
||||
{job.company?.name ?? t("jobTableCompany")}
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 900, fontSize: 21, lineHeight: 1.1, letterSpacing: -0.4, textWrap: "balance", overflowWrap: "anywhere" }}>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: 16, lineHeight: 1.35, overflowWrap: "anywhere" }}>
|
||||
{job.jobTitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
{columns.status ? <Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} sx={{ fontWeight: 800, maxWidth: "100%", height: "auto", minHeight: 24, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }} /> : null}
|
||||
{columns.status ? <Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} variant="outlined" sx={{ fontWeight: 700, maxWidth: "100%", height: "auto", minHeight: 24, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }} /> : null}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
|
||||
@@ -756,7 +771,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1, "& .MuiButton-root": { minHeight: 44, whiteSpace: "normal", lineHeight: 1.25 } }}>
|
||||
<Button variant="outlined" startIcon={<EditOutlinedIcon />} onClick={() => setEditJobId(job.id)} sx={{ minHeight: 42, fontWeight: 700 }}>
|
||||
{t("jobTableEdit")}
|
||||
</Button>
|
||||
@@ -801,7 +816,6 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TableBody>
|
||||
{jobsResource.loading ? <TableRow><TableCell colSpan={visibleDesktopColumns}><Typography sx={{ py: 2, textAlign: "center" }}>{t("loading")}</Typography></TableCell></TableRow> : null}
|
||||
{!jobsResource.loading && !jobsResource.error && rowModels.map(({ job, toneName, actionSignals, appliedDateLabel, isSelected }) => {
|
||||
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;
|
||||
return (
|
||||
<TableRow
|
||||
key={job.id}
|
||||
@@ -821,15 +835,16 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
}}
|
||||
sx={{
|
||||
cursor: mode === "jobs" && !job.isDeleted ? "pointer" : "default",
|
||||
backgroundColor: alpha(tone, theme.palette.mode === "dark" ? 0.1 : 0.06),
|
||||
backgroundColor: isSelected ? "action.selected" : "background.paper",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
"&:focus-visible": { outline: "3px solid", outlineColor: "primary.main", outlineOffset: -3 },
|
||||
}}
|
||||
>
|
||||
<TableCell padding="checkbox"><Checkbox inputProps={{ "aria-label": t("jobTableSelectJob", { title: job.jobTitle }) }} checked={isSelected} onChange={(e) => toggleSelected(job.id, e.target.checked)} /></TableCell>
|
||||
<TableCell sx={{ minWidth: 140, fontWeight: 700 }}>{job.company?.name ?? ""}</TableCell>
|
||||
<TableCell sx={{ minWidth: 140, color: "text.secondary", fontWeight: 600 }}>{job.company?.name ?? ""}</TableCell>
|
||||
<TableCell sx={{ minWidth: 260 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<Typography component="span" sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{job.jobTitle}</Typography>
|
||||
<Typography component="span" sx={{ fontWeight: 700, overflowWrap: "anywhere" }}>{job.jobTitle}</Typography>
|
||||
{actionSignals.map((signal) => (
|
||||
<Chip
|
||||
key={`${job.id}-${signal.label}-${signal.detail}`}
|
||||
@@ -838,7 +853,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
color={signal.color}
|
||||
variant={signal.variant === "contained" ? "filled" : "outlined"}
|
||||
title={signal.detail}
|
||||
sx={{ fontWeight: 800, cursor: "pointer" }}
|
||||
sx={{ fontWeight: 700, cursor: "pointer" }}
|
||||
clickable
|
||||
onClick={signal.onClick}
|
||||
aria-label={t("jobTableWorkflowSignal", { title: job.jobTitle, signal: signal.label })}
|
||||
@@ -847,10 +862,10 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 130 }}>{job.location || "—"}</TableCell>
|
||||
{columns.status ? <TableCell><Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} /></TableCell> : null}
|
||||
{columns.status ? <TableCell><Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} variant="outlined" sx={{ fontWeight: 600 }} /></TableCell> : null}
|
||||
{columns.dateApplied ? <TableCell>{appliedDateLabel}</TableCell> : null}
|
||||
{columns.daysSince ? <TableCell>{job.daysSince ?? "—"}</TableCell> : null}
|
||||
<TableCell>{job.deadline ? new Date(job.deadline).toLocaleDateString() : "—"}</TableCell>
|
||||
<TableCell>{job.deadline ? new Date(job.deadline).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—"}</TableCell>
|
||||
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableLink")}</a> : ""}</TableCell> : null}
|
||||
<TableCell align="right">
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5, whiteSpace: "nowrap" }}>
|
||||
@@ -874,7 +889,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
|
||||
<EditJobDialog open={editJobId !== null} jobId={editJobId} onClose={() => setEditJobId(null)} onSaved={() => setReloadToken((token) => token + 1)} />
|
||||
<Menu anchorEl={statusAnchor} open={Boolean(statusAnchor)} onClose={() => { setStatusAnchor(null); setStatusJobId(null); }}>
|
||||
{statusOptions.map((status) => <MenuItem key={status} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, status); setStatusAnchor(null); setStatusJobId(null); }}>{t("jobTableSetStatus", { status })}</MenuItem>)}
|
||||
{statusOptions.map((status) => <MenuItem key={status} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, status); setStatusAnchor(null); setStatusJobId(null); }}>{t("jobTableSetStatus", { status: statusLabel(t, status) })}</MenuItem>)}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user