feat(discovery): schedule saved-search alerts
This commit is contained in:
@@ -275,7 +275,9 @@ export const translations = {
|
||||
jobDiscoveryViewListing: "View listing",
|
||||
jobDiscoverySaveTracker: "Save to tracker",
|
||||
jobDiscoverySavedSearches: "Saved searches",
|
||||
jobDiscoverySavedSearchesHelp: "Run a saved NAV search to see which vacancies are genuinely new since your last check.",
|
||||
jobDiscoverySavedSearchesHelp: "Saved NAV searches are checked automatically. You will be notified when new vacancies appear.",
|
||||
jobDiscoveryAutomaticAlerts: "Automatic alerts",
|
||||
jobDiscoveryUpdateAlertsFailed: "The alert setting could not be updated. Try again.",
|
||||
jobDiscoverySavedName: "Search name",
|
||||
jobDiscoverySavedDefaultName: "Recent vacancies",
|
||||
jobDiscoverySaveSearch: "Save search",
|
||||
@@ -2675,7 +2677,9 @@ export const translations = {
|
||||
jobDiscoveryViewListing: "Vis stilling",
|
||||
jobDiscoverySaveTracker: "Lagre i oversikten",
|
||||
jobDiscoverySavedSearches: "Lagrede søk",
|
||||
jobDiscoverySavedSearchesHelp: "Kjør et lagret NAV-søk for å se hvilke stillinger som faktisk er nye siden sist.",
|
||||
jobDiscoverySavedSearchesHelp: "Lagrede NAV-søk kontrolleres automatisk. Du får et varsel når nye stillinger dukker opp.",
|
||||
jobDiscoveryAutomaticAlerts: "Automatiske varsler",
|
||||
jobDiscoveryUpdateAlertsFailed: "Varslingsinnstillingen kunne ikke oppdateres. Prøv igjen.",
|
||||
jobDiscoverySavedName: "Navn på søket",
|
||||
jobDiscoverySavedDefaultName: "Nylige stillinger",
|
||||
jobDiscoverySaveSearch: "Lagre søk",
|
||||
|
||||
@@ -93,3 +93,17 @@ test("runs a saved search, marks unseen vacancies, and dismisses them", async ()
|
||||
await waitFor(() => expect(screen.queryByText("Platform Engineer")).not.toBeInTheDocument());
|
||||
expect(mockedApi.patch).toHaveBeenCalledWith("/job-discovery/saved-searches/7/results/new-1/dismiss", { isDismissed: true });
|
||||
});
|
||||
|
||||
test("lets the user pause automatic alerts for a saved search", async () => {
|
||||
const saved = { id: 7, name: "Backend Oslo", query: "backend", location: "Oslo", isActive: true, resultCount: 1, dismissedCount: 0 };
|
||||
mockedApi.get.mockResolvedValue({ data: [saved] } as any);
|
||||
mockedApi.patch.mockResolvedValue({ data: { ...saved, isActive: false } } as any);
|
||||
renderPage();
|
||||
|
||||
const toggle = await screen.findByRole("switch", { name: "Automatic alerts" });
|
||||
expect(toggle).toBeChecked();
|
||||
fireEvent.click(toggle);
|
||||
|
||||
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/job-discovery/saved-searches/7", { isActive: false }));
|
||||
await waitFor(() => expect(toggle).not.toBeChecked());
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||
import { Alert, Box, Button, Card, CardActions, CardContent, Chip, CircularProgress, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, Card, CardActions, CardContent, Chip, CircularProgress, FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import BookmarkAddOutlinedIcon from "@mui/icons-material/BookmarkAddOutlined";
|
||||
@@ -26,7 +26,7 @@ type DiscoveredJob = {
|
||||
isNew?: boolean;
|
||||
};
|
||||
|
||||
type SavedSearch = { id: number; name: string; query: string; location: string; lastRunAtUtc?: string; resultCount: number; dismissedCount: number };
|
||||
type SavedSearch = { id: number; name: string; query: string; location: string; isActive: boolean; lastRunAtUtc?: string; resultCount: number; dismissedCount: number };
|
||||
type SavedSearchRun = { search: SavedSearch; jobs: Array<{ job: DiscoveredJob; isNew: boolean; isDismissed: boolean }> };
|
||||
|
||||
type SortOrder = "updated" | "deadline" | "title";
|
||||
@@ -47,6 +47,7 @@ export default function JobDiscoveryPage() {
|
||||
const [savedName, setSavedName] = useState("");
|
||||
const [savingSearch, setSavingSearch] = useState(false);
|
||||
const [activeSavedSearchId, setActiveSavedSearchId] = useState<number | null>(null);
|
||||
const [updatingSavedSearchId, setUpdatingSavedSearchId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -102,6 +103,15 @@ export default function JobDiscoveryPage() {
|
||||
} catch { setError(t("jobDiscoveryDeleteSavedFailed")); }
|
||||
};
|
||||
|
||||
const toggleSavedSearchAlerts = async (saved: SavedSearch) => {
|
||||
setUpdatingSavedSearchId(saved.id); setError("");
|
||||
try {
|
||||
const response = await api.patch<SavedSearch>(`/job-discovery/saved-searches/${saved.id}`, { isActive: !saved.isActive });
|
||||
setSavedSearches((current) => current.map((item) => item.id === saved.id ? response.data : item));
|
||||
} catch { setError(t("jobDiscoveryUpdateAlertsFailed")); }
|
||||
finally { setUpdatingSavedSearchId(null); }
|
||||
};
|
||||
|
||||
const dismissJob = async (jobId: string) => {
|
||||
if (activeSavedSearchId === null) return;
|
||||
try {
|
||||
@@ -148,6 +158,11 @@ export default function JobDiscoveryPage() {
|
||||
<Typography variant="caption" color="text.secondary">{[saved.query, saved.location].filter(Boolean).join(" · ") || t("jobDiscoveryAllRecent")}{saved.lastRunAtUtc ? ` · ${t("jobDiscoveryLastRun", { date: formatDate(saved.lastRunAtUtc) ?? "" })}` : ""}</Typography>
|
||||
</Box>
|
||||
<Chip size="small" label={t("jobDiscoveryTrackedCount", { count: saved.resultCount })} />
|
||||
<FormControlLabel
|
||||
sx={{ m: 0 }}
|
||||
control={<Switch size="small" checked={saved.isActive} disabled={updatingSavedSearchId === saved.id} onChange={() => void toggleSavedSearchAlerts(saved)} />}
|
||||
label={<Typography variant="body2">{t("jobDiscoveryAutomaticAlerts")}</Typography>}
|
||||
/>
|
||||
<Button size="small" startIcon={<RefreshIcon />} onClick={() => void runSavedSearch(saved)} disabled={loading}>{t("jobDiscoveryRunSaved")}</Button>
|
||||
<Button size="small" color="error" startIcon={<DeleteOutlineIcon />} onClick={() => void deleteSavedSearch(saved)}>{t("adminUsersDelete")}</Button>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user