Files
jobbi-bewerbung/views/bewerbung.ejs
T
thomasandClaude fa07dbf90a /bewerbung: Tabs Unterlagen/E-Mail + Admin-Nutzerdetailseite
- Bewerbungsdetail: Toggle zwischen Bewerbungsunterlagen und E-Mail-Korrespondenz
  (hash-basiert), weniger Scroll auf der Seite.
- Admin: Klick auf einen Nutzer öffnet neue Verwaltungsseite
  /admin/users/:id (Passwort, Anmelden als, Jobsuche, Admin-Status, Löschen).
- Admin-Tabelle: Löschen und Jobsuche aus den Aktionen entfernt
  (jetzt auf der Nutzerdetailseite).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 08:37:54 +02:00

1163 lines
90 KiB
Plaintext
Raw 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', { hideSettings: true }) %>
<%
const statusColor = {
'Entwurf': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
'Gesendet': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200',
'Eingangsbestätigung': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'In Bearbeitung': 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200',
'Interessiert': 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200',
'Telefonat': 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-200',
'Warten auf Rückmeldung': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'Warten auf meine Antwort': 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-900 dark:text-fuchsia-200',
'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'Vertragsverhandlung': 'bg-lime-100 text-lime-800 dark:bg-lime-900 dark:text-lime-200',
'Einstellung': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'Absage': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
'Absage von meiner Seite': 'bg-rose-100 text-rose-800 dark:bg-rose-900 dark:text-rose-200',
'Keine Rückmeldung': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
};
const aktuellerStatus = (application.status && application.status.trim()) ? application.status.trim() : 'Ohne Status';
const today = new Date().toISOString().split('T')[0];
%>
<main class="flex-1 container mx-auto px-4 py-8 max-w-7xl">
<!-- Back link -->
<a href="/" class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-6">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Zurück zur Übersicht
</a>
<!-- Title -->
<div class="flex flex-wrap items-center gap-3 mb-6">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">
<%= application.firma %> <span class="text-gray-400 dark:text-gray-500"></span> <%= application.stelle %>
</h1>
<span class="px-3 py-1 rounded-full text-sm font-medium <%= statusColor[aktuellerStatus] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
<%= aktuellerStatus %>
</span>
<%- include('partials/favorit-button', { j: application, endpoint: '/api/bewerbungen/' + application.id + '/favorit' }) %>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
<div class="space-y-8">
<!-- Application data -->
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">Bewerbungsdaten</h2>
<form action="/bewerbung/<%= application.id %>" method="POST" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Datum *</label>
<input type="date" name="datum" required value="<%= application.datum %>"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Art der Bewerbung</label>
<select name="art" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<option value="">-- Bitte wählen --</option>
<% artOptions.forEach(option => { %>
<option value="<%= option %>" <%= application.art === option ? 'selected' : '' %>><%= option %></option>
<% }); %>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Firma *</label>
<input type="text" name="firma" required value="<%= application.firma %>"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Stelle *</label>
<input type="text" name="stelle" required value="<%= application.stelle %>"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Labels</label>
<%- include('partials/labelPicker', { labelOptions: labelOptions, selected: application.labelsArr }) %>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notizen</label>
<textarea name="notizen" rows="4"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Allgemeine Notizen zur Bewerbung..."><%= application.notizen || '' %></textarea>
</div>
<div>
<label class="text-sm font-medium text-amber-700 dark:text-amber-400 mb-2 flex items-center gap-1.5">
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
Interne Notizen (nicht im PDF)
</label>
<textarea name="interne_notizen" rows="4"
class="w-full px-3 py-2 border border-amber-300 dark:border-amber-700 rounded-md bg-amber-50 dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Nur für dich erscheint nicht im Export..."><%= application.interne_notizen || '' %></textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
Bewerbungsdaten speichern
</button>
</div>
</form>
<!-- Eigene Anhänge: private files for the user only, never exported/sent.
Kept außerhalb des Bewerbungsdaten-Formulars, damit der Upload
(multipart) und das Löschen als eigene Formulare funktionieren. -->
<div class="mt-5 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30 p-4">
<div class="flex items-center gap-2 mb-3">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 shrink-0">
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</span>
<div class="min-w-0">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200 leading-tight">Eigene Anhänge</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 leading-tight">Nur für dich erscheinen nicht im PDF und werden nicht mitversendet.</p>
</div>
</div>
<% if (interneAnhaenge && interneAnhaenge.length) { %>
<ul class="space-y-2 mb-3">
<% interneAnhaenge.forEach(a => { %>
<% const isPdf = (a.mime === 'application/pdf') || /\.pdf$/i.test(a.dateiname || ''); %>
<li class="group flex items-center gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-3 py-2.5 hover:border-gray-300 dark:hover:border-gray-600 transition-colors">
<a href="/bewerbung/<%= application.id %>/interne-anhaenge/<%= a.id %>/download?inline=1" target="_blank" rel="noopener"
class="flex items-center gap-3 min-w-0 flex-1" title="<%= isPdf ? 'PDF im neuen Tab öffnen' : 'Im neuen Tab öffnen' %>">
<span class="inline-flex items-center justify-center w-9 h-9 rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 shrink-0">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
</span>
<span class="min-w-0">
<span class="block text-sm font-medium text-gray-800 dark:text-gray-100 truncate group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:underline"><%= a.name || a.dateiname %></span>
<span class="block text-xs text-gray-500 dark:text-gray-400 truncate"><%= a.dateiname %></span>
</span>
</a>
<div class="flex items-center gap-1 shrink-0">
<a href="/bewerbung/<%= application.id %>/interne-anhaenge/<%= a.id %>/download?inline=1" target="_blank" rel="noopener"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
title="Öffnen" aria-label="Öffnen">
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</a>
<a href="/bewerbung/<%= application.id %>/interne-anhaenge/<%= a.id %>/download"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
title="Herunterladen" aria-label="Herunterladen">
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
</a>
<form action="/bewerbung/<%= application.id %>/interne-anhaenge/<%= a.id %>/delete" method="POST"
onsubmit="return confirm('Diesen Anhang löschen?');" class="inline-block">
<button type="submit"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:bg-red-50 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 transition-colors"
title="Löschen" aria-label="Löschen">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</form>
</div>
</li>
<% }); %>
</ul>
<% } else { %>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">Noch keine eigenen Anhänge.</p>
<% } %>
<form action="/bewerbung/<%= application.id %>/interne-anhaenge" method="POST" enctype="multipart/form-data" class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<div class="grid grid-cols-1 sm:grid-cols-[1fr_auto] gap-3 items-start">
<div class="space-y-2">
<label class="block">
<span class="sr-only">Datei</span>
<input type="file" name="datei" required
accept="application/pdf,image/*,.doc,.docx,.xls,.xlsx,.txt,.zip"
class="block w-full text-sm text-gray-600 dark:text-gray-300 file:mr-3 file:py-2 file:px-4 file:rounded-md file:border-0 file:font-medium file:bg-gray-600 file:text-white hover:file:bg-gray-700 file:cursor-pointer file:transition-colors">
</label>
<label class="block">
<span class="sr-only">Bezeichnung (optional)</span>
<input type="text" name="name" placeholder="Bezeichnung (optional) z. B. Arbeitszeugnis"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-400/40 focus:border-blue-400">
</label>
</div>
<div class="flex sm:items-end">
<button type="submit"
class="w-full sm:w-auto inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors shadow-sm">
<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="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Hochladen
</button>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">PDFs lassen sich per Klick direkt öffnen. Max. 15 MB.</p>
</form>
</div>
</section>
<!-- Status timeline -->
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-1">Status-Verlauf</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
Jede Statusänderung wird mit Datum dokumentiert. Der jüngste Eintrag bestimmt den aktuellen Status.
</p>
<!-- Existing entries -->
<% if (verlauf.length > 0) { %>
<ol class="relative border-l border-gray-200 dark:border-gray-600 ml-2 space-y-6 mb-8">
<% verlauf.forEach(eintrag => { %>
<li class="ml-6">
<span class="absolute -left-1.5 mt-2 w-3 h-3 rounded-full bg-blue-500 ring-4 ring-white dark:ring-gray-800"></span>
<form action="/bewerbung/<%= application.id %>/verlauf/<%= eintrag.id %>" method="POST"
class="bg-gray-50 dark:bg-gray-700/40 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Datum</label>
<input type="date" name="datum" required value="<%= eintrag.datum %>"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Status</label>
<select name="status" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<% statusOptions.forEach(option => { %>
<option value="<%= option %>" <%= eintrag.status === option ? 'selected' : '' %>><%= option %></option>
<% }); %>
</select>
</div>
</div>
<div class="mb-3">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Kommentar</label>
<textarea name="kommentar" rows="2"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Was ist passiert?"><%= eintrag.kommentar || '' %></textarea>
</div>
<div class="flex justify-end gap-2">
<button type="submit"
class="px-3 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
Speichern
</button>
</div>
</form>
<form action="/bewerbung/<%= application.id %>/verlauf/<%= eintrag.id %>/delete" method="POST" class="mt-1 text-right"
onsubmit="return confirm('Diesen Verlaufseintrag löschen?');">
<button type="submit"
class="text-xs text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 hover:underline">
Eintrag löschen
</button>
</form>
</li>
<% }); %>
</ol>
<% } else { %>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8">Noch keine Statusänderungen dokumentiert.</p>
<% } %>
<!-- Add new entry -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Statusänderung hinzufügen</h3>
<form action="/bewerbung/<%= application.id %>/verlauf" method="POST" class="space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Datum</label>
<input type="date" name="datum" required value="<%= today %>"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Status</label>
<select name="status" required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<% statusOptions.forEach(option => { %>
<option value="<%= option %>"><%= option %></option>
<% }); %>
</select>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Kommentar</label>
<textarea name="kommentar" rows="2"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="Was ist passiert?"></textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors">
Statusänderung hinzufügen
</button>
</div>
</form>
</div>
</section>
<!-- Termine (mirrored to the SOGo calendar via CalDAV) -->
<% if (caldavConfigured) { %>
<section id="termine" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<div class="flex items-center gap-2 mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Termine</h2>
</div>
<% if (terminOk) { %>
<div class="mb-3 rounded-md bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 px-3 py-2 text-sm text-green-700 dark:text-green-300">Termin wurde im Kalender angelegt.</div>
<% } %>
<% if (terminError) { %>
<div class="mb-3 rounded-md bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 px-3 py-2 text-sm text-red-700 dark:text-red-300"><%= terminError %></div>
<% } %>
<% if (terminVorschlag) { %>
<div class="mb-3 rounded-md bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 px-3 py-2 text-sm text-blue-700 dark:text-blue-300">
Vorstellungsgespräch erfasst möchtest du den Termin in den Kalender aufnehmen? Zeit prüfen und speichern.
</div>
<% } %>
<% if (termine && termine.length) { %>
<ul class="space-y-2 mb-4">
<% termine.forEach(function(t){
var when = t.ganztags
? new Date(t.start).toLocaleDateString('de-DE', { timeZone: caldavTz, day: '2-digit', month: 'long', year: 'numeric' })
: new Date(t.start).toLocaleString('de-DE', { timeZone: caldavTz, weekday: 'short', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
var bis = (!t.ganztags && t.ende) ? new Date(t.ende).toLocaleTimeString('de-DE', { timeZone: caldavTz, hour: '2-digit', minute: '2-digit' }) : '';
%>
<li class="flex flex-wrap items-start justify-between gap-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 px-3 py-2">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= t.typ === 'vorstellungsgespraech' ? 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200' : 'bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-200' %>"><%= t.typ === 'vorstellungsgespraech' ? 'Vorstellungsgespräch' : 'Termin' %></span>
<span class="text-sm font-medium text-gray-800 dark:text-gray-100 break-words"><%= t.titel %></span>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300 mt-0.5"><%= when %><% if (bis) { %><%= bis %> Uhr<% } %></p>
<% if (t.ort) { %><p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">📍 <%= t.ort %></p><% } %>
<% if (t.notiz) { %><p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 whitespace-pre-line"><%= t.notiz %></p><% } %>
</div>
<div class="flex shrink-0 items-center gap-2">
<button type="button" onclick="document.getElementById('terminEdit_<%= t.id %>').open = !document.getElementById('terminEdit_<%= t.id %>').open;" class="inline-flex items-center gap-1 px-2 py-1 text-xs 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">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Bearbeiten
</button>
<form action="/bewerbung/<%= application.id %>/termine/<%= t.id %>/delete" method="POST" onsubmit="return confirm('Diesen Termin löschen (auch im Kalender)?');">
<button type="submit" class="inline-flex items-center gap-1 px-2 py-1 text-xs 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">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
Löschen
</button>
</form>
</div>
<details id="terminEdit_<%= t.id %>" class="mt-2">
<form action="/bewerbung/<%= application.id %>/termine/<%= t.id %>/edit" method="POST" class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 space-y-3">
<div class="grid gap-3 sm:grid-cols-2">
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Art</span>
<select name="typ" class="terminEditTyp mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<option value="vorstellungsgespraech" <%= t.typ === 'vorstellungsgespraech' ? 'selected' : '' %>>Vorstellungsgespräch</option>
<option value="termin" <%= t.typ === 'termin' ? 'selected' : '' %>>Allgemeiner Termin</option>
</select>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Titel</span>
<input name="titel" value="<%= t.titel %>" placeholder="z. B. Vorstellungsgespräch" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Datum</span>
<input name="datum" type="date" required value="<%= t.editDatum %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs terminEditZeit">
<span class="text-gray-500 dark:text-gray-400">Uhrzeit (von / bis)</span>
<span class="mt-1 flex items-center gap-2">
<input name="von" type="time" value="<%= t.editVon %>" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<span class="text-gray-400"></span>
<input name="bis" type="time" value="<%= t.editBis %>" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</span>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Ort</span>
<input name="ort" value="<%= t.ort || '' %>" placeholder="Adresse oder Ort" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Erinnerung</span>
<select name="erinnerung_min" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<% [0,15,30,60,120,1440].forEach(function(m){ %>
<option value="<%= m %>" <%= Number(t.erinnerung_min) === m ? 'selected' : '' %>><%= m === 0 ? 'keine' : (m < 60 ? m + ' Min vorher' : (m < 1440 ? (m/60) + ' Stunde' + (m/60 > 1 ? 'n' : '') + ' vorher' : '1 Tag vorher')) %></option>
<% }); %>
</select>
</label>
</div>
<label class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<input type="checkbox" name="ganztags" value="1" class="terminEditGanztags rounded border-gray-300 dark:border-gray-600 text-blue-600" <%= t.ganztags ? 'checked' : '' %>>
Ganztägig
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Notiz</span>
<textarea name="notiz" rows="2" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"><%= t.notiz || '' %></textarea>
</label>
<div class="flex items-center gap-2">
<button type="submit" class="inline-flex items-center gap-1.5 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>
<button type="button" onclick="document.getElementById('terminEdit_<%= t.id %>').open = false;" class="px-3 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">Abbrechen</button>
</div>
</form>
</details>
</li>
<% }); %>
</ul>
<% } else { %>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Noch keine Termine für diese Bewerbung.</p>
<% } %>
<details <%= terminVorschlag ? 'open' : '' %> class="border-t border-gray-200 dark:border-gray-700 pt-3">
<summary class="cursor-pointer inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline select-none">
<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="M12 4v16m8-8H4"></path></svg>
Termin hinzufügen
</summary>
<form action="/bewerbung/<%= application.id %>/termine" method="POST" class="mt-3 space-y-3" id="terminForm">
<div class="grid gap-3 sm:grid-cols-2">
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Art</span>
<select name="typ" id="terminTyp" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<option value="vorstellungsgespraech" <%= terminVorschlag ? 'selected' : '' %>>Vorstellungsgespräch</option>
<option value="termin" <%= terminVorschlag ? '' : 'selected' %>>Allgemeiner Termin</option>
</select>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Titel</span>
<input name="titel" value="<%= terminVorschlag ? ('Vorstellungsgespräch ' + application.firma) : '' %>" placeholder="z. B. Vorstellungsgespräch" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Datum</span>
<input name="datum" type="date" required value="<%= terminVorschlag ? terminVorschlag.datum : '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs" id="zeitWrap">
<span class="text-gray-500 dark:text-gray-400">Uhrzeit (von / bis)</span>
<span class="mt-1 flex items-center gap-2">
<input name="von" type="time" value="09:00" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<span class="text-gray-400"></span>
<input name="bis" type="time" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</span>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Ort</span>
<input name="ort" value="<%= application.ort || '' %>" placeholder="Adresse oder Ort" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Erinnerung</span>
<select name="erinnerung_min" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<option value="0">keine</option>
<option value="15">15 Min vorher</option>
<option value="30">30 Min vorher</option>
<option value="60" selected>1 Stunde vorher</option>
<option value="120">2 Stunden vorher</option>
<option value="1440">1 Tag vorher</option>
</select>
</label>
</div>
<label class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<input type="checkbox" name="ganztags" value="1" id="ganztagsChk" class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
Ganztägig
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Notiz</span>
<textarea name="notiz" rows="2" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm"></textarea>
</label>
<button type="submit" class="inline-flex items-center gap-1.5 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>
In den Kalender aufnehmen
</button>
</form>
</details>
<script>
(function () {
var chk = document.getElementById('ganztagsChk');
var wrap = document.getElementById('zeitWrap');
if (chk && wrap) {
var sync = function () { wrap.style.opacity = chk.checked ? '0.4' : '1'; wrap.querySelectorAll('input').forEach(function (i) { i.disabled = chk.checked; }); };
chk.addEventListener('change', sync); sync();
}
// Bearbeiten-Formulare: „Ganztägig" deaktiviert die Uhrzeit-Felder.
document.querySelectorAll('input.terminEditGanztags').forEach(function (g) {
var form = g.closest('form');
if (!form) return;
var zeit = form.querySelector('.terminEditZeit');
var sync = function () {
zeit.style.opacity = g.checked ? '0.4' : '1';
zeit.querySelectorAll('input').forEach(function (i) { i.disabled = g.checked; });
};
g.addEventListener('change', sync); sync();
});
})();
</script>
</section>
<% } %>
</div>
<div class="space-y-8">
<% const genStatus = application.generierung_status; %>
<% const zeigeUnterlagen = genStatus || (anhaenge && anhaenge.length) || (application.stellenbeschreibung && application.stellenbeschreibung.trim()); %>
<% if (zeigeUnterlagen) { %>
<!-- Umschalter Unterlagen <-> E-Mail-Korrespondenz -->
<style>
.bw-tab{display:inline-flex;align-items:center;gap:.45rem;padding:.5rem 1rem;border-radius:.625rem;font-size:.875rem;font-weight:500;color:rgb(75 85 99);background:transparent;border:1px solid transparent;cursor:pointer;transition:background-color .15s,color .15s,border-color .15s}
.dark .bw-tab{color:rgb(209 213 219)}
.bw-tab:hover{background:rgb(243 244 246)}
.dark .bw-tab:hover{background:rgb(55 65 81)}
.bw-tab.active{background:rgb(37 99 235);color:#fff}
.bw-tab.active:hover{background:rgb(29 78 216)}
</style>
<div class="flex items-center gap-2" id="rechtsTabs">
<button type="button" data-tab="unterlagen" class="bw-tab active">
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
Unterlagen
</button>
<button type="button" data-tab="korrespondenz" class="bw-tab">
<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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
E-Mail
</button>
</div>
<!-- Bewerbungsunterlagen (KI-generiert) -->
<section id="unterlagen" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
data-status="<%= genStatus || '' %>">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Bewerbungsunterlagen</h2>
</div>
<!-- Generation status banner -->
<div id="genBanner">
<% if (!genStatus || genStatus === 'nicht_gestartet') { %>
<div class="flex items-center gap-3 rounded-lg bg-gray-50 dark:bg-gray-700/40 border border-gray-200 dark:border-gray-700 px-4 py-3 mb-4">
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm text-gray-700 dark:text-gray-300">Noch keine Unterlagen generiert. Ergänze bei Bedarf unten Notizen (z. B. die vollständige Firmenanschrift) und starte die Generierung.</span>
</div>
<% } else if (genStatus === 'ausstehend') { %>
<div class="flex items-center gap-3 rounded-lg bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 px-4 py-3 mb-4">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span class="text-sm text-blue-800 dark:text-blue-200">Die KI erstellt gerade individuelle Bewerbungsunterlagen … Diese Seite aktualisiert sich automatisch.</span>
</div>
<% } else if (genStatus === 'fehler') { %>
<div class="rounded-lg bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 px-4 py-3 mb-4">
<div class="flex items-center gap-2 mb-1">
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<span class="text-sm font-semibold text-red-800 dark:text-red-200">Generierung fehlgeschlagen</span>
</div>
<p class="text-sm text-red-700 dark:text-red-300 ml-7"><%= application.generierung_fehler || 'Unbekannter Fehler.' %></p>
</div>
<% } else if (genStatus === 'fertig') { %>
<div class="flex items-center gap-2 rounded-lg bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 px-4 py-3 mb-4">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm text-green-800 dark:text-green-200">Unterlagen wurden erstellt. Bitte vor dem Versand prüfen und ggf. anpassen.</span>
</div>
<% } %>
</div>
<!-- Attachments -->
<div id="anhaengeListe">
<% if (anhaenge && anhaenge.length) { %>
<ul class="space-y-2">
<% anhaenge.forEach(a => { %>
<li class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 px-4 py-3">
<a href="/anhaenge/<%= a.id %>/download?inline=1" target="_blank" rel="noopener"
class="flex items-center gap-3 min-w-0 group" title="Im neuen Tab öffnen">
<svg class="w-6 h-6 text-red-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
<div class="min-w-0">
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 truncate group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:underline"><%= a.name || a.dateiname %></p>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"><%= a.dateiname %></p>
</div>
</a>
<div class="flex items-center gap-3 shrink-0">
<a href="/anhaenge/<%= a.id %>/download"
class="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Download
</a>
<form action="/bewerbung/<%= application.id %>/anhaenge/<%= a.id %>/delete" method="POST"
onsubmit="return confirm('Diesen Anhang löschen?');">
<button type="submit" class="text-red-500 hover:text-red-700 dark:text-red-400" aria-label="Anhang löschen">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</form>
</div>
</li>
<% }); %>
</ul>
<% } else if (genStatus !== 'ausstehend') { %>
<p class="text-sm text-gray-500 dark:text-gray-400">Noch keine generierten Unterlagen vorhanden.</p>
<% } %>
</div>
<!-- Editable e-mail cover text (Begleit-E-Mail) -->
<% if ((application.email_anschreiben && application.email_anschreiben.trim()) || genStatus === 'fertig') { %>
<div id="email" class="mt-6 border-t border-gray-200 dark:border-gray-700 pt-5">
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
<h3 class="text-sm font-semibold text-gray-800 dark:text-white">Begleit-E-Mail</h3>
<button type="button" id="emailCopyBtn"
class="inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 hover:underline">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
<span id="emailCopyLabel">E-Mail-Text kopieren</span>
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Kurzer, normgerechter Text zum Versand der Bewerbung per E-Mail (Unterlagen im Anhang).
Du kannst Betreff und Text vor dem Versand frei anpassen und speichern.
</p>
<form action="/bewerbung/<%= application.id %>/email" method="POST">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Betreff</label>
<input type="text" name="email_betreff" id="emailBetreff" value="<%= application.email_betreff || '' %>"
class="w-full px-3 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
placeholder="Bewerbung als …">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">E-Mail-Text</label>
<textarea name="email_anschreiben" id="emailText" rows="10"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed font-sans"
placeholder="Sehr geehrte Damen und Herren,&#10;&#10;…"><%= application.email_anschreiben || '' %></textarea>
<div class="flex justify-end mt-3">
<button type="submit"
class="inline-flex items-center gap-1.5 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="M5 13l4 4L19 7"></path>
</svg>
E-Mail-Text speichern
</button>
</div>
</form>
</div>
<% } %>
<!-- LLM notes + generate/regenerate -->
<form action="/bewerbung/<%= application.id %>/generieren" method="POST"
id="generierenForm"
data-bestaetigen="<%= genStatus === 'fertig' ? '1' : '' %>"
class="mt-6 border-t border-gray-200 dark:border-gray-700 pt-5">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Notizen für die KI
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
Zusätzliche Infos für die Generierung z. B. die vollständige Firmenanschrift (Straße, PLZ, Ort),
ein Ansprechpartner oder besondere Hinweise zur Bewerbung. Die KI berücksichtigt diese Angaben.
</p>
<textarea name="llm_notizen" rows="4"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
placeholder="z. B.&#10;Musterfirma GmbH&#10;Musterstraße 12&#10;10115 Berlin&#10;Ansprechpartner: Frau Müller"><%= application.llm_notizen || '' %></textarea>
<!-- Which documents to generate (vorbelegt aus den Einstellungen). -->
<div class="mt-4" id="dokumentAuswahl">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Was soll generiert werden?</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
Vorbelegt ist deine Auswahl aus den <a href="/einstellungen" class="text-blue-600 dark:text-blue-400 hover:underline">Einstellungen</a>
(Standard: Anschreiben und Lebenslauf). Wird der Lebenslauf abgewählt,
führt ihn das Anschreiben auch nicht mehr unter „Anlagen“ auf.
</p>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="dokument" value="anschreiben" data-dokument
<%= dokumentAuswahl.includes('anschreiben') ? 'checked' : '' %>
class="rounded border-gray-300 dark:border-gray-600">
Anschreiben
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="dokument" value="lebenslauf" data-dokument
<%= dokumentAuswahl.includes('lebenslauf') ? 'checked' : '' %>
class="rounded border-gray-300 dark:border-gray-600">
Lebenslauf
</label>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-2 hidden" id="dokumentHinweis">
Bitte mindestens ein Dokument auswählen.
</p>
</div>
<!-- Optional extra attachments to enclose (vorbelegt aus den Einstellungen). -->
<div class="mt-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Zusätzliche Anlagen</label>
<% if (basisAnhaenge && basisAnhaenge.length) { %>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
Wähle aus, welche Anlagen (z. B. Zeugnisse) mitgeschickt werden. Vorbelegt ist deine Auswahl
aus den <a href="/einstellungen" class="text-blue-600 dark:text-blue-400 hover:underline">Einstellungen</a>
(Standard: keine); die Auswahl wird auch im Anschreiben unter „Anlagen“ berücksichtigt.
</p>
<div class="space-y-1.5">
<% basisAnhaenge.forEach(function(ba){ %>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200">
<input type="checkbox" name="anlage" value="<%= ba.id %>"
<%= anlagenAuswahl.includes(ba.id) ? 'checked' : '' %>
class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
<%= ba.name || ba.dateiname %>
<% if (ba.name && ba.dateiname && ba.name !== ba.dateiname) { %>
<span class="text-xs text-gray-400">(<%= ba.dateiname %>)</span>
<% } %>
</label>
<% }); %>
</div>
<% } else { %>
<p class="text-xs text-gray-400 dark:text-gray-500">
Keine zusätzlichen Anlagen hinterlegt. Unter
<a href="/vorlagen" class="text-blue-600 dark:text-blue-400 hover:underline">Vorlagen</a>
kannst du statische Anlagen (z. B. Zeugnisse) hochladen.
</p>
<% } %>
</div>
<div class="flex justify-end mt-4">
<button type="submit" <%= genStatus === 'ausstehend' ? 'disabled' : '' %>
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:opacity-50 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="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
<%= genStatus === 'ausstehend' ? 'Wird generiert…' : (genStatus === 'fertig' ? 'Notizen speichern & neu generieren' : 'Unterlagen generieren') %>
</button>
</div>
</form>
<!-- Job description from the import -->
<% if (application.stellenbeschreibung && application.stellenbeschreibung.trim()) { %>
<details class="mt-6 rounded-lg border border-gray-200 dark:border-gray-700">
<summary class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300">
Importierte Stellenbeschreibung anzeigen
</summary>
<div class="px-4 pb-4">
<% if (application.quelle_url) { %>
<a href="<%= safeUrl(application.quelle_url) %>" target="_blank" rel="noopener"
class="inline-block mb-3 text-sm text-blue-600 dark:text-blue-400 hover:underline break-all"><%= application.quelle_url %></a>
<% } %>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= application.stellenbeschreibung %></p>
</div>
</details>
<% } %>
</section>
<% } %>
<!-- E-Mail-Korrespondenz -->
<section id="korrespondenz" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">E-Mail-Korrespondenz</h2>
<% if (mailConfigured) { %>
<form action="/email/fetch" method="POST" class="shrink-0">
<input type="hidden" name="back" value="/bewerbung/<%= application.id %>#korrespondenz">
<button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
Postfach abrufen
</button>
</form>
<% } %>
</div>
<% if (!mailConfigured) { %>
<div class="flex items-center gap-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 px-4 py-3">
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<span class="text-sm text-yellow-800 dark:text-yellow-200">E-Mail ist nicht konfiguriert. Bitte <code>MAIL_HOST</code>, <code>MAIL_USER</code> und <code>MAIL_PASSWORD</code> in der <code>.env</code> setzen.</span>
</div>
<% } else { %>
<% if (mailError) { %>
<div class="rounded-lg bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 px-4 py-3 mb-4 text-sm text-red-800 dark:text-red-200"><%= mailError %></div>
<% } %>
<% if (mailOk) { %>
<div class="rounded-lg bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 px-4 py-3 mb-4 text-sm text-green-800 dark:text-green-200"><%= mailOk %></div>
<% } %>
<!-- Conversation thread -->
<% if (emails && emails.length) { %>
<ul class="space-y-3 mb-6">
<% emails.forEach(function(e){ var out = e.direction === 'out'; %>
<li class="rounded-lg border <%= out ? 'border-blue-200 dark:border-blue-800 bg-blue-50/60 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40' %> px-4 py-3">
<div class="flex flex-wrap items-center justify-between gap-2 mb-1">
<div class="flex items-center gap-2 min-w-0">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= out ? 'bg-blue-600 text-white' : 'bg-gray-600 text-white' %>"><%= out ? 'Gesendet' : 'Empfangen' %></span>
<span class="text-sm text-gray-700 dark:text-gray-200 truncate">
<%= out ? ('An: ' + (e.to_addr || '')) : ('Von: ' + (e.from_addr || '')) %>
</span>
</div>
<span class="text-xs text-gray-500 dark:text-gray-400 shrink-0"><%= e.email_date ? new Date(e.email_date).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' }) : '' %></span>
</div>
<p class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1"><%= e.subject || '(kein Betreff)' %></p>
<% if (e.display_srcdoc) { %>
<iframe class="email-html-frame w-full border border-gray-200 dark:border-gray-600 rounded bg-white" style="height:6rem"
sandbox="allow-same-origin" referrerpolicy="no-referrer"
srcdoc="<%= e.display_srcdoc %>"></iframe>
<% } else { %>
<p class="whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300"><%= e.body_text || '' %></p>
<% } %>
<% if (e.anhaenge && e.anhaenge.length) { %>
<div class="mt-2 flex flex-wrap gap-2">
<% e.anhaenge.forEach(function(a){ %>
<a href="/email-anhaenge/<%= a.id %>/download?inline=1" target="_blank" rel="noopener" class="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 hover:underline">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
<%= a.name || 'Anhang' %>
</a>
<% }); %>
</div>
<% } %>
<% if (!out) { %>
<div class="mt-2">
<button type="button" class="reply-toggle text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 hover:underline" data-target="reply-<%= e.id %>">Antworten</button>
</div>
<form action="/bewerbung/<%= application.id %>/email/send" method="POST" id="reply-<%= e.id %>" class="hidden mt-3 border-t border-gray-200 dark:border-gray-600 pt-3">
<input type="hidden" name="reply_to" value="<%= e.id %>">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">An</label>
<input type="email" name="to" required value="<%= e.from_addr_clean || '' %>"
class="w-full px-3 py-2 mb-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Betreff</label>
<input type="text" name="subject" value="<%= /^\s*(re|aw)\s*:/i.test(e.subject || '') ? (e.subject || '') : ('Re: ' + (e.subject || '')) %>"
class="reply-subject w-full px-3 py-2 mb-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm">
<!-- KI-Antwort: erst sagen, was drinstehen soll, dann generieren lassen.
Ohne Vorgabe kommt nur eine allgemeine Höflichkeitsantwort heraus. -->
<div class="mb-3 rounded-md border border-purple-200 dark:border-purple-800/60 bg-purple-50/60 dark:bg-purple-900/10 p-3">
<div class="flex flex-wrap items-center justify-between gap-2 mb-1.5">
<label for="ai-prompt-<%= e.id %>" class="flex items-center gap-1.5 text-xs font-medium text-purple-700 dark:text-purple-300">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
Was soll die KI antworten?
</label>
<select class="ai-reply-typ text-xs px-1.5 py-0.5 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200">
<option value="antwort">Antwort</option>
<option value="absage">Absage (andere Stelle)</option>
</select>
</div>
<textarea id="ai-prompt-<%= e.id %>" rows="2"
class="ai-reply-prompt w-full px-3 py-2 border border-purple-200 dark:border-purple-800/60 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm leading-relaxed"
placeholder="z. B. Termin am Dienstag 14 Uhr zusagen, nach dem Ablauf des Gesprächs fragen oder: kürzer und förmlicher"></textarea>
<div class="flex flex-wrap items-center justify-between gap-2 mt-2">
<p class="ai-reply-status text-xs text-gray-500 dark:text-gray-400">
Ohne Vorgabe antwortet die KI nur allgemein auf die Nachricht.
</p>
<button type="button" class="ai-reply-btn inline-flex items-center gap-1.5 shrink-0 px-3 py-1.5 text-xs font-medium rounded-md bg-purple-600 hover:bg-purple-700 text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
data-email-id="<%= e.id %>" data-app-id="<%= application.id %>" data-target="reply-<%= e.id %>">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<span class="ai-reply-label">KI-Antwort generieren</span>
</button>
</div>
</div>
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Nachricht</label>
<textarea name="body" rows="12" required
class="reply-body w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm leading-relaxed"><%= e.reply_quote ? '\n\n' + e.reply_quote : '' %></textarea>
<% if (basisAnhaenge && basisAnhaenge.length) { %>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mt-2 mb-1">Anlagen</p>
<div class="space-y-1">
<% basisAnhaenge.forEach(function(ba){ %>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200">
<input type="checkbox" name="basis_anlage" value="<%= ba.id %>"
class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
<%= ba.name || ba.dateiname %>
<% if (ba.name && ba.dateiname && ba.name !== ba.dateiname) { %>
<span class="text-xs text-gray-400">(<%= ba.dateiname %>)</span>
<% } %>
</label>
<% }); %>
</div>
<% } %>
<div class="flex justify-end mt-2">
<button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">Antwort senden</button>
</div>
</form>
<% } %>
</li>
<% }); %>
</ul>
<% } else { %>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">Noch keine E-Mails. Sende die Bewerbung unten oder rufe das Postfach ab.</p>
<% } %>
<!-- Compose / send the application -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-5">
<h3 class="text-sm font-semibold text-gray-800 dark:text-white mb-1">Bewerbung per E-Mail senden</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Versand über <code><%= mailFrom %></code>. Empfängeradresse eingeben, Betreff und Text prüfen,
Anhänge auswählen und senden.
</p>
<form action="/bewerbung/<%= application.id %>/email/send" method="POST"
onsubmit="return confirm('Bewerbung jetzt verbindlich an ' + (this.to.value.trim() || 'den Empfänger') + ' senden?');">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Empfänger (E-Mail)</label>
<input type="email" name="to" required value="<%= application.email_empfaenger || '' %>"
placeholder="personal@unternehmen.de"
class="w-full px-3 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Betreff</label>
<input type="text" name="subject" value="<%= application.email_betreff || ('Bewerbung als ' + (application.stelle || '')) %>"
class="w-full px-3 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Nachricht</label>
<textarea name="body" rows="8"
class="w-full px-3 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"><%= application.email_anschreiben || '' %></textarea>
<% if (anhaenge && anhaenge.length) { %>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Anhänge</p>
<div class="space-y-1 mb-3">
<% anhaenge.forEach(function(a){ %>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200">
<input type="checkbox" name="anhang" value="<%= a.id %>" checked
class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
<%= a.name || a.dateiname %> <span class="text-xs text-gray-400">(<%= a.dateiname %>)</span>
</label>
<% }); %>
</div>
<% } else { %>
<p class="text-xs text-gray-400 mb-3">Keine generierten Anhänge vorhanden - erst Unterlagen generieren.</p>
<% } %>
<div class="flex justify-end">
<button type="submit" class="inline-flex items-center gap-1.5 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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path></svg>
Bewerbung senden
</button>
</div>
</form>
</div>
<% } %>
</section>
</div>
</div>
</main>
<%- include('partials/footer') %>
<script>
(function () {
const cy = document.getElementById('currentYear');
if (cy) cy.textContent = new Date().getFullYear();
// Copy the e-mail cover text (subject + body) to the clipboard.
const copyBtn = document.getElementById('emailCopyBtn');
if (copyBtn) copyBtn.addEventListener('click', async () => {
const betreff = (document.getElementById('emailBetreff') || {}).value || '';
const body = (document.getElementById('emailText') || {}).value || '';
const label = document.getElementById('emailCopyLabel');
const payload = (betreff ? 'Betreff: ' + betreff + '\n\n' : '') + body;
try {
await navigator.clipboard.writeText(payload);
if (label) { const t = label.textContent; label.textContent = 'Kopiert!'; setTimeout(() => { label.textContent = t; }, 1500); }
} catch (e) { /* clipboard blocked — ignore */ }
});
// Size a sandboxed e-mail iframe to its content height. The sandbox
// keeps allow-same-origin (so we can read contentDocument to measure)
// but enables no scripts, no forms and no popups, so mail content can
// neither execute nor escape the frame.
function fitFrame(f) {
try {
const doc = f.contentDocument || (f.contentWindow && f.contentWindow.document);
if (!doc || !doc.body) return;
const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
if (h > 0) f.style.height = (h + 6) + 'px';
} catch (e) { /* cross-origin (shouldn't happen) — leave default height */ }
}
document.querySelectorAll('.email-html-frame').forEach((f) => {
f.addEventListener('load', () => fitFrame(f));
fitFrame(f);
// Late reflow once embedded images have loaded.
setTimeout(() => fitFrame(f), 400);
setTimeout(() => fitFrame(f), 1500);
});
// Toggle inline reply forms under received e-mails.
document.querySelectorAll('.reply-toggle').forEach((btn) => {
btn.addEventListener('click', () => {
const form = document.getElementById(btn.dataset.target);
if (!form) return;
form.classList.toggle('hidden');
// On open, put the cursor above the quoted text.
if (!form.classList.contains('hidden')) {
const ta = form.querySelector('.reply-body');
if (ta) { ta.focus(); try { ta.setSelectionRange(0, 0); } catch (e) {} ta.scrollTop = 0; }
}
});
});
// KI-Antwort: Der Nutzer gibt vor, was drinstehen soll; die KI schreibt
// die Mail dazu. Steht schon ein Entwurf im Feld, geht er mit — dann
// überarbeitet die KI ihn entlang der neuen Vorgabe, statt ihn (und die
// eigenen Änderungen daran) wegzuwerfen.
document.querySelectorAll('.ai-reply-btn').forEach((btn) => {
const form = document.getElementById(btn.dataset.target);
if (!form) return;
const typSelect = form.querySelector('.ai-reply-typ');
const promptFeld = form.querySelector('.ai-reply-prompt');
const status = form.querySelector('.ai-reply-status');
const body = form.querySelector('.reply-body');
const subject = form.querySelector('.reply-subject');
const label = btn.querySelector('.ai-reply-label');
// Das zitierte Original steht unter dem Entwurf und ist nicht Teil davon.
let zitat = (body ? body.value : '').trim();
const entwurfText = () => {
let t = body ? body.value : '';
if (zitat && t.trimEnd().endsWith(zitat)) t = t.trimEnd().slice(0, -zitat.length);
return t.trim();
};
const syncLabel = () => {
if (!label) return;
const absage = typSelect && typSelect.value === 'absage';
const ueberarbeiten = entwurfText().length > 0;
label.textContent = ueberarbeiten
? 'Entwurf überarbeiten'
: (absage ? 'KI-Absage generieren' : 'KI-Antwort generieren');
};
const melde = (text, art) => {
if (!status) return;
status.textContent = text;
status.className = 'ai-reply-status text-xs ' + (
art === 'fehler' ? 'text-red-600 dark:text-red-400'
: art === 'ok' ? 'text-green-700 dark:text-green-400'
: 'text-gray-500 dark:text-gray-400');
};
if (typSelect) typSelect.addEventListener('change', syncLabel);
if (body) body.addEventListener('input', syncLabel);
syncLabel();
const generieren = async () => {
const typ = typSelect ? typSelect.value : 'antwort';
const hinweise = promptFeld ? promptFeld.value.trim() : '';
const entwurf = entwurfText();
const prev = label ? label.textContent : '';
if (label) label.textContent = entwurf ? 'Überarbeite …' : 'Generiere …';
btn.disabled = true;
melde(entwurf
? 'Die KI überarbeitet den Entwurf nach deiner Vorgabe …'
: 'Die KI schreibt die Antwort …', 'info');
try {
const res = await fetch('/bewerbung/' + btn.dataset.appId + '/email/ai-reply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email_id: btn.dataset.emailId, typ, hinweise, entwurf }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Fehler');
if (body && data.text) {
if (data.quote) zitat = data.quote;
// Entwurf oben, zitiertes Original darunter — wie im Mailprogramm.
body.value = data.text + (zitat ? '\n\n' + zitat : '');
body.focus();
try { body.setSelectionRange(data.text.length, data.text.length); } catch (e) {}
}
if (subject && data.betreff) subject.value = data.betreff;
melde(hinweise
? 'Entwurf nach deiner Vorgabe erstellt prüfen, anpassen, senden.'
: 'Entwurf erstellt. Für Änderungen oben vorgeben, was anders sein soll.', 'ok');
syncLabel();
} catch (e) {
melde('KI-Antwort fehlgeschlagen: ' + (e.message || 'Unbekannter Fehler'), 'fehler');
if (label) label.textContent = prev;
} finally {
btn.disabled = false;
syncLabel();
}
};
btn.addEventListener('click', generieren);
// Strg/Cmd + Enter im Prompt-Feld generiert direkt.
if (promptFeld) {
promptFeld.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
if (!btn.disabled) generieren();
}
});
}
});
// While documents are being generated, poll for completion and
// reload the page once the status changes.
const unterlagen = document.getElementById('unterlagen');
if (unterlagen && unterlagen.dataset.status === 'ausstehend') {
const appId = <%= application.id %>;
const poll = setInterval(async () => {
try {
const res = await fetch('/api/bewerbungen/' + appId + '/generierung', { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
if (data.status && data.status !== 'ausstehend') {
clearInterval(poll);
window.location.reload();
}
} catch (e) { /* keep polling */ }
}, 4000);
}
// --- Umschalter Unterlagen <-> E-Mail-Korrespondenz (rechte Spalte) ---
(function () {
var unterlagen = document.getElementById('unterlagen');
var korrespondenz = document.getElementById('korrespondenz');
if (!unterlagen || !korrespondenz) return; // kein Toggle nötig
var tabs = Array.prototype.slice.call(document.querySelectorAll('#rechtsTabs [data-tab]'));
function setActive(name) {
unterlagen.classList.toggle('hidden', name !== 'unterlagen');
korrespondenz.classList.toggle('hidden', name !== 'korrespondenz');
tabs.forEach(function (t) { t.classList.toggle('active', t.getAttribute('data-tab') === name); });
// E-Mail-iframes nachmessen, falls sie beim Laden versteckt waren.
if (name === 'korrespondenz') {
korrespondenz.querySelectorAll('.email-html-frame').forEach(fitFrame);
}
}
function fromHash() {
var h = (location.hash || '').replace(/^#/, '');
return (h === 'unterlagen' || h === 'korrespondenz') ? h : 'unterlagen';
}
setActive(fromHash());
window.addEventListener('hashchange', function () { setActive(fromHash()); });
tabs.forEach(function (t) {
t.addEventListener('click', function () {
history.replaceState(null, '', '#' + t.getAttribute('data-tab'));
setActive(t.getAttribute('data-tab'));
});
});
})();
})();
// Generation form: require at least one document, and keep the
// "replace existing files" confirmation when regenerating.
(function () {
const form = document.getElementById('generierenForm');
if (!form) return;
const boxes = Array.from(form.querySelectorAll('[data-dokument]'));
const hinweis = document.getElementById('dokumentHinweis');
form.addEventListener('submit', function (e) {
const gewaehlt = boxes.filter((b) => b.checked);
if (!gewaehlt.length) {
e.preventDefault();
if (hinweis) hinweis.classList.remove('hidden');
return;
}
if (form.dataset.bestaetigen === '1') {
const was = gewaehlt.length === 2
? 'Anschreiben und Lebenslauf'
: (gewaehlt[0].value === 'anschreiben' ? 'das Anschreiben' : 'der Lebenslauf');
if (!confirm('Es ' + (gewaehlt.length === 2 ? 'werden ' : 'wird ') + was
+ ' neu generiert. Vorhandene generierte Dateien werden ersetzt. Fortfahren?')) {
e.preventDefault();
}
}
});
boxes.forEach((b) => b.addEventListener('change', function () {
if (hinweis && boxes.some((x) => x.checked)) hinweis.classList.add('hidden');
}));
})();
</script>
<%- include('partials/favorit-script') %>
</body>
</html>