Aktivitäten-Feed: empfangene und gesendete E-Mails einbeziehen
Der Aktivitäten-Feed (Startseite, /aktivitaeten, /statistik) führt Status- änderungen und E-Mails per UNION zusammen, sortiert nach Erfassungszeit. E-Mail-Einträge bekommen einen eigenen Farbpunkt (blau = erhalten, sky = gesendet); ohne verknüpfte Bewerbung wird Absender bzw. Empfänger als Kopfzeile gezeigt. Das UNION steckt in einem Subselect, weil SQLite ORDER BY an einem Compound-SELECT keine Ergebnisspalten nach Namen aufloest. Die Paginierung von /aktivitaeten zaehlt jetzt Statusaenderungen + E-Mails. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1644,6 +1644,51 @@ initializeDatabase().then(async () => {
|
|||||||
// files are isolated on disk the same way their DB rows are.
|
// files are isolated on disk the same way their DB rows are.
|
||||||
const userDir = userStorageDir;
|
const userDir = userStorageDir;
|
||||||
|
|
||||||
|
// Einheitlicher Aktivitäten-Feed: Statusänderungen UND E-Mails (empfangen wie
|
||||||
|
// gesendet) werden zusammengeführt und chronologisch nach Erfassungszeit
|
||||||
|
// sortiert. `typ` ('status' | 'email') unterscheidet die Herkunft für die
|
||||||
|
// Darstellung; direction/from_addr/to_addr sind nur bei E-Mails gefüllt.
|
||||||
|
// Das UNION steckt in einem Subselect, weil SQLite ORDER BY an einem Compound-
|
||||||
|
// SELECT keine Ergebnisspalten nach Namen auflöst — erst der äußere SELECT
|
||||||
|
// sieht created_at/sort_id als echte Spalten.
|
||||||
|
const AKTIVITAET_FEED_INNER = `
|
||||||
|
SELECT 'status' AS typ, sv.id AS sort_id, sv.created_at, sv.datum,
|
||||||
|
sv.status, sv.kommentar, sv.bewerbung_id,
|
||||||
|
b.firma, b.stelle,
|
||||||
|
NULL AS direction, NULL AS subject, NULL AS from_addr, NULL AS to_addr
|
||||||
|
FROM status_verlauf sv
|
||||||
|
LEFT JOIN bewerbungen b ON b.id = sv.bewerbung_id AND b.user_id = sv.user_id
|
||||||
|
WHERE sv.user_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'email' AS typ, e.id AS sort_id, e.created_at,
|
||||||
|
COALESCE(e.email_date, e.created_at) AS datum,
|
||||||
|
CASE WHEN e.direction = 'out' THEN 'E-Mail gesendet' ELSE 'E-Mail erhalten' END AS status,
|
||||||
|
e.subject AS kommentar, e.bewerbung_id,
|
||||||
|
b.firma, b.stelle,
|
||||||
|
e.direction, e.subject, e.from_addr, e.to_addr
|
||||||
|
FROM emails e
|
||||||
|
LEFT JOIN bewerbungen b ON b.id = e.bewerbung_id AND b.user_id = e.user_id
|
||||||
|
WHERE e.user_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function loadAktivitaet(U, { limit = null, offset = 0 } = {}) {
|
||||||
|
const params = [U, U];
|
||||||
|
let sql = `SELECT * FROM (${AKTIVITAET_FEED_INNER}) AS feed ORDER BY created_at DESC, typ DESC, sort_id DESC`;
|
||||||
|
if (limit != null) {
|
||||||
|
sql += ' LIMIT ? OFFSET ?';
|
||||||
|
params.push(limit, offset);
|
||||||
|
}
|
||||||
|
return dbAll(sql, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesamtzahl der Aktivitäten = Statusänderungen + E-Mails (für die Paginierung
|
||||||
|
// der /aktivitaeten-Seite).
|
||||||
|
async function countAktivitaet(U) {
|
||||||
|
const sv = await dbGet('SELECT COUNT(*) AS n FROM status_verlauf WHERE user_id = ?', [U]);
|
||||||
|
const em = await dbGet('SELECT COUNT(*) AS n FROM emails WHERE user_id = ?', [U]);
|
||||||
|
return (sv ? sv.n : 0) + (em ? em.n : 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.get('/', async (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1679,19 +1724,12 @@ initializeDatabase().then(async () => {
|
|||||||
GROUP BY status ORDER BY count DESC
|
GROUP BY status ORDER BY count DESC
|
||||||
`, [U]);
|
`, [U]);
|
||||||
|
|
||||||
// Letzte Aktivitäten für die Startseite: die jüngsten Statusänderungen, quer
|
// Letzte Aktivitäten für die Startseite: die jüngsten Ereignisse quer über
|
||||||
// über alle Bewerbungen. Sortiert nach Erfassungszeit (created_at), nicht nach
|
// alle Bewerbungen — Statusänderungen ebenso wie empfangene und gesendete
|
||||||
// dem fachlichen Datum — sonst würde ein nachgetragener alter Eintrag oben
|
// E-Mails. Sortiert nach Erfassungszeit (created_at), nicht nach dem
|
||||||
|
// fachlichen Datum — sonst würde ein nachgetragener alter Eintrag oben
|
||||||
// stehen, obwohl gerade etwas anderes passiert ist.
|
// stehen, obwohl gerade etwas anderes passiert ist.
|
||||||
const aktivitaet = await dbAll(`
|
const aktivitaet = await loadAktivitaet(U, { limit: 8 });
|
||||||
SELECT sv.datum, sv.status, sv.kommentar,
|
|
||||||
b.id AS bewerbung_id, b.firma, b.stelle
|
|
||||||
FROM status_verlauf sv
|
|
||||||
LEFT JOIN bewerbungen b ON b.id = sv.bewerbung_id AND b.user_id = sv.user_id
|
|
||||||
WHERE sv.user_id = ?
|
|
||||||
ORDER BY datetime(sv.created_at) DESC, sv.id DESC
|
|
||||||
LIMIT 8
|
|
||||||
`, [U]);
|
|
||||||
|
|
||||||
// Get available months/years for filter
|
// Get available months/years for filter
|
||||||
const availableMonths = await dbAll(`
|
const availableMonths = await dbAll(`
|
||||||
@@ -1798,8 +1836,7 @@ initializeDatabase().then(async () => {
|
|||||||
app.get('/aktivitaeten', async (req, res) => {
|
app.get('/aktivitaeten', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const U = uid();
|
const U = uid();
|
||||||
const gesamtRow = await dbGet('SELECT COUNT(*) AS anzahl FROM status_verlauf WHERE user_id = ?', [U]);
|
const gesamt = await countAktivitaet(U);
|
||||||
const gesamt = gesamtRow ? gesamtRow.anzahl : 0;
|
|
||||||
const seiten = Math.max(1, Math.ceil(gesamt / AKTIVITAETEN_PRO_SEITE));
|
const seiten = Math.max(1, Math.ceil(gesamt / AKTIVITAETEN_PRO_SEITE));
|
||||||
|
|
||||||
// Eine Seite außerhalb des Bereichs (getippt oder nach dem Löschen von
|
// Eine Seite außerhalb des Bereichs (getippt oder nach dem Löschen von
|
||||||
@@ -1807,15 +1844,10 @@ initializeDatabase().then(async () => {
|
|||||||
const gewuenscht = parseInt(req.query.seite, 10);
|
const gewuenscht = parseInt(req.query.seite, 10);
|
||||||
const seite = Math.min(Math.max(Number.isInteger(gewuenscht) ? gewuenscht : 1, 1), seiten);
|
const seite = Math.min(Math.max(Number.isInteger(gewuenscht) ? gewuenscht : 1, 1), seiten);
|
||||||
|
|
||||||
const aktivitaet = await dbAll(`
|
const aktivitaet = await loadAktivitaet(U, {
|
||||||
SELECT sv.datum, sv.status, sv.kommentar,
|
limit: AKTIVITAETEN_PRO_SEITE,
|
||||||
b.id AS bewerbung_id, b.firma, b.stelle
|
offset: (seite - 1) * AKTIVITAETEN_PRO_SEITE
|
||||||
FROM status_verlauf sv
|
});
|
||||||
LEFT JOIN bewerbungen b ON b.id = sv.bewerbung_id AND b.user_id = sv.user_id
|
|
||||||
WHERE sv.user_id = ?
|
|
||||||
ORDER BY datetime(sv.created_at) DESC, sv.id DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`, [U, AKTIVITAETEN_PRO_SEITE, (seite - 1) * AKTIVITAETEN_PRO_SEITE]);
|
|
||||||
|
|
||||||
res.render('aktivitaeten', {
|
res.render('aktivitaeten', {
|
||||||
aktivitaet,
|
aktivitaet,
|
||||||
@@ -1934,16 +1966,9 @@ initializeDatabase().then(async () => {
|
|||||||
{ label: 'Einstellung', count: ever('Einstellung') }
|
{ label: 'Einstellung', count: ever('Einstellung') }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Recent activity: latest status changes with the application they belong to.
|
// Recent activity: latest status changes and emails with the application
|
||||||
const aktivitaet = await dbAll(`
|
// they belong to.
|
||||||
SELECT sv.datum, sv.status, sv.kommentar, b.id as bewerbung_id,
|
const aktivitaet = await loadAktivitaet(U, { limit: 12 });
|
||||||
b.firma, b.stelle
|
|
||||||
FROM status_verlauf sv
|
|
||||||
LEFT JOIN bewerbungen b ON b.id = sv.bewerbung_id
|
|
||||||
WHERE sv.user_id = ?
|
|
||||||
ORDER BY datetime(sv.created_at) DESC, sv.id DESC
|
|
||||||
LIMIT 12
|
|
||||||
`, [U]);
|
|
||||||
|
|
||||||
res.render('statistik', {
|
res.render('statistik', {
|
||||||
total,
|
total,
|
||||||
|
|||||||
+22
-11
@@ -69,28 +69,39 @@
|
|||||||
|
|
||||||
<% if (aktivitaet.length) { %>
|
<% if (aktivitaet.length) { %>
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
||||||
<% aktivitaet.forEach(function (a) { %>
|
<% aktivitaet.forEach(function (a) {
|
||||||
|
var dotCls = a.typ === 'email'
|
||||||
|
? (a.direction === 'out' ? 'bg-sky-500' : 'bg-blue-500')
|
||||||
|
: (statusDot[a.status] || 'bg-gray-300');
|
||||||
|
var titel = '', titelSub = '';
|
||||||
|
if (a.bewerbung_id) {
|
||||||
|
titel = a.firma || '(ohne Firma)';
|
||||||
|
titelSub = a.stelle || '';
|
||||||
|
} else if (a.typ === 'email') {
|
||||||
|
titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail');
|
||||||
|
} else {
|
||||||
|
titel = 'Gelöschte Bewerbung';
|
||||||
|
}
|
||||||
|
%>
|
||||||
<li>
|
<li>
|
||||||
<% if (a.bewerbung_id) { %>
|
<% if (a.bewerbung_id) { %>
|
||||||
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="flex items-start gap-3 px-6 py-3">
|
<div class="flex items-start gap-3 px-6 py-3">
|
||||||
<% } %>
|
<% } %>
|
||||||
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= statusDot[a.status] || 'bg-gray-300' %>"></span>
|
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= dotCls %>"></span>
|
||||||
<span class="min-w-0 flex-1">
|
<span class="min-w-0 flex-1">
|
||||||
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||||
<% if (a.bewerbung_id) { %>
|
<span class="text-sm font-medium <%= a.bewerbung_id ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400' %> break-words"><%= titel %></span>
|
||||||
<span class="text-sm font-medium text-gray-900 dark:text-white break-words"><%= a.firma || '(ohne Firma)' %></span>
|
<% if (titelSub) { %>
|
||||||
<% if (a.stelle) { %>
|
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= titelSub %></span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= a.stelle %></span>
|
|
||||||
<% } %>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Gelöschte Bewerbung</span>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
|
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
|
||||||
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
|
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
|
||||||
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
|
<% if (a.datum) { %>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
|
||||||
|
<% } %>
|
||||||
<% if (a.kommentar && a.kommentar.trim()) { %>
|
<% if (a.kommentar && a.kommentar.trim()) { %>
|
||||||
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
|
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -102,7 +113,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="px-6 py-16 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div class="px-6 py-16 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
Noch keine Statusänderungen protokolliert.
|
Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|||||||
+31
-14
@@ -348,27 +348,42 @@
|
|||||||
|
|
||||||
<% if (aktivitaet && aktivitaet.length) { %>
|
<% if (aktivitaet && aktivitaet.length) { %>
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
||||||
<% aktivitaet.forEach(function (a) { %>
|
<% aktivitaet.forEach(function (a) {
|
||||||
|
// E-Mail-Einträge bekommen einen eigenen Farbpunkt; bei Status-
|
||||||
|
// änderungen greift die Status-Farbpalette.
|
||||||
|
var dotCls = a.typ === 'email'
|
||||||
|
? (a.direction === 'out' ? 'bg-sky-500' : 'bg-blue-500')
|
||||||
|
: (statusDot[a.status] || 'bg-gray-300');
|
||||||
|
// Kopzeile: verknüpfte Bewerbung -> Firma·Stelle, sonst bei E-Mail
|
||||||
|
// der Absender/Empfänger, sonst Hinweis auf gelöschte Bewerbung.
|
||||||
|
var titel = '', titelSub = '';
|
||||||
|
if (a.bewerbung_id) {
|
||||||
|
titel = a.firma || '(ohne Firma)';
|
||||||
|
titelSub = a.stelle || '';
|
||||||
|
} else if (a.typ === 'email') {
|
||||||
|
titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail');
|
||||||
|
titelSub = '';
|
||||||
|
} else {
|
||||||
|
titel = 'Gelöschte Bewerbung';
|
||||||
|
}
|
||||||
|
%>
|
||||||
<li>
|
<li>
|
||||||
<!-- Eintrag zu einer noch existierenden Bewerbung ist ein Link, einer zu
|
<!-- Eintrag zu einer noch existierenden Bewerbung ist ein Link, einer zu
|
||||||
einer gelöschten bleibt ein Block. Bewusst zwei ausgeschriebene Zweige:
|
einer gelöschten oder einer nicht zugeordneten E-Mail bleibt ein Block.
|
||||||
den Tag-Namen samt href per <%%= %> zusammenzusetzen escaped die
|
Bewusst zwei ausgeschriebene Zweige: den Tag-Namen samt href per <%%= %>
|
||||||
Anführungszeichen und erzeugt Links wie /%22/bewerbung/121%22. -->
|
zusammenzusetzen escaped die Anführungszeichen und erzeugt Links wie
|
||||||
|
/%22/bewerbung/121%22. -->
|
||||||
<% if (a.bewerbung_id) { %>
|
<% if (a.bewerbung_id) { %>
|
||||||
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="flex items-start gap-3 px-6 py-3">
|
<div class="flex items-start gap-3 px-6 py-3">
|
||||||
<% } %>
|
<% } %>
|
||||||
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= statusDot[a.status] || 'bg-gray-300' %>"></span>
|
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= dotCls %>"></span>
|
||||||
<span class="min-w-0 flex-1">
|
<span class="min-w-0 flex-1">
|
||||||
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||||
<% if (a.bewerbung_id) { %>
|
<span class="text-sm font-medium <%= a.bewerbung_id ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400' %> break-words"><%= titel %></span>
|
||||||
<span class="text-sm font-medium text-gray-900 dark:text-white break-words"><%= a.firma || '(ohne Firma)' %></span>
|
<% if (titelSub) { %>
|
||||||
<% if (a.stelle) { %>
|
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= titelSub %></span>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= a.stelle %></span>
|
|
||||||
<% } %>
|
|
||||||
<% } else { %>
|
|
||||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Gelöschte Bewerbung</span>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
<!-- Status, Datum und Kommentar in einer Zeile: ein rechtsbündiges
|
<!-- Status, Datum und Kommentar in einer Zeile: ein rechtsbündiges
|
||||||
@@ -376,7 +391,9 @@
|
|||||||
dort ohne Bezug stehen. -->
|
dort ohne Bezug stehen. -->
|
||||||
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
|
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
|
||||||
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
|
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
|
||||||
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
|
<% if (a.datum) { %>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
|
||||||
|
<% } %>
|
||||||
<% if (a.kommentar && a.kommentar.trim()) { %>
|
<% if (a.kommentar && a.kommentar.trim()) { %>
|
||||||
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
|
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -388,7 +405,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<div class="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div class="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
Noch keine Statusänderungen protokolliert.
|
Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+16
-9
@@ -309,7 +309,7 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h2 id="aktivitaet" class="text-gray-900 dark:text-white">Letzte Aktivität</h2>
|
<h2 id="aktivitaet" class="text-gray-900 dark:text-white">Letzte Aktivität</h2>
|
||||||
<p class="sub mb-5">
|
<p class="sub mb-5">
|
||||||
Die jüngsten Statusänderungen deiner Bewerbungen.
|
Die jüngsten Statusänderungen und E-Mails deiner Bewerbungen.
|
||||||
<a href="/aktivitaeten" class="text-blue-600 dark:text-sky-400 hover:underline">Alle</a>
|
<a href="/aktivitaeten" class="text-blue-600 dark:text-sky-400 hover:underline">Alle</a>
|
||||||
</p>
|
</p>
|
||||||
<% if (aktivitaet.length) {
|
<% if (aktivitaet.length) {
|
||||||
@@ -319,29 +319,36 @@
|
|||||||
'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');
|
||||||
|
const emailColor = (a) => a.direction === 'out' ? '#0ea5e9' : '#3b82f6';
|
||||||
%>
|
%>
|
||||||
<div class="tl">
|
<div class="tl">
|
||||||
<% aktivitaet.forEach(a => { %>
|
<% aktivitaet.forEach(a => {
|
||||||
<div class="tl-item" style="--stat-accent:<%= statusColor(a.status) %>">
|
const accent = a.typ === 'email' ? emailColor(a) : statusColor(a.status);
|
||||||
|
let titel = '', titelSub = '';
|
||||||
|
if (a.bewerbung_id) { titel = a.firma || '(ohne Firma)'; titelSub = a.stelle || ''; }
|
||||||
|
else if (a.typ === 'email') { titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail'); }
|
||||||
|
else { titel = 'Gelöschte Bewerbung'; }
|
||||||
|
%>
|
||||||
|
<div class="tl-item" style="--stat-accent:<%= accent %>">
|
||||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||||
<% if (a.bewerbung_id) { %>
|
<% if (a.bewerbung_id) { %>
|
||||||
<a href="/bewerbung/<%= a.bewerbung_id %>" class="text-sm font-medium text-gray-900 dark:text-white hover:underline"><%= a.firma || '(ohne Firma)' %></a>
|
<a href="/bewerbung/<%= a.bewerbung_id %>" class="text-sm font-medium text-gray-900 dark:text-white hover:underline"><%= titel %></a>
|
||||||
<span class="text-sm text-gray-500 dark:text-slate-400">· <%= a.stelle || '' %></span>
|
<% if (titelSub) { %><span class="text-sm text-gray-500 dark:text-slate-400">· <%= titelSub %></span><% } %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Gelöschte Bewerbung</span>
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-200"><%= titel %></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-sm">
|
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-sm">
|
||||||
<span class="inline-flex items-center gap-1.5 font-medium" style="color:<%= statusColor(a.status) %>">
|
<span class="inline-flex items-center gap-1.5 font-medium" style="color:<%= accent %>">
|
||||||
<span class="swatch" style="margin:0"></span><%= a.status %>
|
<span class="swatch" style="margin:0"></span><%= a.status %>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 dark:text-slate-500">· <%= a.datum %></span>
|
<% if (a.datum) { %><span class="text-gray-400 dark:text-slate-500">· <%= a.datum %></span><% } %>
|
||||||
<% if (a.kommentar) { %><span class="text-gray-500 dark:text-slate-400 truncate">· <%= a.kommentar %></span><% } %>
|
<% if (a.kommentar) { %><span class="text-gray-500 dark:text-slate-400 truncate">· <%= a.kommentar %></span><% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</div>
|
</div>
|
||||||
<% } else { %><div class="stat-empty">Noch keine Statusänderungen protokolliert.</div><% } %>
|
<% } else { %><div class="stat-empty">Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.</div><% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
Reference in New Issue
Block a user