Termine: alle anstehenden auf Startseite + Bearbeiten; Status Telefonat
- Startseite zeigt alle anstehenden Termine (vorher auf 6 begrenzt) - Termine lassen sich inline bearbeiten (CalDAV-Update, Neu-Anlage bei 404/412) - Neuer Bewerbungsstatus Telefonat (Listen, Farben, API, PDF, Statistik) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ function withLabels(row) {
|
|||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
||||||
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
||||||
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
|
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ function localInputToUtc(dateStr, timeStr) {
|
|||||||
return wallToUtc(y, mo, d, h || 0, mi || 0, TZ);
|
return wallToUtc(y, mo, d, h || 0, mi || 0, TZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UTC Date -> Berliner Wanduhr-Bestandteile { datum: 'YYYY-MM-DD', zeit: 'HH:MM' }
|
||||||
|
// (für vorausgefüllte Bearbeiten-Formulare).
|
||||||
|
function utcToWallInputs(date) {
|
||||||
|
const p = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(date).reduce((a, x) => (a[x.type] = x.value, a), {});
|
||||||
|
return { datum: `${p.year}-${p.month}-${p.day}`, zeit: `${p.hour}:${p.minute}` };
|
||||||
|
}
|
||||||
|
|
||||||
// UTC Date -> iCalendar UTC stamp "YYYYMMDDTHHMMSSZ".
|
// UTC Date -> iCalendar UTC stamp "YYYYMMDDTHHMMSSZ".
|
||||||
function toIcsUtc(date) {
|
function toIcsUtc(date) {
|
||||||
return date.getUTCFullYear() + pad(date.getUTCMonth() + 1) + pad(date.getUTCDate())
|
return date.getUTCFullYear() + pad(date.getUTCMonth() + 1) + pad(date.getUTCDate())
|
||||||
@@ -335,6 +345,7 @@ module.exports = {
|
|||||||
collectionUrl,
|
collectionUrl,
|
||||||
wallToUtc,
|
wallToUtc,
|
||||||
localInputToUtc,
|
localInputToUtc,
|
||||||
|
utcToWallInputs,
|
||||||
buildICS,
|
buildICS,
|
||||||
parseVEvent,
|
parseVEvent,
|
||||||
getCtag,
|
getCtag,
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ const ART_OPTIONS = [
|
|||||||
];
|
];
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
||||||
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
||||||
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
|
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
|
||||||
];
|
];
|
||||||
|
|||||||
+2
-1
@@ -355,7 +355,7 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
|
|
||||||
// ---- Group entries by status (order kept in sync with views/index.ejs) ----
|
// ---- Group entries by status (order kept in sync with views/index.ejs) ----
|
||||||
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
||||||
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
|
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
|
||||||
// Accent colour per status (RGB) – reused by the chart and the list headings
|
// Accent colour per status (RGB) – reused by the chart and the list headings
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
@@ -364,6 +364,7 @@ function generatePdfDocument(settings, applications, month, year) {
|
|||||||
'Eingangsbestätigung': [14, 165, 233],
|
'Eingangsbestätigung': [14, 165, 233],
|
||||||
'In Bearbeitung': [20, 184, 166],
|
'In Bearbeitung': [20, 184, 166],
|
||||||
'Interessiert': [236, 72, 153],
|
'Interessiert': [236, 72, 153],
|
||||||
|
'Telefonat': [244, 114, 182],
|
||||||
'Warten auf Rückmeldung': [249, 115, 22],
|
'Warten auf Rückmeldung': [249, 115, 22],
|
||||||
'Warten auf meine Antwort': [217, 70, 239],
|
'Warten auf meine Antwort': [217, 70, 239],
|
||||||
'Vorstellungsgespräch': [245, 158, 11],
|
'Vorstellungsgespräch': [245, 158, 11],
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const ART_OPTIONS = [
|
|||||||
];
|
];
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
'Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
||||||
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
||||||
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung'
|
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung'
|
||||||
];
|
];
|
||||||
@@ -862,6 +862,16 @@ async function autoBlacklistOffer(offer, grund) {
|
|||||||
// Upcoming appointments (not yet ended), newest first, for the dashboard widget.
|
// Upcoming appointments (not yet ended), newest first, for the dashboard widget.
|
||||||
async function upcomingTermine(limit = 6) {
|
async function upcomingTermine(limit = 6) {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
// limit = null/0 ⇒ kein Limit, alle anstehenden Termine (z. B. Startseite).
|
||||||
|
if (!limit) {
|
||||||
|
return dbAll(
|
||||||
|
`SELECT t.*, b.firma AS bewerbung_firma, b.stelle AS bewerbung_stelle
|
||||||
|
FROM termine t LEFT JOIN bewerbungen b ON b.id = t.bewerbung_id
|
||||||
|
WHERE t.user_id = ? AND COALESCE(t.ende, t.start) >= ?
|
||||||
|
ORDER BY t.start ASC`,
|
||||||
|
[currentUserId(), now]
|
||||||
|
);
|
||||||
|
}
|
||||||
return dbAll(
|
return dbAll(
|
||||||
`SELECT t.*, b.firma AS bewerbung_firma, b.stelle AS bewerbung_stelle
|
`SELECT t.*, b.firma AS bewerbung_firma, b.stelle AS bewerbung_stelle
|
||||||
FROM termine t LEFT JOIN bewerbungen b ON b.id = t.bewerbung_id
|
FROM termine t LEFT JOIN bewerbungen b ON b.id = t.bewerbung_id
|
||||||
@@ -1779,7 +1789,7 @@ initializeDatabase().then(async () => {
|
|||||||
await attachVerlauf(applications);
|
await attachVerlauf(applications);
|
||||||
applications.forEach((a) => { a.labelsArr = parseLabels(a.labels); });
|
applications.forEach((a) => { a.labelsArr = parseLabels(a.labels); });
|
||||||
// Upcoming calendar appointments for the dashboard widget.
|
// Upcoming calendar appointments for the dashboard widget.
|
||||||
const kommendeTermine = await upcomingTermine(6);
|
const kommendeTermine = await upcomingTermine(null);
|
||||||
|
|
||||||
// Get statistics
|
// Get statistics
|
||||||
const totalCount = await dbGet('SELECT COUNT(*) as count FROM bewerbungen WHERE user_id = ?', [U]);
|
const totalCount = await dbGet('SELECT COUNT(*) as count FROM bewerbungen WHERE user_id = ?', [U]);
|
||||||
@@ -2013,7 +2023,7 @@ initializeDatabase().then(async () => {
|
|||||||
|
|
||||||
// Any reply at all — everything past "Gesendet" except "Keine Rückmeldung".
|
// Any reply at all — everything past "Gesendet" except "Keine Rückmeldung".
|
||||||
const rueckStatus = [
|
const rueckStatus = [
|
||||||
'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert',
|
'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert', 'Telefonat',
|
||||||
'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
'Vorstellungsgespräch', 'Vertragsverhandlung',
|
||||||
'Absage', 'Absage von meiner Seite', 'Einstellung'
|
'Absage', 'Absage von meiner Seite', 'Einstellung'
|
||||||
@@ -2532,6 +2542,13 @@ initializeDatabase().then(async () => {
|
|||||||
const basisAnhaenge = await dbAll('SELECT id, name, dateiname FROM basis_anhaenge WHERE user_id = ? ORDER BY id ASC', [U]);
|
const basisAnhaenge = await dbAll('SELECT id, name, dateiname FROM basis_anhaenge WHERE user_id = ? ORDER BY id ASC', [U]);
|
||||||
// Calendar appointments for this application (mirrored to SOGo).
|
// Calendar appointments for this application (mirrored to SOGo).
|
||||||
const termine = await dbAll('SELECT * FROM termine WHERE bewerbung_id = ? AND user_id = ? ORDER BY start ASC', [id, U]);
|
const termine = await dbAll('SELECT * FROM termine WHERE bewerbung_id = ? AND user_id = ? ORDER BY start ASC', [id, U]);
|
||||||
|
// Vorausgefüllte Werte für die Bearbeiten-Formulare (Berliner Wanduhr).
|
||||||
|
termine.forEach((t) => {
|
||||||
|
const s = caldav.utcToWallInputs(new Date(t.start));
|
||||||
|
t.editDatum = s.datum;
|
||||||
|
t.editVon = t.ganztags ? '09:00' : s.zeit;
|
||||||
|
t.editBis = (!t.ganztags && t.ende) ? caldav.utcToWallInputs(new Date(t.ende)).zeit : '';
|
||||||
|
});
|
||||||
|
|
||||||
// E-Mail correspondence (sent + received), oldest first, with attachments.
|
// E-Mail correspondence (sent + received), oldest first, with attachments.
|
||||||
const emails = await dbAll(
|
const emails = await dbAll(
|
||||||
@@ -3019,6 +3036,70 @@ initializeDatabase().then(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Termin bearbeiten: aktualisiert den Kalender-Termin (CalDAV) und das
|
||||||
|
// lokale Spiegelbild. Legt einen neuen CalDAV-Eintrag an, falls der alte
|
||||||
|
// entfernt wurde (z. B. remote gelöscht).
|
||||||
|
app.post('/bewerbung/:id/termine/:tid/edit', async (req, res) => {
|
||||||
|
const { id, tid } = req.params;
|
||||||
|
try {
|
||||||
|
const t = await dbGet('SELECT * FROM termine WHERE id = ? AND bewerbung_id = ? AND user_id = ?', [tid, id, uid()]);
|
||||||
|
if (!t) return res.redirect('/bewerbung/' + id + '#termine');
|
||||||
|
if (!caldav.isConfigured()) {
|
||||||
|
return res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent('Kalender ist nicht konfiguriert.') + '#termine');
|
||||||
|
}
|
||||||
|
const b = req.body || {};
|
||||||
|
if (!b.datum) {
|
||||||
|
return res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent('Bitte ein Datum angeben.') + '#termine');
|
||||||
|
}
|
||||||
|
const ganztags = b.ganztags === 'on' || b.ganztags === '1' || b.ganztags === 'true';
|
||||||
|
const typ = b.typ === 'vorstellungsgespraech' ? 'vorstellungsgespraech' : 'termin';
|
||||||
|
const titel = (b.titel || '').trim() || (typ === 'vorstellungsgespraech' ? 'Vorstellungsgespräch' : 'Termin');
|
||||||
|
const erinnerung = Math.max(0, parseInt(b.erinnerung_min, 10) || 0);
|
||||||
|
|
||||||
|
let start, ende = null;
|
||||||
|
if (ganztags) {
|
||||||
|
const [y, mo, d] = String(b.datum).split('-').map(Number);
|
||||||
|
start = caldav.wallToUtc(y, mo, d, 0, 0);
|
||||||
|
} else {
|
||||||
|
start = caldav.localInputToUtc(b.datum, b.von || '09:00');
|
||||||
|
if (b.bis) ende = caldav.localInputToUtc(b.datum, b.bis);
|
||||||
|
}
|
||||||
|
|
||||||
|
const evt = {
|
||||||
|
summary: titel, location: b.ort || '', description: b.notiz || '',
|
||||||
|
start, end: ende, allDay: ganztags, alarmMin: erinnerung,
|
||||||
|
};
|
||||||
|
|
||||||
|
let href = t.caldav_href, etag = t.caldav_etag, caldavUid = t.caldav_uid;
|
||||||
|
if (href) {
|
||||||
|
const upd = await caldav.updateEvent({ href, etag, ...evt }).catch((e) => {
|
||||||
|
// 412/404 bedeutet: der entfernte Termin wurde zwischenzeitlich
|
||||||
|
// geändert oder gelöscht — dann legen wir ihn neu an.
|
||||||
|
if (/HTTP (404|412)/.test(e.message)) return null;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
if (upd) { etag = upd.etag; caldavUid = upd.uid; }
|
||||||
|
else { const created = await caldav.createEvent(evt); href = created.href; etag = created.etag; caldavUid = created.uid; }
|
||||||
|
} else {
|
||||||
|
const created = await caldav.createEvent(evt);
|
||||||
|
href = created.href; etag = created.etag; caldavUid = created.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbRun(
|
||||||
|
`UPDATE termine SET typ = ?, titel = ?, ort = ?, notiz = ?, start = ?, ende = ?, ganztags = ?,
|
||||||
|
erinnerung_min = ?, caldav_uid = ?, caldav_href = ?, caldav_etag = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND user_id = ?`,
|
||||||
|
[typ, sanitizeInput(titel), sanitizeInput(b.ort || ''), sanitizeInput(b.notiz || ''),
|
||||||
|
start.toISOString(), ende ? ende.toISOString() : null, ganztags ? 1 : 0, erinnerung,
|
||||||
|
caldavUid, href, etag, tid, uid()]
|
||||||
|
);
|
||||||
|
res.redirect('/bewerbung/' + id + '?terminok=1#termine');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error editing termin:', error);
|
||||||
|
res.redirect('/bewerbung/' + id + '?terminerror=' + encodeURIComponent(error.message || 'Termin konnte nicht aktualisiert werden.') + '#termine');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Update a timeline entry
|
// Update a timeline entry
|
||||||
app.post('/bewerbung/:id/verlauf/:eintragId', async (req, res) => {
|
app.post('/bewerbung/:id/verlauf/:eintragId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
'Eingangsbestätigung': 'bg-blue-500',
|
'Eingangsbestätigung': 'bg-blue-500',
|
||||||
'In Bearbeitung': 'bg-teal-500',
|
'In Bearbeitung': 'bg-teal-500',
|
||||||
'Interessiert': 'bg-pink-500',
|
'Interessiert': 'bg-pink-500',
|
||||||
|
'Telefonat': 'bg-sky-500',
|
||||||
'Warten auf Rückmeldung': 'bg-orange-500',
|
'Warten auf Rückmeldung': 'bg-orange-500',
|
||||||
'Warten auf meine Antwort': 'bg-fuchsia-500',
|
'Warten auf meine Antwort': 'bg-fuchsia-500',
|
||||||
'Vorstellungsgespräch': 'bg-yellow-500',
|
'Vorstellungsgespräch': 'bg-yellow-500',
|
||||||
|
|||||||
+75
-1
@@ -13,6 +13,7 @@
|
|||||||
'Eingangsbestätigung': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-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',
|
'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',
|
'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 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',
|
'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',
|
'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
@@ -730,7 +731,7 @@
|
|||||||
var bis = (!t.ganztags && t.ende) ? new Date(t.ende).toLocaleTimeString('de-DE', { timeZone: caldavTz, 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">
|
<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">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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="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>
|
<span class="text-sm font-medium text-gray-800 dark:text-gray-100 break-words"><%= t.titel %></span>
|
||||||
@@ -739,12 +740,74 @@
|
|||||||
<% if (t.ort) { %><p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">📍 <%= t.ort %></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><% } %>
|
<% 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>
|
||||||
|
<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)?');">
|
<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">
|
<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>
|
<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
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -820,6 +883,17 @@
|
|||||||
var sync = function () { wrap.style.opacity = chk.checked ? '0.4' : '1'; wrap.querySelectorAll('input').forEach(function (i) { i.disabled = chk.checked; }); };
|
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();
|
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>
|
</script>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+3
-1
@@ -145,7 +145,7 @@
|
|||||||
// Status-Reihenfolge synchron zu public/js/main.js halten. Steht hier oben,
|
// Status-Reihenfolge synchron zu public/js/main.js halten. Steht hier oben,
|
||||||
// weil sowohl die Liste als auch die Aktivitäten weiter unten sie brauchen.
|
// weil sowohl die Liste als auch die Aktivitäten weiter unten sie brauchen.
|
||||||
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
|
||||||
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
|
||||||
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
|
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
|
||||||
const statusDot = {
|
const statusDot = {
|
||||||
'Entwurf': 'bg-purple-500',
|
'Entwurf': 'bg-purple-500',
|
||||||
@@ -153,6 +153,7 @@
|
|||||||
'Eingangsbestätigung': 'bg-blue-500',
|
'Eingangsbestätigung': 'bg-blue-500',
|
||||||
'In Bearbeitung': 'bg-teal-500',
|
'In Bearbeitung': 'bg-teal-500',
|
||||||
'Interessiert': 'bg-pink-500',
|
'Interessiert': 'bg-pink-500',
|
||||||
|
'Telefonat': 'bg-sky-500',
|
||||||
'Warten auf Rückmeldung': 'bg-orange-500',
|
'Warten auf Rückmeldung': 'bg-orange-500',
|
||||||
'Warten auf meine Antwort': 'bg-fuchsia-500',
|
'Warten auf meine Antwort': 'bg-fuchsia-500',
|
||||||
'Vorstellungsgespräch': 'bg-yellow-500',
|
'Vorstellungsgespräch': 'bg-yellow-500',
|
||||||
@@ -246,6 +247,7 @@
|
|||||||
app.status === 'Vertragsverhandlung' ? 'bg-lime-100 text-lime-800 dark:bg-lime-800 dark:text-lime-100' :
|
app.status === 'Vertragsverhandlung' ? 'bg-lime-100 text-lime-800 dark:bg-lime-800 dark:text-lime-100' :
|
||||||
app.status === 'In Bearbeitung' ? 'bg-teal-100 text-teal-800 dark:bg-teal-800 dark:text-teal-100' :
|
app.status === 'In Bearbeitung' ? 'bg-teal-100 text-teal-800 dark:bg-teal-800 dark:text-teal-100' :
|
||||||
app.status === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' :
|
app.status === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' :
|
||||||
|
app.status === 'Telefonat' ? 'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100' :
|
||||||
app.status === 'Warten auf Rückmeldung' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
|
app.status === 'Warten auf Rückmeldung' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
|
||||||
app.status === 'Warten auf meine Antwort' ? 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-800 dark:text-fuchsia-100' :
|
app.status === 'Warten auf meine Antwort' ? 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-800 dark:text-fuchsia-100' :
|
||||||
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
|
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
|
||||||
|
|||||||
+2
-2
@@ -238,7 +238,7 @@
|
|||||||
const maxS = Math.max(...byStatus.map(s => s.count));
|
const maxS = Math.max(...byStatus.map(s => s.count));
|
||||||
const statusColor = (s) => ({
|
const statusColor = (s) => ({
|
||||||
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
|
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
|
||||||
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
|
'Interessiert':'#8b5cf6','Telefonat':'#f472b6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
|
||||||
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
|
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
|
||||||
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
|
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
|
||||||
}[s] || '#2563eb');
|
}[s] || '#2563eb');
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
<% if (aktivitaet.length) {
|
<% if (aktivitaet.length) {
|
||||||
const statusColor = (s) => ({
|
const statusColor = (s) => ({
|
||||||
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
|
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
|
||||||
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
|
'Interessiert':'#8b5cf6','Telefonat':'#f472b6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
|
||||||
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
|
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
|
||||||
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
|
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
|
||||||
}[s] || '#2563eb');
|
}[s] || '#2563eb');
|
||||||
|
|||||||
Reference in New Issue
Block a user