fix: repair frontend production build regressions
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
test('test harness is configured', () => {
|
test('test harness is configured', () => {
|
||||||
expect(true).toBe(true);
|
expect(true).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|||||||
@@ -394,10 +394,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mt: 1, flexWrap: "wrap" }}>
|
<Box sx={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mt: 1, flexWrap: "wrap" }}>
|
||||||
<FormControlLabel
|
<FormControlLabel control={<Checkbox checked={saveAndAddAnother} onChange={(e) => setSaveAndAddAnother(e.target.checked)} />} label="Save and add another" />
|
||||||
control={<Checkbox checked={saveAndAddAnother} onChange={(e) => setSaveAndAddAnother(e.target.checked)} />}
|
|
||||||
label="Save and add another"
|
|
||||||
/>
|
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
<Button variant="outlined" onClick={onClose}>Cancel</Button>
|
<Button variant="outlined" onClick={onClose}>Cancel</Button>
|
||||||
<Button variant="contained" onClick={() => void createJob()} disabled={saving || !canSave}>
|
<Button variant="contained" onClick={() => void createJob()} disabled={saving || !canSave}>
|
||||||
@@ -410,12 +407,3 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
veAndAddAnother ? "Save and continue" : "Add job"}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ interface JobStats {
|
|||||||
averageDaysSinceApplied: number;
|
averageDaysSinceApplied: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReminderJob = {
|
||||||
|
id: number;
|
||||||
|
tailoredCvText?: string | null;
|
||||||
|
followUpReason?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
type AnalyticsPoint = { month: string; applied: number; responses: number };
|
type AnalyticsPoint = { month: string; applied: number; responses: number };
|
||||||
type TagPoint = { tag: string; count: number };
|
type TagPoint = { tag: string; count: number };
|
||||||
type SummarizerMetrics = {
|
type SummarizerMetrics = {
|
||||||
@@ -126,12 +132,14 @@ export default function DashboardView() {
|
|||||||
const [analytics, setAnalytics] = useState<AnalyticsPoint[]>([]);
|
const [analytics, setAnalytics] = useState<AnalyticsPoint[]>([]);
|
||||||
const [tags, setTags] = useState<TagPoint[]>([]);
|
const [tags, setTags] = useState<TagPoint[]>([]);
|
||||||
const [summarizerMetrics, setSummarizerMetrics] = useState<SummarizerMetrics | null>(null);
|
const [summarizerMetrics, setSummarizerMetrics] = useState<SummarizerMetrics | null>(null);
|
||||||
|
const [reminderJobs, setReminderJobs] = useState<ReminderJob[]>([]);
|
||||||
const [prefs, setPrefs] = useState<Prefs>(() => loadPrefs());
|
const [prefs, setPrefs] = useState<Prefs>(() => loadPrefs());
|
||||||
const [prefsAnchor, setPrefsAnchor] = useState<HTMLElement | null>(null);
|
const [prefsAnchor, setPrefsAnchor] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get<JobStats>("/jobapplications/stats").then((r) => setStats(r.data));
|
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<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([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -430,16 +438,3 @@ export default function DashboardView() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
cent summarizer errors recorded."}</Typography>
|
|
||||||
</Paper>
|
|
||||||
</Paper>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ interface JobApplication {
|
|||||||
followUpReason?: string | null;
|
followUpReason?: string | null;
|
||||||
shortSummary?: string | null;
|
shortSummary?: string | null;
|
||||||
fullSummary?: string | null;
|
fullSummary?: string | null;
|
||||||
|
tailoredCvText?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PagedResult<T> {
|
interface PagedResult<T> {
|
||||||
@@ -140,6 +141,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
const [locationFilter, setLocationFilter] = useState("");
|
const [locationFilter, setLocationFilter] = useState("");
|
||||||
const debouncedLocation = useDebouncedValue(locationFilter, 250);
|
const debouncedLocation = useDebouncedValue(locationFilter, 250);
|
||||||
const [needsFollowUpOnly, setNeedsFollowUpOnly] = useState(false);
|
const [needsFollowUpOnly, setNeedsFollowUpOnly] = useState(false);
|
||||||
|
const [readinessFilter, setReadinessFilter] = useState<"all" | "needs-work" | "interview">("all");
|
||||||
const { companies } = useCompanies();
|
const { companies } = useCompanies();
|
||||||
const [companyFilterId, setCompanyFilterId] = useState<number | "All">("All");
|
const [companyFilterId, setCompanyFilterId] = useState<number | "All">("All");
|
||||||
const [detailsJobId, setDetailsJobId] = useState<number | null>(null);
|
const [detailsJobId, setDetailsJobId] = useState<number | null>(null);
|
||||||
@@ -196,10 +198,16 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
setExpanded((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
setExpanded((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedAllOnPage = jobs.length > 0 && jobs.every((job) => selectedIds.includes(job.id));
|
const filteredJobs = useMemo(() => {
|
||||||
|
if (readinessFilter === "all") return jobs;
|
||||||
|
if (readinessFilter === "interview") return jobs.filter((job) => job.status === "Interview" || job.status === "Interviewing");
|
||||||
|
return jobs.filter((job) => !job.tailoredCvText || !job.notes);
|
||||||
|
}, [jobs, readinessFilter]);
|
||||||
|
|
||||||
|
const selectedAllOnPage = filteredJobs.length > 0 && filteredJobs.every((job) => selectedIds.includes(job.id));
|
||||||
|
|
||||||
const toggleSelectAll = (checked: boolean) => {
|
const toggleSelectAll = (checked: boolean) => {
|
||||||
setSelectedIds(checked ? jobs.map((job) => job.id) : []);
|
setSelectedIds(checked ? filteredJobs.map((job) => job.id) : []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelected = (id: number, checked: boolean) => {
|
const toggleSelected = (id: number, checked: boolean) => {
|
||||||
@@ -274,15 +282,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: 2, alignItems: "center", justifyContent: "space-between", mt: 2, flexWrap: "wrap" }}>
|
||||||
<TextField
|
<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" }} />
|
||||||
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" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControl sx={{ minWidth: 160 }} size="small">
|
<FormControl sx={{ minWidth: 160 }} size="small">
|
||||||
<InputLabel>Status</InputLabel>
|
<InputLabel>Status</InputLabel>
|
||||||
@@ -303,6 +303,16 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
<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="Needs follow-up" /> : 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>
|
||||||
|
</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="Show deleted" /> : 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); }} />
|
<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="Columns"><IconButton onClick={(e) => setColumnsAnchor(e.currentTarget)}><ViewColumnIcon /></IconButton></Tooltip>
|
||||||
@@ -391,7 +401,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{jobs.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" }}>No jobs found.</Typography></TableCell></TableRow> : null}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</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]} />
|
<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]} />
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type NavItem = {
|
|||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
section?: string;
|
section?: string;
|
||||||
|
badgeCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function initialsFrom(s?: string) {
|
function initialsFrom(s?: string) {
|
||||||
|
|||||||
@@ -54,9 +54,14 @@ function formatBytes(bytes?: number | null) {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayMetadata(value?: string | null) {
|
||||||
|
return value && value.trim().length > 0 ? value : "-";
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminSystemPage() {
|
export default function AdminSystemPage() {
|
||||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [runningProbe, setRunningProbe] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -193,13 +198,3 @@ export default function AdminSystemPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.summarizer.lastError}</Alert> : null}
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tus.summarizer.lastError}</Alert> : null}
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type UserDto = {
|
|||||||
|
|
||||||
export default function AdminUsersPage() {
|
export default function AdminUsersPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { confirmAction } = useDialogActions();
|
||||||
const [users, setUsers] = useState<UserDto[]>([]);
|
const [users, setUsers] = useState<UserDto[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -122,21 +123,9 @@ export default function AdminUsersPage() {
|
|||||||
Send a quick delivery check using the configured SMTP settings. Leave the recipient blank to use your admin email.
|
Send a quick delivery check using the configured SMTP settings. Leave the recipient blank to use your admin email.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||||
<TextField
|
<TextField label="Recipient email" value={testEmailTo} onChange={(e) => setTestEmailTo(e.target.value)} placeholder="Uses your admin email if left blank" />
|
||||||
label="Recipient email"
|
|
||||||
value={testEmailTo}
|
|
||||||
onChange={(e) => setTestEmailTo(e.target.value)}
|
|
||||||
placeholder="Uses your admin email if left blank"
|
|
||||||
/>
|
|
||||||
<TextField label="Subject" value={testEmailSubject} onChange={(e) => setTestEmailSubject(e.target.value)} />
|
<TextField label="Subject" value={testEmailSubject} onChange={(e) => setTestEmailSubject(e.target.value)} />
|
||||||
<TextField
|
<TextField label="Message" multiline minRows={3} value={testEmailMessage} onChange={(e) => setTestEmailMessage(e.target.value)} sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} />
|
||||||
label="Message"
|
|
||||||
multiline
|
|
||||||
minRows={3}
|
|
||||||
value={testEmailMessage}
|
|
||||||
onChange={(e) => setTestEmailMessage(e.target.value)}
|
|
||||||
sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5 }}>
|
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5 }}>
|
||||||
<Button variant="contained" disabled={sendingTestEmail} onClick={() => void sendTestEmail()}>
|
<Button variant="contained" disabled={sendingTestEmail} onClick={() => void sendTestEmail()}>
|
||||||
@@ -212,28 +201,6 @@ export default function AdminUsersPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{!loading && users.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7}>
|
|
||||||
<Typography sx={{ color: "text.secondary", py: 2, textAlign: "center" }}>No users.</Typography>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : null}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
<Button size="small" color="error" variant="outlined" onClick={() => void remove(u)}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{!loading && users.length === 0 ? (
|
{!loading && users.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5}>
|
<TableCell colSpan={5}>
|
||||||
|
|||||||
@@ -32,9 +32,12 @@ export interface JobApplication {
|
|||||||
deadline?: string;
|
deadline?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverLetterText?: string;
|
coverLetterText?: string;
|
||||||
|
recruiterMessageDraft?: string | null;
|
||||||
jobUrl?: string;
|
jobUrl?: string;
|
||||||
shortSummary?: string;
|
shortSummary?: string;
|
||||||
fullSummary?: string | null;
|
fullSummary?: string | null;
|
||||||
|
tailoredCvText?: string | null;
|
||||||
|
tailoredCvUpdatedAt?: string | null;
|
||||||
|
|
||||||
hasResume?: boolean;
|
hasResume?: boolean;
|
||||||
hasCoverLetter?: boolean;
|
hasCoverLetter?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user