Polish UI, harden company creation, and add error pages
This commit is contained in:
@@ -46,6 +46,7 @@ import EditJobDialog from "./EditJobDialog";
|
||||
import { useToast } from "../toast";
|
||||
import SavedViewsMenu, { SavedViewParams } from "./SavedViewsMenu";
|
||||
import { useDialogActions } from "../dialogs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
interface JobApplication {
|
||||
id: number;
|
||||
@@ -125,6 +126,7 @@ function statusTone(status: string): string {
|
||||
export default function JobTable({ refreshToken, pageSize, onPageSizeChange, columns, onColumnsChange, mode = "jobs" }: Props) {
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { confirmAction } = useDialogActions();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -218,39 +220,39 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
if (jobsToDelete.length === 0) return false;
|
||||
if (jobsToDelete.length === 1) {
|
||||
const job = jobsToDelete[0];
|
||||
return confirmAction(`Move "${job.jobTitle}" at ${job.company?.name ?? "this company"} to trash?`, { title: "Move job to trash", confirmLabel: "Move", destructive: true });
|
||||
return confirmAction(t("jobTableMoveOneConfirm", { title: job.jobTitle, company: job.company?.name ?? t("jobTableCompany") }), { title: t("jobTableMoveToTrashTitle"), confirmLabel: t("jobTableMove"), destructive: true });
|
||||
}
|
||||
return confirmAction(`Move ${jobsToDelete.length} selected jobs to trash?`, { title: "Move jobs to trash", confirmLabel: "Move", destructive: true });
|
||||
return confirmAction(t("jobTableMoveManyConfirm", { count: jobsToDelete.length }), { title: t("jobTableMoveJobsToTrashTitle"), confirmLabel: t("jobTableMove"), destructive: true });
|
||||
};
|
||||
|
||||
const softDelete = async (job: JobApplication) => {
|
||||
if (!(await confirmDelete([job]))) return;
|
||||
try {
|
||||
await api.delete(`/jobapplications/${job.id}`);
|
||||
toast("Job moved to trash.", "success", { label: "Undo", onClick: () => { void restore(job.id); } });
|
||||
toast(t("jobTableMovedToTrash"), "success", { label: "Undo", onClick: () => { void restore(job.id); } });
|
||||
setReloadToken((t) => t + 1);
|
||||
} catch {
|
||||
toast("Failed to delete job.", "error");
|
||||
toast(t("jobTableDeleteFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const restore = async (id: number) => {
|
||||
try {
|
||||
await api.post(`/jobapplications/${id}/restore`);
|
||||
toast("Job restored.", "success");
|
||||
toast(t("jobTableRestored"), "success");
|
||||
setReloadToken((t) => t + 1);
|
||||
} catch {
|
||||
toast("Failed to restore job.", "error");
|
||||
toast(t("jobTableRestoreFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const setStatusQuick = async (id: number, status: string) => {
|
||||
try {
|
||||
await api.patch(`/jobapplications/${id}/status`, { status });
|
||||
toast(`Status set to ${status}.`, "success");
|
||||
toast(t("jobTableStatusSet", { status }), "success");
|
||||
setReloadToken((t) => t + 1);
|
||||
} catch {
|
||||
toast("Failed to update status.", "error");
|
||||
toast(t("jobTableStatusUpdateFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,11 +266,11 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
if (action === "restore") return api.post(`/jobapplications/${id}/restore`);
|
||||
return api.patch(`/jobapplications/${id}/status`, { status: value });
|
||||
}));
|
||||
toast(`Updated ${selectedIds.length} jobs.`, "success");
|
||||
toast(t("jobTableUpdatedJobs", { count: selectedIds.length }), "success");
|
||||
setReloadToken((t) => t + 1);
|
||||
setSelectedIds([]);
|
||||
} catch {
|
||||
toast("Bulk action failed.", "error");
|
||||
toast(t("jobTableBulkActionFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -282,55 +284,55 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
||||
<TextField label="Search" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} placeholder="Title, company, notes, messages" size="small" InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }} sx={{ minWidth: 320, flex: "1 1 320px" }} />
|
||||
<TextField label={t("jobTableSearch")} value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} placeholder={t("jobTableSearchPlaceholder")} size="small" InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }} sx={{ minWidth: 320, flex: "1 1 320px" }} />
|
||||
|
||||
<FormControl sx={{ minWidth: 160 }} size="small">
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select value={statusFilter} label="Status" onChange={(e) => { setStatusFilter(e.target.value); setPage(0); }}>
|
||||
{["All", "Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"].map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
|
||||
<InputLabel>{t("jobTableStatus")}</InputLabel>
|
||||
<Select value={statusFilter} label={t("jobTableStatus")} onChange={(e) => { setStatusFilter(e.target.value); setPage(0); }}>
|
||||
{[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>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ minWidth: 220 }} size="small">
|
||||
<InputLabel>Company</InputLabel>
|
||||
<Select value={companyFilterId} label="Company" onChange={(e) => { setCompanyFilterId(e.target.value as any); setPage(0); }}>
|
||||
<MenuItem value="All">All</MenuItem>
|
||||
<InputLabel>{t("jobTableCompany")}</InputLabel>
|
||||
<Select value={companyFilterId} label={t("jobTableCompany")} onChange={(e) => { setCompanyFilterId(e.target.value as any); setPage(0); }}>
|
||||
<MenuItem value="All">{t("jobTableAll")}</MenuItem>
|
||||
{companies.map((c) => <MenuItem key={c.id} value={c.id}>{c.name}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField label="Location" value={locationFilter} onChange={(e) => { setLocationFilter(e.target.value); setPage(0); }} sx={{ minWidth: 200, flex: "1 1 200px" }} />
|
||||
<TextField label={t("jobTableLocation")} value={locationFilter} onChange={(e) => { setLocationFilter(e.target.value); setPage(0); }} sx={{ minWidth: 200, flex: "1 1 200px" }} />
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
{mode === "jobs" ? <FormControlLabel control={<Checkbox checked={needsFollowUpOnly} onChange={(e) => { setNeedsFollowUpOnly(e.target.checked); setPage(0); }} />} label="Needs follow-up" /> : null}
|
||||
{mode === "jobs" ? <FormControlLabel control={<Checkbox checked={needsFollowUpOnly} onChange={(e) => { setNeedsFollowUpOnly(e.target.checked); setPage(0); }} />} label={t("jobTableNeedsFollowUp")} /> : null}
|
||||
{mode === "jobs" ? (
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Readiness</InputLabel>
|
||||
<Select value={readinessFilter} label="Readiness" onChange={(e) => setReadinessFilter(e.target.value as any)}>
|
||||
<MenuItem value="all">All readiness</MenuItem>
|
||||
<MenuItem value="needs-work">Needs work</MenuItem>
|
||||
<MenuItem value="interview">Interview stage</MenuItem>
|
||||
<InputLabel>{t("jobTableReadiness")}</InputLabel>
|
||||
<Select value={readinessFilter} label={t("jobTableReadiness")} onChange={(e) => setReadinessFilter(e.target.value as any)}>
|
||||
<MenuItem value="all">{t("jobTableAllReadiness")}</MenuItem>
|
||||
<MenuItem value="needs-work">{t("jobTableNeedsWork")}</MenuItem>
|
||||
<MenuItem value="interview">{t("jobTableInterviewStage")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
{mode === "jobs" ? <FormControlLabel control={<Checkbox checked={includeDeleted} onChange={(e) => { setIncludeDeleted(e.target.checked); setPage(0); }} />} label="Show deleted" /> : null}
|
||||
{mode === "jobs" ? <FormControlLabel control={<Checkbox checked={includeDeleted} onChange={(e) => { setIncludeDeleted(e.target.checked); setPage(0); }} />} label={t("jobTableShowDeleted")} /> : null}
|
||||
<SavedViewsMenu current={{ q: search.trim() || undefined, status: statusFilter !== "All" ? statusFilter : undefined, companyId: companyFilterId === "All" ? undefined : (companyFilterId as number), location: locationFilter.trim() || undefined, needsFollowUp: needsFollowUpOnly ? true : undefined }} onApply={(p: SavedViewParams) => { setSearch(p.q ?? ""); setStatusFilter(p.status ?? "All"); setCompanyFilterId(p.companyId ?? "All"); setLocationFilter(p.location ?? ""); setNeedsFollowUpOnly(Boolean(p.needsFollowUp)); setPage(0); }} />
|
||||
<Tooltip title="Columns"><IconButton onClick={(e) => setColumnsAnchor(e.currentTarget)}><ViewColumnIcon /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableColumns")}><IconButton onClick={(e) => setColumnsAnchor(e.currentTarget)}><ViewColumnIcon /></IconButton></Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{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 }}>{selectedIds.length} selected</Typography>
|
||||
<Typography sx={{ fontWeight: 800 }}>{t("jobTableSelected", { count: selectedIds.length })}</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
{mode === "trash" ? <Button variant="outlined" onClick={() => void runBulkAction("restore")}>Restore selected</Button> : <Button variant="outlined" color="error" onClick={() => void runBulkAction("delete")}>Delete selected</Button>}
|
||||
{mode === "trash" ? <Button variant="outlined" onClick={() => void runBulkAction("restore")}>{t("jobTableRestoreSelected")}</Button> : <Button variant="outlined" color="error" onClick={() => void runBulkAction("delete")}>{t("jobTableDeleteSelected")}</Button>}
|
||||
{mode === "jobs" ? ["Waiting", "Interview", "Rejected", "Ghosted", "Offer"].map((s) => <Button key={s} variant="outlined" onClick={() => void runBulkAction("status", s)}>{s}</Button>) : null}
|
||||
</Box>
|
||||
</Paper>
|
||||
) : null}
|
||||
|
||||
<Menu anchorEl={columnsAnchor} open={Boolean(columnsAnchor)} onClose={() => setColumnsAnchor(null)}>
|
||||
{([ ["status", "Status"], ["dateApplied", "Date applied"], ["daysSince", "Days"], ["jobUrl", "Job URL"] ] as const).map(([key, label]) => (
|
||||
{([ ["status", t("settingsColumnStatus")], ["dateApplied", t("settingsColumnDateApplied")], ["daysSince", t("settingsColumnDays")], ["jobUrl", t("settingsColumnJobUrl")] ] as const).map(([key, label]) => (
|
||||
<MenuItem key={key} onClick={() => onColumnsChange({ ...columns, [key]: !columns[key] })}>
|
||||
<Checkbox checked={columns[key]} />
|
||||
{label}
|
||||
@@ -344,13 +346,13 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox"><Checkbox checked={selectedAllOnPage} indeterminate={selectedIds.length > 0 && !selectedAllOnPage} onChange={(e) => toggleSelectAll(e.target.checked)} /></TableCell>
|
||||
<TableCell width={1} />
|
||||
<TableCell sortDirection={sortBy === "company" ? sortDir : false}><TableSortLabel active={sortBy === "company"} direction={sortBy === "company" ? sortDir : "asc"} onClick={() => requestSort("company")}>Company</TableSortLabel></TableCell>
|
||||
<TableCell sortDirection={sortBy === "jobTitle" ? sortDir : false}><TableSortLabel active={sortBy === "jobTitle"} direction={sortBy === "jobTitle" ? sortDir : "asc"} onClick={() => requestSort("jobTitle")}>Role</TableSortLabel></TableCell>
|
||||
{columns.status ? <TableCell sortDirection={sortBy === "status" ? sortDir : false}><TableSortLabel active={sortBy === "status"} direction={sortBy === "status" ? sortDir : "asc"} onClick={() => requestSort("status")}>Status</TableSortLabel></TableCell> : null}
|
||||
{columns.dateApplied ? <TableCell sortDirection={sortBy === "dateApplied" ? sortDir : false}><TableSortLabel active={sortBy === "dateApplied"} direction={sortBy === "dateApplied" ? sortDir : "asc"} onClick={() => requestSort("dateApplied")}>Date Applied</TableSortLabel></TableCell> : null}
|
||||
{columns.daysSince ? <TableCell sortDirection={sortBy === "daysSince" ? sortDir : false}><TableSortLabel active={sortBy === "daysSince"} direction={sortBy === "daysSince" ? sortDir : "asc"} onClick={() => requestSort("daysSince")}>Days</TableSortLabel></TableCell> : null}
|
||||
{columns.jobUrl ? <TableCell>Job URL</TableCell> : null}
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
<TableCell sortDirection={sortBy === "company" ? sortDir : false}><TableSortLabel active={sortBy === "company"} direction={sortBy === "company" ? sortDir : "asc"} onClick={() => requestSort("company")}>{t("jobTableCompany")}</TableSortLabel></TableCell>
|
||||
<TableCell sortDirection={sortBy === "jobTitle" ? sortDir : false}><TableSortLabel active={sortBy === "jobTitle"} direction={sortBy === "jobTitle" ? sortDir : "asc"} onClick={() => requestSort("jobTitle")}>{t("jobTableRole")}</TableSortLabel></TableCell>
|
||||
{columns.status ? <TableCell sortDirection={sortBy === "status" ? sortDir : false}><TableSortLabel active={sortBy === "status"} direction={sortBy === "status" ? sortDir : "asc"} onClick={() => requestSort("status")}>{t("jobTableStatus")}</TableSortLabel></TableCell> : null}
|
||||
{columns.dateApplied ? <TableCell sortDirection={sortBy === "dateApplied" ? sortDir : false}><TableSortLabel active={sortBy === "dateApplied"} direction={sortBy === "dateApplied" ? sortDir : "asc"} onClick={() => requestSort("dateApplied")}>{t("jobTableDateApplied")}</TableSortLabel></TableCell> : null}
|
||||
{columns.daysSince ? <TableCell sortDirection={sortBy === "daysSince" ? sortDir : false}><TableSortLabel active={sortBy === "daysSince"} direction={sortBy === "daysSince" ? sortDir : "asc"} onClick={() => requestSort("daysSince")}>{t("jobTableDays")}</TableSortLabel></TableCell> : null}
|
||||
{columns.jobUrl ? <TableCell>{t("settingsColumnJobUrl")}</TableCell> : null}
|
||||
<TableCell align="right">{t("jobTableActions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -367,21 +369,21 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<span>{job.jobTitle}</span>
|
||||
{job.needsFollowUp ? <Chip size="small" label="Follow up" title={job.followUpReason ?? undefined} sx={{ fontWeight: 800 }} /> : null}
|
||||
{!job.tailoredCvText && !job.isDeleted ? <Chip size="small" label="CV missing" color="warning" variant="outlined" /> : null}
|
||||
{job.tailoredCvText ? <Chip size="small" label="CV ready" color="success" variant="outlined" /> : null}
|
||||
{job.needsFollowUp ? <Chip size="small" label={t("jobTableFollowUp")} title={job.followUpReason ?? undefined} sx={{ fontWeight: 800 }} /> : null}
|
||||
{!job.tailoredCvText && !job.isDeleted ? <Chip size="small" label={t("jobTableCvMissing")} color="warning" variant="outlined" /> : null}
|
||||
{job.tailoredCvText ? <Chip size="small" label={t("jobTableCvReady")} color="success" variant="outlined" /> : null}
|
||||
</Box>
|
||||
</TableCell>
|
||||
{columns.status ? <TableCell><Chip label={normalizeStatus(job.status)} size="small" color={toneName as any} /></TableCell> : null}
|
||||
{columns.dateApplied ? <TableCell>{new Date(job.dateApplied).toLocaleDateString()}</TableCell> : null}
|
||||
{columns.daysSince ? <TableCell>{job.daysSince}</TableCell> : null}
|
||||
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">Link</a> : ""}</TableCell> : null}
|
||||
{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 }}>
|
||||
<Tooltip title="Edit"><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Quick status"><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title="Open"><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title="Restore"><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title="Soft delete"><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton size="small" onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableOpen")}><IconButton size="small" onClick={() => setDetailsJobId(job.id)}><LaunchIcon fontSize="small" /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -389,11 +391,11 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<TableCell sx={{ py: 0 }} colSpan={columns.status && columns.dateApplied && columns.daysSince && columns.jobUrl ? 9 : 8}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ p: 2, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
|
||||
<Box><Typography variant="overline">Location</Typography><Typography>{job.location ?? "-"}</Typography></Box>
|
||||
<Box><Typography variant="overline">Salary</Typography><Typography>{job.salary ?? "-"}</Typography></Box>
|
||||
<Box><Typography variant="overline">Job URL</Typography><Typography>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">Open listing</a> : "-"}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">Skills</Typography><Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>{parseTags(job.tags).length ? parseTags(job.tags).slice(0, 8).map((tag) => <Chip key={tag} label={tag} size="small" />) : <Typography sx={{ color: "text.secondary" }}>No tags</Typography>}</Box></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">Overview</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{generateOverview(job) || "No summary yet."}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("jobTableLocation")}</Typography><Typography>{job.location ?? "-"}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("addJobModalSalary")}</Typography><Typography>{job.salary ?? "-"}</Typography></Box>
|
||||
<Box><Typography variant="overline">{t("settingsColumnJobUrl")}</Typography><Typography>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableOpenListing")}</a> : "-"}</Typography></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">{t("jobTableSkills")}</Typography><Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 0.5 }}>{parseTags(job.tags).length ? parseTags(job.tags).slice(0, 8).map((tag) => <Chip key={tag} label={tag} size="small" />) : <Typography sx={{ color: "text.secondary" }}>{t("jobTableNoTags")}</Typography>}</Box></Box>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}><Typography variant="overline">{t("jobTableOverview")}</Typography><Typography sx={{ whiteSpace: "pre-wrap" }}>{generateOverview(job) || t("jobTableNoSummaryYet")}</Typography></Box>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
@@ -401,7 +403,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{filteredJobs.length === 0 ? <TableRow><TableCell colSpan={9}><Typography sx={{ py: 2, textAlign: "center" }}>No jobs found.</Typography></TableCell></TableRow> : null}
|
||||
{filteredJobs.length === 0 ? <TableRow><TableCell colSpan={9}><Typography sx={{ py: 2, textAlign: "center" }}>{t("jobTableNoJobsFound")}</Typography></TableCell></TableRow> : null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination component="div" count={total} page={page} onPageChange={(_, next) => setPage(next)} rowsPerPage={pageSize} onRowsPerPageChange={(e) => { onPageSizeChange(Number(e.target.value) as 15 | 20 | 25); setPage(0); }} rowsPerPageOptions={[15, 20, 25]} />
|
||||
@@ -410,7 +412,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
<JobDetailsDialog open={detailsJobId !== null} jobId={detailsJobId} onClose={() => setDetailsJobId(null)} />
|
||||
<EditJobDialog open={editJobId !== null} jobId={editJobId} onClose={() => setEditJobId(null)} onSaved={() => setReloadToken((t) => t + 1)} />
|
||||
<Menu anchorEl={statusAnchor} open={Boolean(statusAnchor)} onClose={() => { setStatusAnchor(null); setStatusJobId(null); }}>
|
||||
{["Waiting", "Interview", "Offer", "Rejected", "Ghosted"].map((s) => <MenuItem key={s} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, s); setStatusAnchor(null); setStatusJobId(null); }}>Set {s}</MenuItem>)}
|
||||
{(["Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const).map((s) => <MenuItem key={s} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, s); setStatusAnchor(null); setStatusJobId(null); }}>{t("jobTableSetStatus", { status: s })}</MenuItem>)}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user