From 1e273ddd922e77e2ca315ea808ad839ca45a889b Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Tue, 29 Sep 2026 17:34:59 +0200 Subject: [PATCH] =?UTF-8?q?Scanner:=20Altersfenster=20"=C3=A4lter=20als=20?= =?UTF-8?q?/=20j=C3=BCnger=20als"=20(Tage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ersetzt die feste Auswahl bis/über 21 Tage durch zwei freie Felder, einzeln oder kombiniert (z. B. älter 14, jünger 30). API: olderThan, youngerThan; age=recent|old bleibt für Altaufrufe. Co-Authored-By: Claude Opus 5.5 --- docs/08-anleitung-benutzer.md | 4 +- docs/12-scanner.md | 9 +++-- server/src/routes/scanner.js | 15 +++++++ src/components/scanner/ScannerDashboard.tsx | 43 +++++++++++++++------ 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/docs/08-anleitung-benutzer.md b/docs/08-anleitung-benutzer.md index e896733..9224a85 100644 --- a/docs/08-anleitung-benutzer.md +++ b/docs/08-anleitung-benutzer.md @@ -212,8 +212,8 @@ Kontakt (*Kontakt → Stundensatz vor Ort / remote*). ## Scanner Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna, -freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard filtert -*Anzeigenalter* nach „bis 21 Tage“ oder „älter als 21 Tage“. Technik +freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard grenzen +*Älter als* und *Jünger als* (Tage) das Alter ein, z. B. älter 14, jünger 30. Technik siehe [12 Scanner](12-scanner.md). 1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter diff --git a/docs/12-scanner.md b/docs/12-scanner.md index a3b22a2..a8cbc4b 100644 --- a/docs/12-scanner.md +++ b/docs/12-scanner.md @@ -4,9 +4,10 @@ Der Scanner durchsucht Jobbörsen nach offenen Stellen, die ICON besetzen kann Festanstellungen ebenso wie Projekte. Region immer DACH. Eingeführt am 29.09.2026. Gesucht wird **ohne Altersgrenze** (Änderung vom 29.09.2026, vorher höchstens -21 Tage): auch ältere Anzeigen kommen herein. Im Dashboard filtert -**Anzeigenalter** nach „bis 21 Tage" bzw. „älter als 21 Tage", die Sortierung -kennt „älteste Anzeige". +21 Tage): auch ältere Anzeigen kommen herein. Im Dashboard grenzen die Felder +**Älter als** und **Jünger als** (Tage) das Alter frei ein – z. B. älter 14, +jünger 30. Leer heißt keine Grenze; Anzeigen ohne Datum fallen heraus, sobald +eines der Felder gesetzt ist. Die Sortierung kennt „älteste Anzeige". Menü: **Scanner** (Dashboard) mit dem Unterpunkt **Suchagenten**; Zeitplan und API-Schlüssel unter **Einstellungen → Scanner** (nur Administratoren). @@ -276,7 +277,7 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`. | POST | `/agents` | alle | anlegen `{ name, keywords[], alarm_keywords[], sources[], excludes[], is_active }` – Schlagwort oder Alarm-Schlagwort Pflicht | | PUT | `/agents/:id` | alle | ändern (gleicher Körper) | | DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit | -| GET | `/matches` | alle | Filter `hours` (gefunden), `age` (`recent`/`old`, Grenze 21 Tage ab Veröffentlichung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` | +| GET | `/matches` | alle | Filter `hours` (gefunden), `olderThan` / `youngerThan` (Tage ab Veröffentlichung, einzeln oder zusammen; `age=recent|old` = feste 21-Tage-Grenze, aus der ersten Fassung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` | | GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) | | POST | `/alarms/ack` | alle | `{ ids: [] }` – quittiert für alle Benutzer | | PATCH | `/matches/:id` | alle | `{ status }` | diff --git a/server/src/routes/scanner.js b/server/src/routes/scanner.js index 0cfa5e2..e96c3cd 100644 --- a/server/src/routes/scanner.js +++ b/server/src/routes/scanner.js @@ -251,6 +251,21 @@ router.get('/matches', async (req, res, next) => { ); params.push(AGE_DAYS); } + /* + * Freies Altersfenster in Tagen: `olderThan` = mindestens so alt, + * `youngerThan` = hoechstens so alt (z. B. aelter 14, juenger 30). + * Beide optional und mit `age` kombinierbar. + */ + const olderThan = Number(req.query.olderThan); + if (Number.isFinite(olderThan) && olderThan > 0) { + where.push('j.published_at < NOW() - INTERVAL ? DAY'); + params.push(Math.round(olderThan)); + } + const youngerThan = Number(req.query.youngerThan); + if (Number.isFinite(youngerThan) && youngerThan > 0) { + where.push('j.published_at >= NOW() - INTERVAL ? DAY'); + params.push(Math.round(youngerThan)); + } if (req.query.alarm === '1') where.push('m.alarm = 1'); /* diff --git a/src/components/scanner/ScannerDashboard.tsx b/src/components/scanner/ScannerDashboard.tsx index 4637e9f..418c9de 100644 --- a/src/components/scanner/ScannerDashboard.tsx +++ b/src/components/scanner/ScannerDashboard.tsx @@ -65,8 +65,9 @@ export const ScannerDashboard: React.FC = () => { const [showDiscarded, setShowDiscarded] = useState(false); const [search, setSearch] = useState(''); const [sort, setSort] = useState({ key: 'known', dir: 'desc' }); - /** Alter der Anzeige: alle | bis 21 Tage | älter als 21 Tage */ - const [age, setAge] = useState<'' | 'recent' | 'old'>(''); + /** Altersfenster der Anzeige in Tagen; leer = keine Grenze. */ + const [olderThan, setOlderThan] = useState(''); + const [youngerThan, setYoungerThan] = useState(''); const [alarmOnly, setAlarmOnly] = useState(false); const wasRunning = useRef(false); @@ -81,7 +82,8 @@ export const ScannerDashboard: React.FC = () => { known: knownOnly ? 1 : undefined, status: showDiscarded ? 'alle' : undefined, q: search.trim() || undefined, - age: age || undefined, + olderThan: Number(olderThan) > 0 ? Number(olderThan) : undefined, + youngerThan: Number(youngerThan) > 0 ? Number(youngerThan) : undefined, alarm: alarmOnly ? 1 : undefined, sort: sort.key, dir: sort.dir, @@ -91,7 +93,7 @@ export const ScannerDashboard: React.FC = () => { } catch (err) { toast(err instanceof Error ? err.message : 'Treffer nicht ladbar.', 'error'); } - }, [hours, agentId, source, minScore, knownOnly, showDiscarded, search, age, alarmOnly, sort, toast]); + }, [hours, agentId, source, minScore, knownOnly, showDiscarded, search, olderThan, youngerThan, alarmOnly, sort, toast]); const loadOverview = useCallback(async () => { try { @@ -175,7 +177,7 @@ export const ScannerDashboard: React.FC = () => {

- Offene Stellen aus Jobbörsen in DACH – jedes Alter, gefiltert wird unten nach „Anzeigenalter“. + Offene Stellen aus Jobbörsen in DACH – jedes Alter, eingrenzen unten mit „Älter als“ / „Jünger als“.

{ ))} - - + + setOlderThan(event.target.value)} + placeholder="–" + title="Nur Anzeigen, die mindestens so viele Tage alt sind" + className={inputClass} + /> + + + setYoungerThan(event.target.value)} + placeholder="–" + title="Nur Anzeigen, die höchstens so viele Tage alt sind" + className={`${inputClass} ${ + Number(olderThan) > 0 && Number(youngerThan) > 0 && Number(youngerThan) <= Number(olderThan) + ? 'border-red-400' + : '' + }`} + />