Files
thomasandClaude 472259f189 Zeitplan: gesamten Zeitplan direkt in der Sektion deaktivierbar
aktiv-Hauptschalter aus der Sektion 'Automatische Suche' in die Sektion
'Zeitplan' verschoben, sodass der gesamte Zeitplan dort ein-/ausgeschaltet
werden kann. Wochentage + Uhrzeit werden beim Deaktivieren nur abgedunkelt,
die Einstellungen bleiben erhalten.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 18:26:00 +02:00

343 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<%- include('partials/head') %>
</head>
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
<%- include('partials/header') %>
<main class="flex-1 container mx-auto px-4 py-8 max-w-4xl">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-800 dark:text-white">Jobsuche</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Du legst fest, <strong>wo</strong> gesucht wird. <strong>Was</strong> gesucht wird Rollen und
Technologien leitet die KI aus deinem hinterlegten Lebenslauf ab.
</p>
</div>
<% if (fremd) { %>
<div class="mb-6 rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4">
<p class="text-sm text-amber-800 dark:text-amber-200">
Du bearbeitest als Administrator das Suchprofil von <strong><%= zielUser.username %></strong>.
<a href="/jobsuche" class="underline">Zum eigenen Profil</a>
</p>
</div>
<% } %>
<% if (fehler) { %>
<div class="mb-6 rounded-lg border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-800 dark:text-red-200"><%= fehler %></p>
</div>
<% } %>
<% if (hinweis) { %>
<div class="mb-6 rounded-lg border border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-900/20 p-4">
<p class="text-sm text-green-800 dark:text-green-200"><%= hinweis %></p>
</div>
<% } %>
<% if (!hatLebenslauf || !hatOllamaKey) { %>
<div class="mb-6 rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4">
<p class="text-sm font-medium text-amber-800 dark:text-amber-200 mb-1">Vor dem ersten Suchlauf fehlt noch etwas:</p>
<ul class="text-sm text-amber-800 dark:text-amber-200 list-disc list-inside space-y-0.5">
<% if (!hatLebenslauf) { %>
<li>Kein Lebenslauf hinterlegt ohne ihn kann die KI dein Suchprofil nicht ableiten.
<a href="/vorlagen" class="underline">Unter Vorlagen anlegen</a></li>
<% } %>
<% if (!hatOllamaKey) { %>
<li>Kein Ollama-API-Schlüssel der Suchlauf läuft über dein eigenes KI-Kontingent.
<a href="/einstellungen" class="underline">In den Einstellungen eintragen</a></li>
<% } %>
</ul>
</div>
<% } %>
<form action="/jobsuche" method="POST" class="space-y-6">
<% if (fremd) { %><input type="hidden" name="ziel_user" value="<%= zielUser.id %>"><% } %>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-base font-semibold text-gray-800 dark:text-white">Wo gesucht wird</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 mb-5">Modus und Orte.</p>
<div class="space-y-2 mb-5">
<% var modusText = {
regional: 'Nur Stellen mit Arbeitsort in den unten genannten Städten.',
remote: 'Nur Stellen, die zu 100 % im Homeoffice ausübbar sind deutschlandweit, Firmensitz egal.',
beides: 'Zwei Durchgänge: regional in deinen Städten und zusätzlich 100 % Remote deutschlandweit.'
}; %>
<% modi.forEach(function (m) { %>
<label class="flex items-start gap-3 p-3 rounded-md border border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/40">
<input type="radio" name="modus" value="<%= m %>" <%= profil.modus === m ? 'checked' : '' %>
class="mt-0.5 border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
<span>
<span class="block text-sm font-medium text-gray-800 dark:text-white">
<%= m === 'regional' ? 'Regional' : (m === 'remote' ? '100 % Remote' : 'Regional + Remote') %>
</span>
<span class="block text-xs text-gray-500 dark:text-gray-400"><%= modusText[m] %></span>
</span>
</label>
<% }); %>
</div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="staedte">Städte</label>
<textarea id="staedte" name="staedte" rows="3" spellcheck="false"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"
placeholder="Berlin, Potsdam, Hamburg, München"><%= profil.staedte.join(', ') %></textarea>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
Kommagetrennt, höchstens <%= maxStaedte %>. Die <strong>erste Stadt gilt als Wohnort</strong> und wird
am höchsten priorisiert. Bei „100 % Remote“ werden die Städte ignoriert.
</p>
</section>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h3 class="text-base font-semibold text-gray-800 dark:text-white">Feinschliff</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Unter welchen Titeln gesucht wird und was die Suche zusätzlich beachten oder verwerfen soll.
</p>
</div>
<button type="button" id="feinschliffKi"
<%= (!hatUnterlagen || !hatOllamaKey) ? 'disabled' : '' %>
title="<%= !hatUnterlagen ? 'Dafür fehlen Basis-Unterlagen unter Vorlagen.' : (!hatOllamaKey ? 'Dafür fehlt der Ollama-API-Schlüssel in den Einstellungen.' : 'Leitet Vorschläge aus deinen Basis-Unterlagen ab.') %>"
class="inline-flex items-center gap-2 px-3 py-1.5 shrink-0 text-sm border border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400 rounded-md transition-colors hover:bg-blue-50 dark:hover:bg-blue-900/30 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>
</svg>
<span id="feinschliffKiText">Per KI vorschlagen</span>
</button>
</div>
<p id="feinschliffHinweis" class="hidden text-sm mt-3"></p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-3 mb-5">
Der KI-Vorschlag wertet deine <a href="/vorlagen" class="underline">Basis-Unterlagen</a> aus und
überschreibt alle drei Felder. Gespeichert wird er erst, wenn du unten auf „Speichern“ klickst.
</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="berufsbezeichnungen">Berufsbezeichnungen</label>
<textarea id="berufsbezeichnungen" name="berufsbezeichnungen" rows="2"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"
placeholder="z. B. Systemadministrator, IT-Administrator, Fachinformatiker Systemintegration"><%= profil.berufsbezeichnungen %></textarea>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
<strong>Danach wird gesucht</strong> was hier fehlt, findet die Suche nicht. Kommagetrennt;
gängige Varianten desselben Titels ergänzt die Suche selbst. Leer = die Suche leitet die Titel
wie bisher aus deinem Lebenslauf ab.
</p>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="zusatz_begriffe">Zusätzlich berücksichtigen</label>
<textarea id="zusatz_begriffe" name="zusatz_begriffe" rows="3"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"
placeholder="z. B. Teilzeit, Rechenzentrum, Systemhaus"><%= profil.zusatz_begriffe %></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="ausschluesse">Ausschließen</label>
<textarea id="ausschluesse" name="ausschluesse" rows="3"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"
placeholder="z. B. Zeitarbeit, Headhunter, Schichtdienst"><%= profil.ausschluesse %></textarea>
</div>
</div>
</section>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-base font-semibold text-gray-800 dark:text-white">Zeitplan</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Ist er aktiv, sucht die KI automatisch an den gewählten Tagen zur eingestellten Uhrzeit
und legt neue Treffer als Jobangebote an. Deaktivierst du ihn, läuft keine automatische
Suche mehr — die eingestellten Wochentage und die Uhrzeit bleiben aber erhalten.
</p>
</div>
<label class="inline-flex items-center gap-2 shrink-0">
<input type="checkbox" name="aktiv" value="1" id="zeitplan_aktiv" <%= profil.aktiv ? 'checked' : '' %>
<%= !darfZeitplanAendern ? 'disabled' : '' %>
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 disabled:opacity-60">
<span class="text-sm text-gray-700 dark:text-gray-300">aktiv</span>
</label>
</div>
<% if (!darfZeitplanAendern) { %>
<p class="text-xs text-amber-700 dark:text-amber-300 mt-3">
Der Zeitplan (aktiv, Wochentage, Uhrzeit) wird vom Administrator verwaltet.
</p>
<% } %>
<div id="zeitplan_felder" class="flex flex-wrap gap-2 mb-5 mt-5 <%= !profil.aktiv ? 'opacity-60' : '' %>">
<% wochentage.forEach(function (t) { %>
<label class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-gray-300 dark:border-gray-600 text-sm <%= darfZeitplanAendern ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/40' : 'opacity-70 cursor-not-allowed' %>">
<input type="checkbox" name="tage" value="<%= t.nr %>"
<%= profil.zeitplan_tage.indexOf(t.nr) !== -1 ? 'checked' : '' %>
<%= !darfZeitplanAendern ? 'disabled' : '' %>
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 disabled:opacity-60">
<span class="text-gray-700 dark:text-gray-300"><%= t.kurz %></span>
</label>
<% }); %>
</div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="zeitplan_zeit">Uhrzeit</label>
<input type="time" id="zeitplan_zeit" name="zeitplan_zeit" value="<%= profil.zeitplan_zeit %>"
<%= !darfZeitplanAendern ? 'disabled' : '' %>
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm disabled:opacity-60">
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
Der Lauf startet innerhalb weniger Minuten nach dieser Uhrzeit.
</p>
</section>
<div class="flex items-center justify-end gap-3">
<a href="/jobsuche<%= fremd ? '?user=' + zielUser.id : '' %>"
class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
Verwerfen
</a>
<button type="submit"
class="inline-flex items-center gap-2 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Speichern
</button>
</div>
</form>
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mt-6">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h3 class="text-base font-semibold text-gray-800 dark:text-white">Suchläufe</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Ein Lauf durchsucht das Web und legt neue Treffer als
<a href="/jobangebote" class="text-blue-600 dark:text-blue-400 hover:underline">Jobangebote</a> an.
</p>
</div>
<% if (darfSuchlaufStarten) { %>
<form action="/jobsuche/start" method="POST" class="shrink-0">
<% if (fremd) { %><input type="hidden" name="ziel_user" value="<%= zielUser.id %>"><% } %>
<button type="submit"
class="inline-flex items-center gap-2 px-4 py-2 text-sm bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Jetzt suchen
</button>
</form>
<% } else if (!fremd) { %>
<p class="text-xs text-gray-400 dark:text-gray-500 shrink-0 text-right max-w-[12rem]">
Manuelle Suchläufe werden vom Administrator verwaltet.
</p>
<% } %>
</div>
<div id="laeufe">
<%- include('partials/suchlaeufe', { laeufe: laeufe }) %>
</div>
</section>
</main>
<%- include('partials/footer') %>
<script>
(function () {
const cy = document.getElementById('currentYear');
if (cy) cy.textContent = new Date().getFullYear();
// While a run is queued or in flight, refresh the list so the user sees it
// progress without reloading. Stops once nothing is pending any more.
const zielUser = <%- fremd ? zielUser.id : 'null' %>;
const url = '/jobsuche/laeufe' + (zielUser ? '?user=' + zielUser : '');
let timer = null;
function offen(laeufe) {
return laeufe.some(function (l) { return l.status === 'angefordert' || l.status === 'laeuft'; });
}
async function poll() {
try {
const res = await fetch(url, { headers: { Accept: 'application/json' } });
if (!res.ok) return;
const laeufe = await res.json();
if (!offen(laeufe)) {
clearInterval(timer);
location.reload(); // fertig -> Ergebnisse serverseitig rendern
}
} catch (e) { /* nächster Tick */ }
}
const initial = <%- JSON.stringify(laeufe.map(function (l) { return { status: l.status }; })) %>;
if (offen(initial)) timer = setInterval(poll, 5000);
})();
// "Per KI vorschlagen": lets the model derive the two guard-rail lists from the
// user's Basis-Unterlagen. Only fills the fields — saving stays a deliberate act.
(function () {
const btn = document.getElementById('feinschliffKi');
if (!btn) return;
const label = document.getElementById('feinschliffKiText');
const hinweis = document.getElementById('feinschliffHinweis');
const berufe = document.getElementById('berufsbezeichnungen');
const zusatz = document.getElementById('zusatz_begriffe');
const ausschluesse = document.getElementById('ausschluesse');
const zielUser = <%- fremd ? zielUser.id : 'null' %>;
const FARBE = {
info: 'text-gray-500 dark:text-gray-400',
ok: 'text-green-700 dark:text-green-400',
fehler: 'text-red-600 dark:text-red-400',
};
function melde(text, art) {
hinweis.textContent = text;
hinweis.className = 'text-sm mt-3 ' + FARBE[art];
}
btn.addEventListener('click', async function () {
btn.disabled = true;
label.textContent = 'KI denkt nach …';
melde('Die KI liest deine Basis-Unterlagen das dauert einen Moment.', 'info');
const modus = document.querySelector('input[name="modus"]:checked');
try {
const res = await fetch('/jobsuche/feinschliff', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ziel_user: zielUser,
modus: modus ? modus.value : '',
staedte: document.getElementById('staedte').value,
}),
});
const data = await res.json().catch(function () { return {}; });
if (!res.ok) throw new Error(data.error || ('Fehler ' + res.status));
berufe.value = data.berufsbezeichnungen || '';
zusatz.value = data.zusatz_begriffe || '';
ausschluesse.value = data.ausschluesse || '';
melde('Vorschlag übernommen. Prüfe ihn und klicke auf „Speichern“.', 'ok');
} catch (e) {
melde(e.message || 'Der KI-Vorschlag ist fehlgeschlagen.', 'fehler');
} finally {
btn.disabled = false;
label.textContent = 'Per KI vorschlagen';
}
});
})();
// Zeitplan aktiv/deaktiv: stimmt die Sichtbarkeit der Wochentage + Uhrzeit
// mit dem Hauptschalter ab. Die Felder werden nur abgedunkelt, nicht
// deaktiviert — so bleiben die eingestellten Wochentage erhalten, wenn der
// Zeitplan später wieder aktiviert wird. Ist der Schalter gesperrt (Admin
// verwaltet den Zeitplan), greift die serverseitige Vorgabe unverändert.
(function () {
const aktiv = document.getElementById('zeitplan_aktiv');
if (!aktiv || aktiv.disabled) return;
const felder = document.getElementById('zeitplan_felder');
const zeit = document.getElementById('zeitplan_zeit');
function sync() {
const aus = !aktiv.checked;
if (felder) felder.classList.toggle('opacity-60', aus);
if (zeit) zeit.classList.toggle('opacity-60', aus);
}
sync();
aktiv.addEventListener('change', sync);
})();
</script>
</body>
</html>