Jobs: Bewerbungssystem für Social-Media-Manager/in
Backend:
- job_applications + job_application_docs Tabellen in DB
- luna_trial_until Spalte in users (Migration)
- routes/jobs.py: POST /apply (FormData + Datei-Upload, max 3×10MB),
GET /my-application, GET /luna-trial-status
- Admin: GET/PATCH /admin/applications, GET /admin/applications/{id}/docs/{doc_id}
- Bei Bewerbung: 14-Tage Luna-Probezugang automatisch aktiviert
- Bei Annahme: is_social_media=1 + Gründer-Status gesetzt
- Status-Mails (pending/reviewing/accepted/rejected) via email_html-Template
- auth.py: require_social_media prüft auch luna_trial_until
Frontend:
- pages/jobs.js: Stellenausschreibung + Bewerbungsformular
(Name, E-Mail, Hund, Social-Handle, Motivation, Datei-Upload)
- Luna-Probezugang Teaser mit Countdown wenn aktiv
- Bestehende Bewerbung: Status-Screen statt Formular
- app.js: 'jobs' Seite registriert
- admin.js: neuer Tab 'Bewerbungen' (filtert nach Status,
Statuswechsel per Dropdown, Detailansicht mit Anhang-Download,
Admin-Notiz-Feld)
- admin.js: Tab 'Jobs' → 'Scheduler' umbenannt
This commit is contained in:
parent
59856e61a1
commit
f378edab5d
7 changed files with 738 additions and 4 deletions
|
|
@ -18,7 +18,8 @@ window.Page_admin = (() => {
|
|||
{ id: 'social', label: 'Social Media', icon: 'camera' },
|
||||
{ id: 'analytics', label: 'Analytics', icon: 'target' },
|
||||
{ id: 'system', label: 'System', icon: 'gear' },
|
||||
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
|
||||
{ id: 'jobs', label: 'Scheduler', icon: 'clock' },
|
||||
{ id: 'bewerbungen', label: 'Bewerbungen', icon: 'user-plus' },
|
||||
{ id: 'partner', label: 'Partner', icon: 'handshake' },
|
||||
{ id: 'outreach', label: 'Outreach', icon: 'envelope-simple' },
|
||||
{ id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
|
||||
|
|
@ -93,6 +94,7 @@ window.Page_admin = (() => {
|
|||
case 'partner': await _renderPartner(el); break;
|
||||
case 'outreach': await _renderOutreach(el); break;
|
||||
case 'audit': await _renderAudit(el); break;
|
||||
case 'bewerbungen': await _renderBewerbungen(el); break;
|
||||
}
|
||||
} catch (e) {
|
||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||
|
|
@ -2375,6 +2377,125 @@ window.Page_admin = (() => {
|
|||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// BEWERBUNGEN — Social-Media-Job
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderBewerbungen(el) {
|
||||
let _statusFilter = 'pending';
|
||||
|
||||
async function _load() {
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4);flex-wrap:wrap;align-items:center">
|
||||
${['pending','reviewing','accepted','rejected','alle'].map(s => `
|
||||
<button class="btn btn-sm ${s===_statusFilter?'btn-primary':'btn-ghost'} adm-bew-filter" data-s="${s}">
|
||||
${s==='pending'?'⏳ Neu':s==='reviewing'?'🔍 In Prüfung':s==='accepted'?'✅ Angenommen':s==='rejected'?'❌ Abgelehnt':'Alle'}
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
<div id="adm-bew-list">${UI.skeleton(3)}</div>`;
|
||||
|
||||
el.querySelectorAll('.adm-bew-filter').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_statusFilter = btn.dataset.s;
|
||||
_load();
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const rows = await API.get(`/jobs/admin/applications?status=${_statusFilter}`);
|
||||
const list = el.querySelector('#adm-bew-list');
|
||||
if (!rows.length) {
|
||||
list.innerHTML = _emptyState('user-plus', 'Keine Bewerbungen', 'Noch keine Bewerbungen in diesem Status.');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(r => `
|
||||
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)" data-id="${r.id}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3)">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:700;font-size:var(--text-base)">${_esc(r.name)}
|
||||
${r.username ? `<span style="color:var(--c-text-muted);font-weight:400;font-size:var(--text-sm)">(@${_esc(r.username)})</span>` : ''}
|
||||
</div>
|
||||
<div style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-top:2px">
|
||||
${_esc(r.email)} · @${_esc(r.social_handle||'—')}
|
||||
${r.dog_name ? ` · 🐕 ${_esc(r.dog_name)} (${_esc(r.dog_rasse||'')})` : ''}
|
||||
</div>
|
||||
<div style="color:var(--c-text-muted);font-size:var(--text-xs);margin-top:2px">
|
||||
${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge
|
||||
</div>
|
||||
<div style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)">
|
||||
${_esc((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2);min-width:120px">
|
||||
<button class="btn btn-sm btn-primary adm-bew-view" data-id="${r.id}">Details</button>
|
||||
<select class="form-control adm-bew-status" data-id="${r.id}"
|
||||
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2)">
|
||||
<option value="pending" ${r.status==='pending' ?'selected':''}>⏳ Neu</option>
|
||||
<option value="reviewing" ${r.status==='reviewing'?'selected':''}>🔍 Prüfung</option>
|
||||
<option value="accepted" ${r.status==='accepted' ?'selected':''}>✅ Angenommen</option>
|
||||
<option value="rejected" ${r.status==='rejected' ?'selected':''}>❌ Abgelehnt</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
|
||||
list.querySelectorAll('.adm-bew-status').forEach(sel => {
|
||||
sel.addEventListener('change', async () => {
|
||||
const id = sel.dataset.id;
|
||||
await API.patch(`/jobs/admin/applications/${id}`, { status: sel.value });
|
||||
UI.toast.success('Status aktualisiert.');
|
||||
setTimeout(_load, 500);
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('.adm-bew-view').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
const app = await API.get(`/jobs/admin/applications/${id}`);
|
||||
const docsHtml = app.docs?.length
|
||||
? app.docs.map(d => `<a href="/api/jobs/admin/applications/${id}/docs/${d.id}"
|
||||
target="_blank" style="display:block;color:var(--c-primary);font-size:var(--text-sm);margin:4px 0">
|
||||
📎 ${_esc(d.filename)}</a>`).join('')
|
||||
: '<span style="color:var(--c-text-muted);font-size:var(--text-sm)">Keine Anhänge</span>';
|
||||
|
||||
UI.modal.open({
|
||||
title: `Bewerbung — ${_esc(app.name)}`,
|
||||
body: `
|
||||
<div style="display:grid;gap:var(--space-3)">
|
||||
<div><b>E-Mail:</b> ${_esc(app.email)}</div>
|
||||
<div><b>Social:</b> @${_esc(app.social_handle||'—')}</div>
|
||||
${app.dog_name ? `<div><b>Hund:</b> ${_esc(app.dog_name)} (${_esc(app.dog_rasse||'')})</div>` : ''}
|
||||
<div><b>Motivation:</b><br>
|
||||
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3);
|
||||
margin-top:var(--space-1);font-size:var(--text-sm);white-space:pre-wrap">${_esc(app.motivation)}</div>
|
||||
</div>
|
||||
<div><b>Anhänge:</b><br>${docsHtml}</div>
|
||||
<div>
|
||||
<b>Admin-Notiz:</b>
|
||||
<textarea id="adm-bew-note" class="form-control" rows="2" style="margin-top:var(--space-1)"
|
||||
placeholder="Interne Notiz / Nachricht an Bewerber">${_esc(app.admin_note||'')}</textarea>
|
||||
</div>
|
||||
</div>`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
|
||||
<button class="btn btn-primary" id="adm-bew-save-note">Notiz speichern</button>`,
|
||||
});
|
||||
document.getElementById('adm-bew-save-note')?.addEventListener('click', async () => {
|
||||
const note = document.getElementById('adm-bew-note')?.value || '';
|
||||
await API.patch(`/jobs/admin/applications/${id}`, { admin_note: note });
|
||||
UI.toast.success('Notiz gespeichert.');
|
||||
UI.modal.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
el.querySelector('#adm-bew-list').innerHTML = _emptyState('warning', 'Fehler', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
await _load();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue