HR Recruiting
SCOPE: pre-hire
๐Ÿ‡ฎ๐Ÿ‡น Italia โ–พ tenant: โ€” โ€”
?
โณ Caricamento HR Recruitingโ€ฆ
`; w.document.write(html); w.document.close(); } function _saveFile(content, filename, type) { const blob = new Blob([content], { type:type+';charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 200); } VIEWS.export = async function(main) { main.innerHTML = `

๐Ÿ“ค Export report recruiting

Genera report PDF/Excel/CSV con dati funnel + candidati attivi
๐Ÿ“„
PDF stampabile
๐Ÿ“Š
Excel (.xls)
๐Ÿ“‹
CSV (raw data)
๐Ÿ“ฆ
JSON (API export)
I report includono: funnel candidature ultimi 30gg + lista candidati attivi (max 500) per ${STATE.tenant}/${STATE.country}.
`; document.getElementById('ex-pdf').addEventListener('click', () => exportRecruitingData('pdf')); document.getElementById('ex-excel').addEventListener('click', () => exportRecruitingData('excel')); document.getElementById('ex-csv').addEventListener('click', () => exportRecruitingData('csv')); document.getElementById('ex-json').addEventListener('click', async () => { const r = await apiPost('/api/search', { jql:`project="${STATE.tenant}" AND issuetype="${STATE.tenant}-HR-CAN" AND labels="country:${STATE.country}"`, fields:['summary','status','labels'], maxResults:500 }); _saveFile(JSON.stringify(r, null, 2), `recruiting-${STATE.tenant}-${STATE.country}.json`, 'application/json'); toast('JSON scaricato', 'success'); }); }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #4 ยท GDPR (export dati candidato + right-to-forget) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['gdpr-export'] = async function(main) { main.innerHTML = `

๐Ÿ“ฅ Export dati candidato (GDPR)

Genera ZIP con tutti i dati personali del candidato per richiesta DSAR
๐Ÿ›ก GDPR Article 15 (Right of access) ยท Genera un export completo: profilo, CV, comunicazioni, valutazioni, audit log. Tempo medio fulfillment: 30 giorni (max 1 mese per legge).
Storico richieste GDPR
โณ
`; document.getElementById('gdpr-search').addEventListener('click', _gdprSearch); await _gdprHistory(); }; async function _gdprSearch() { const id = document.getElementById('gdpr-id').value.trim(); if (!id) { toast('Inserisci candidate key o email', 'warning'); return; } document.getElementById('gdpr-result').innerHTML = '
โณ
'; try { const r = await apiPost('/api/hr/recruiting/gdpr/lookup', { query: id }); if (!r.candidate) { document.getElementById('gdpr-result').innerHTML = '
Nessun candidato trovato
'; return; } const c = r.candidate; document.getElementById('gdpr-result').innerHTML = `
${escHtml(c.name||'?')} ${escHtml(c.candidate_key)}
Email: ${escHtml(c.email||'โ€”')}
Stage: ${escHtml(c.stage||'โ€”')}
Source: ${escHtml(c.source||'โ€”')}
Created: ${escHtml(c.created||'โ€”')}
Country: ${escHtml(c.country||'โ€”')}
`; } catch (e) { document.getElementById('gdpr-result').innerHTML = '
Errore: '+e.message+'
'; } } window._gdprExport = async function(candKey) { toast('Generazione export GDPRโ€ฆ', 'info'); try { const r = await apiPost('/api/hr/recruiting/gdpr/export', { candidate_key: candKey }); if (r.ok) { _saveFile(JSON.stringify(r.data, null, 2), `gdpr-export-${candKey}-${Date.now()}.json`, 'application/json'); toast('Export GDPR generato e scaricato', 'success'); _gdprHistory(); } else { toast('Errore: '+r.error, 'error'); } } catch (e) { toast('Errore: '+e.message, 'error'); } }; async function _gdprHistory() { try { const r = await api('/api/hr/recruiting/gdpr/history?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)); const exports = (r.exports || []).map(x => ({ ...x, action:'gdpr-export', candidate_key: x.key.split(':').slice(-2,-1)[0] || '?' })); const forgets = (r.forgets || []).map(x => ({ ...x, action:'gdpr-forget', candidate_key: 'hash:'+(x.hash||'').slice(0,12) })); const items = [...exports, ...forgets].sort((a,b) => (b.ts||0)-(a.ts||0)); if (!items.length) { document.getElementById('gdpr-history').innerHTML = '
Nessuna richiesta passata
'; return; } document.getElementById('gdpr-history').innerHTML = `${items.map(h => ``).join('')}
DataTipoCandidatoRichiedenteStato
${new Date(h.ts).toLocaleString('it-IT')} ${escHtml(h.action)} ${escHtml(h.candidate_key)} ${escHtml(h.requester||'?')} completed
`; } catch (e) { document.getElementById('gdpr-history').innerHTML = '
Errore: '+e.message+'
'; } } VIEWS['gdpr-forget'] = async function(main) { main.innerHTML = `

๐Ÿ—‘ Right-to-forget (GDPR)

Cancellazione completa dati candidato ยท operazione IRREVERSIBILE
โš  ATTENZIONE ยท Questa azione cancella PERMANENTEMENTE: profilo Jira, CV in R2, KV records, comunicazioni, valutazioni colloqui. Resta solo l'audit log (anonimizzato) per compliance.
L'operazione richiede doppio conferma e 2 ruoli amministrativi (richiedente + approvatore hr_admin).
`; document.getElementById('forget-btn').addEventListener('click', _gdprForget); }; async function _gdprForget() { const id = document.getElementById('forget-id').value.trim(); if (!id) { toast('Inserisci candidate key', 'warning'); return; } if (!confirm(`โš  Cancellare PERMANENTEMENTE tutti i dati di ${id}?\n\nQuesta azione รจ IRREVERSIBILE.`)) return; const c2 = prompt(`Per confermare digita: DELETE ${id}`); if (c2 !== `DELETE ${id}`) { toast('Conferma non valida', 'warning'); return; } try { const r = await apiPost(`/api/hr/recruiting/gdpr/forget?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`, { candidate_id: id, confirm: `DELETE ${id}` }); if (r.ok) { document.getElementById('forget-result').innerHTML = `
โœ“ Cancellazione completata ยท ${r.deleted||0} chiavi KV eliminate ยท hash audit non-PII ${escHtml((r.hash||'').slice(0,16))}โ€ฆ ยท timestamp ${new Date(r.ts).toLocaleString('it-IT')}.
`; toast('Cancellazione GDPR completata', 'success'); } else { toast('Errore: '+r.error, 'error'); } } catch (e) { toast('Errore: '+e.message, 'error'); } } VIEWS['audit-log'] = async function(main) { main.innerHTML = `

๐Ÿ“‹ Audit log recruiting

Log completo accessi/modifiche ยท retention 90gg ยท country-scoped
โณ
`; document.getElementById('al-search').addEventListener('click', _alLoad); await _alLoad(); }; async function _alLoad() { const action = document.getElementById('al-action')?.value || ''; const user = document.getElementById('al-user')?.value || ''; try { const r = await api('/api/hr/recruiting/audit?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)+'&action='+encodeURIComponent(action)+'&user='+encodeURIComponent(user)); const items = r.items || []; if (!items.length) { document.getElementById('al-list').innerHTML = '
Nessun log
'; return; } document.getElementById('al-list').innerHTML = `${items.slice(0,200).map(e => ``).join('')}
TimestampUserActionTargetIPMeta
${new Date(e.ts).toLocaleString('it-IT')} ${escHtml(e.userid||'?')} ${escHtml(e.action)} ${escHtml(e.target||e.candidate_key||e.candidateKey||'โ€”')} ${escHtml(e.ip||'โ€”')} ${escHtml(JSON.stringify(e).slice(0,80))}
${items.length > 200 ? '

โ€ฆ +'+(items.length-200)+' righe

' : ''}`; } catch (e) { document.getElementById('al-list').innerHTML = '
Errore: '+e.message+'
'; } } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #5 ยท CRM CANDIDATO DETAIL // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['candidates-list'] = async function(main) { main.innerHTML = `

๐Ÿ“‹ Lista candidati ยท CRM

Tutti i candidati attivi ยท click per profilo completo
โณ
`; document.getElementById('cl-search').addEventListener('input', e => _clLoad(e.target.value)); await _clLoad(''); }; async function _clLoad(q) { try { const data = await apiPost('/api/search', { jql: `project="${STATE.tenant}" AND issuetype="${STATE.tenant}-HR-CAN" AND labels="country:${STATE.country}"${q?` AND text ~ "${q.replace(/"/g,'')}"`:''} ORDER BY created DESC`, fields: ['summary','status','priority','created','labels','reporter','assignee'], maxResults: 100, }); const items = data.issues || []; if (!items.length) { document.getElementById('cl-list').innerHTML = '
๐Ÿ“ญ Nessun candidato
'; return; } document.getElementById('cl-list').innerHTML = `
${items.map(c => { const labels = c.fields.labels || []; const stage = labels.find(l => l.startsWith('stage:'))?.replace('stage:','') || 'applied'; const source = labels.find(l => l.startsWith('source:'))?.replace('source:','') || 'โ€”'; const score = labels.find(l => l.startsWith('match:'))?.replace('match:','') || null; const stageColors = { applied:'#3b82f6', screening:'#f59e0b', interview:'#a855f7', offer:'#06b6d4', hired:'#16a34a', rejected:'#ef4444' }; return `
${escHtml(c.fields.summary||'')}
${escHtml(c.key)} ยท ${(c.fields.created||'').slice(0,10)}
${stage} ${source} ${score ? `๐ŸŽฏ ${score}%` : ''}
${c.fields.assignee ? `
โ†’ ${escHtml(c.fields.assignee.displayName)}
` : ''}
`; }).join('')}
`; } catch (e) { document.getElementById('cl-list').innerHTML = '
Errore: '+e.message+'
'; } } window.openCandidateDetail = async function(candKey) { const main = document.getElementById('main'); main.innerHTML = '
โณ Caricamento profiloโ€ฆ
'; try { const detail = await api(`/api/hr/recruiting/candidate/detail?key=${encodeURIComponent(candKey)}&project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`); const c = detail.profile; if (!c) { main.innerHTML = '
Candidato non trovato
'; return; } const f = c.fields || {}; const notes = { items: detail.notes || [] }; const comm = { comments: (f.comment?.comments) || [] }; const labels = f.labels || []; const stage = labels.find(l => l.startsWith('stage:'))?.replace('stage:','') || 'applied'; const source = labels.find(l => l.startsWith('source:'))?.replace('source:','') || 'โ€”'; const skills = labels.filter(l => l.startsWith('skill:')).map(l => l.replace('skill:','')); main.innerHTML = `

${escHtml(f.summary||candKey)}

${candKey} ยท stage: ${stage} ยท source: ${source}
๐Ÿ“„ Profilo
${escHtml(f.description?.content?.[0]?.content?.[0]?.text || '(nessuna descrizione)')}
${skills.length ? `
Skill
${skills.map(s=>`${escHtml(s)}`).join('')}
` : ''}
๐Ÿ“œ Cronologia (${(comm.comments||[]).length} commenti)
${(comm.comments||[]).map(co => `
${escHtml(co.author?.displayName||'?')}${(co.created||'').slice(0,16)}
${escHtml(co.body?.content?.[0]?.content?.[0]?.text || '')}
`).join('') || '
Nessun commento
'}
๐Ÿ’ฌ Note interne (HR-only)
${(notes.items||[]).map(n => `
${escHtml(n.author||'?')} ยท ${new Date(n.created_at).toLocaleString('it-IT')}
${escHtml(n.text||'')}
`).join('') || '
Nessuna nota
'}
๐Ÿ“‹ Info
Reporter: ${escHtml(f.reporter?.displayName||'โ€”')}
Assegnato a: ${escHtml(f.assignee?.displayName||'Non assegnato')}
Status: ${escHtml(f.status?.name||'โ€”')}
Prioritร : ${escHtml(f.priority?.name||'โ€”')}
Creato: ${(f.created||'').slice(0,10)}
${(f.attachment||[]).length ? `
๐Ÿ“Ž Allegati (CV, portfolio)
${f.attachment.map(a => `๐Ÿ“ฅ ${escHtml(a.filename||'file')}`).join('')}
` : ''}
๐Ÿš€ Azioni rapide
`; } catch (e) { main.innerHTML = '
Errore: '+e.message+'
'; } }; window._cdAddNote = async function(candKey) { const t = document.getElementById('cd-newnote').value.trim(); if (!t) return; try { await apiPost('/api/hr/recruiting/notes/add', { candidate_key: candKey, text: t }); toast('Nota aggiunta', 'success'); openCandidateDetail(candKey); } catch (e) { toast('Errore: '+e.message, 'error'); } }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #6 ยท TEST EDITOR INTERATTIVO // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• let _testEditState = null; window._ttNewTemplate = async function() { // Override del placeholder con vero editor _testEditState = { id: 'test-'+Date.now().toString(36), title:'', skill:'', duration_min:30, passing_score:60, questions:[] }; _testEditOpen(); }; function _testEditOpen() { const m = document.createElement('div'); m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'; m.onclick = (e) => { if (e.target === m) m.remove(); }; m.innerHTML = `

๐Ÿ“ Editor test

Domande (${_testEditState.questions.length})

`; document.body.appendChild(m); document.getElementById('te-addq').addEventListener('click', _testAddQ); document.getElementById('te-save').addEventListener('click', _testSave); _testRenderQuestions(); } function _testAddQ() { _testEditState.questions.push({ id: 'q'+(_testEditState.questions.length+1), text: '', type: 'choice', options: ['', '', '', ''], correct: 0, weight: 1, }); _testRenderQuestions(); } function _testRenderQuestions() { const c = document.getElementById('te-questions'); c.innerHTML = _testEditState.questions.map((q, idx) => `
Domanda ${idx+1}
${(q.options||[]).map((o,oi) => ``).join('')}
`).join(''); c.querySelectorAll('[data-q-text]').forEach(t => t.addEventListener('input', e => { _testEditState.questions[+t.dataset.qText].text = t.value; })); c.querySelectorAll('[data-q-opt]').forEach(i => i.addEventListener('input', e => { const [qi,oi] = i.dataset.qOpt.split('-').map(Number); _testEditState.questions[qi].options[oi] = i.value; })); c.querySelectorAll('[data-q-correct]').forEach(r => r.addEventListener('change', () => { const [qi,oi] = r.dataset.qCorrect.split('-').map(Number); _testEditState.questions[qi].correct = oi; })); c.querySelectorAll('[data-q-weight]').forEach(w => w.addEventListener('input', () => { _testEditState.questions[+w.dataset.qWeight].weight = parseInt(w.value,10)||1; })); c.querySelectorAll('[data-q-del]').forEach(b => b.addEventListener('click', () => { _testEditState.questions.splice(+b.dataset.qDel, 1); _testRenderQuestions(); })); } async function _testSave() { _testEditState.title = document.getElementById('te-title').value; _testEditState.skill = document.getElementById('te-skill').value; _testEditState.duration_min = parseInt(document.getElementById('te-duration').value,10)||30; _testEditState.passing_score = parseInt(document.getElementById('te-passing').value,10)||60; if (!_testEditState.title || !_testEditState.questions.length) { toast('Titolo + almeno 1 domanda obbligatori', 'warning'); return; } try { const r = await apiPost('/api/hr/recruiting/tests/save', _testEditState); if (r.ok) { toast('Test salvato', 'success'); document.querySelectorAll('div[style*="position:fixed"]').forEach(d=>d.remove()); if (typeof _ttLoad === 'function') _ttLoad(); } } catch (e) { toast('Errore: '+e.message, 'error'); } } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #7 ยท VIDEO SCREENING UPLOAD // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['video-screening'] = async function(main) { main.innerHTML = `

๐ŸŽฌ Video screening

Colloqui pre-registrati ยท domande standard ยท review asincrono
Il candidato riceve un link, registra le risposte (max 90s/domanda) tramite browser webcam. Video salvati cifrati in R2 ยท GDPR retention 6 mesi.
Inviti pendenti
โ€”
In review
โ€”
Completati
โ€”
Storage usato
โ€”
Sessioni video
โณ
`; document.getElementById('vs-questions').addEventListener('click', _vsQuestions); document.getElementById('vs-invite').addEventListener('click', _vsInvite); await _vsLoad(); }; async function _vsLoad() { try { const r = await api('/api/hr/recruiting/video/list?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)); const items = r.items || []; document.getElementById('vs-pending').textContent = items.filter(x=>x.status==='pending').length; document.getElementById('vs-review').textContent = items.filter(x=>x.status==='review').length; document.getElementById('vs-done').textContent = items.filter(x=>x.status==='completed').length; document.getElementById('vs-storage').textContent = ((r.total_bytes||0)/(1024*1024)).toFixed(1)+'MB'; if (!items.length) { document.getElementById('vs-list').innerHTML = '
Nessuna sessione
'; return; } document.getElementById('vs-list').innerHTML = `${items.map(v => ``).join('')}
CandidatoSet domandeStatusInviatoCompletatoScore
${escHtml(v.candidate_name||v.candidate_key)} ${escHtml(v.question_set||'โ€”')} ${v.status} ${(v.invited_at||'').slice(0,10)} ${(v.completed_at||'โ€”').slice(0,10)} ${v.review_score!=null?v.review_score+'/5':'โ€”'} ${v.video_urls?.length ? `` : ''}
`; } catch (e) { document.getElementById('vs-list').innerHTML = '
Errore: '+e.message+'
'; } } function _vsQuestions() { alert('Set domande standard:\n\n1. Presentazione personale (60s)\n2. Perchรฉ ti interessa la posizione (90s)\n3. Esperienza piรน rilevante (90s)\n4. Punti di forza (60s)\n5. Domanda libera (90s)\n\nEditor set custom in arrivo.'); } function _vsInvite() { const candKey = prompt('Candidate key:'); if (!candKey) return; const set = prompt('Set domande:', 'standard-5'); apiPost('/api/hr/recruiting/video/invite', { candidate_key: candKey, question_set: set }) .then(r => { toast(`Invito inviato ยท link: ${r.video_url||'check email'}`, 'success'); _vsLoad(); }) .catch(e => toast('Errore: '+e.message, 'error')); } window._vsReview = async function(id) { // Open review modal con video player + form scoring const m = document.createElement('div'); m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'; m.innerHTML = `

๐ŸŽฌ Review video screening

โณ Loadingโ€ฆ
${[1,2,3,4,5].map(n => ``).join('')}
`; document.body.appendChild(m); let score = null; m.querySelectorAll('[data-vs-score]').forEach(b => b.addEventListener('click', () => { score = parseInt(b.dataset.vsScore,10); m.querySelectorAll('[data-vs-score]').forEach(x => x.classList.remove('primary')); b.classList.add('primary'); })); // Load player try { const r = await api(`/api/hr/recruiting/video/${id}?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`); document.getElementById('vs-player').innerHTML = (r.video_urls||[]).map((u, i) => `
Domanda ${i+1}
`).join('') || '
Nessun video
'; } catch (e) { document.getElementById('vs-player').innerHTML = '
Errore: '+e.message+'
'; } document.getElementById('vs-save').addEventListener('click', async () => { if (!score) { toast('Seleziona uno score', 'warning'); return; } try { await apiPost(`/api/hr/recruiting/video/${id}/review`, { score, notes: document.getElementById('vs-notes').value }); toast('Review salvato', 'success'); m.remove(); _vsLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } }); }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #8 ยท TALENT POOL & TAG // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['talent-pool'] = async function(main) { main.innerHTML = `

๐Ÿท Talent pool & tag

Candidati passivi ยท tagging custom ยท match futuri quando apre nuova posizione
Candidati nel pool
โ€”
Tag definiti
โ€”
Re-engaged 30gg
โ€”
Match futuri auto
โ€”
Tag attivi
โณ
Candidati nel talent pool
โณ
`; document.getElementById('tp-tag').addEventListener('click', _tpCreateTag); await _tpLoad(); }; async function _tpLoad() { try { const r = await api('/api/hr/recruiting/talent-pool?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)); document.getElementById('tp-total').textContent = (r.candidates||[]).length; document.getElementById('tp-tags').textContent = (r.tags||[]).length; document.getElementById('tp-reeng').textContent = r.reengaged_30d||0; document.getElementById('tp-fmatch').textContent = r.future_matches||0; document.getElementById('tp-tag-cloud').innerHTML = (r.tags||[]).map(t => ` ๐Ÿท ${escHtml(t.name)} ${t.count||0} `).join('') || 'Nessun tag'; if (!(r.candidates||[]).length) { document.getElementById('tp-list').innerHTML = '
Pool vuoto
'; return; } document.getElementById('tp-list').innerHTML = `${(r.candidates||[]).map(c => ``).join('')}
CandidatoTagSkillLast contactMatch potenziali
${escHtml(c.name||c.key)} ${escHtml(c.key)} ${(c.tags||[]).map(t=>`${escHtml(t)}`).join(' ')} ${(c.skills||[]).slice(0,3).map(s=>`${escHtml(s)}`).join(' ')} ${(c.last_contact||'โ€”').slice(0,10)} ${c.future_matches||0}
`; } catch (e) { document.getElementById('tp-list').innerHTML = '
Errore: '+e.message+'
'; } } async function _tpCreateTag() { const name = prompt('Nome tag (es. "senior-backend", "passive-talent"):'); if (!name) return; try { await apiPost('/api/hr/recruiting/talent-pool/tag', { name }); toast('Tag creato', 'success'); _tpLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } } window._tpFilterTag = async (tag) => { toast(`Filtro tag: ${tag} (in arrivo)`, 'info'); }; window._tpReengage = async (candKey) => { if (!confirm(`Inviare re-engagement email a ${candKey}?`)) return; try { await apiPost('/api/hr/recruiting/talent-pool/reengage', { candidate_key: candKey }); toast('Email re-engagement inviata', 'success'); _tpLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } }; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #9 ยท BULK IMPORT CV // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['bulk-import'] = async function(main) { main.innerHTML = `

๐Ÿ“ค Bulk import CV

Upload massivo CV ยท parsing AI parallelo ยท review prima import
Carica fino a 50 file PDF/DOCX in una volta. Ogni CV viene parsed via AI (Anthropic/Azure) in parallelo. Tempo stimato: ~3s/CV. Costo: ~โ‚ฌ0.04/CV.
๐Ÿ“
Trascina qui i CV o click per selezionare
PDF ยท DOCX ยท max 10MB ciascuno ยท max 50 file
`; const drop = document.getElementById('bi-drop'); const fileInput = document.getElementById('bi-files'); drop.addEventListener('click', () => fileInput.click()); drop.addEventListener('dragover', e => { e.preventDefault(); drop.style.background='rgba(99,102,241,.15)'; }); drop.addEventListener('dragleave', () => { drop.style.background='rgba(99,102,241,.05)'; }); drop.addEventListener('drop', e => { e.preventDefault(); drop.style.background='rgba(99,102,241,.05)'; _biUpload(e.dataTransfer.files); }); fileInput.addEventListener('change', e => _biUpload(e.target.files)); }; async function _biUpload(files) { if (!files.length) return; const arr = Array.from(files).slice(0, 50); const prog = document.getElementById('bi-progress'); const res = document.getElementById('bi-results'); prog.innerHTML = `
โณ Upload ${arr.length} file in corsoโ€ฆ
`; let parsed = 0, failed = 0; const results = []; for (let i = 0; i < arr.length; i++) { const f = arr[i]; prog.innerHTML = `
โณ ${i+1}/${arr.length}: ${escHtml(f.name)}โ€ฆ
`; try { const fd = new FormData(); fd.append('file', f); fd.append('project', STATE.tenant); fd.append('country', STATE.country); const r = await fetch(`${_workerUrl()}/api/hr/recruiting/cv/upload`, { method:'POST', headers: { ..._headers(), 'Content-Type': undefined }, body: fd, }).then(r => r.json()); if (r.ok) { parsed++; results.push({ file:f.name, ok:true, name:r.parsed?.full_name, email:r.parsed?.email, skills:(r.parsed?.skills||[]).slice(0,5) }); } else { failed++; results.push({ file:f.name, ok:false, error:r.error }); } } catch (e) { failed++; results.push({ file:f.name, ok:false, error:e.message }); } } prog.innerHTML = `
โœ“ ${parsed} parsed ยท ${failed} errori ยท ${arr.length} totali
`; res.innerHTML = `
Risultati parsing
${results.map(r => ``).join('')}
FileStatusNomeEmailTop skill
${escHtml(r.file)} ${r.ok ? 'โœ“ OK' : 'โœ— FAIL'} ${escHtml(r.name||'โ€”')} ${escHtml(r.email||'โ€”')} ${(r.skills||[]).map(s=>`${escHtml(s)}`).join(' ')}
`; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #10 ยท EMAIL FOLLOW-UP AUTO // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS['auto-email'] = async function(main) { main.innerHTML = `

๐Ÿ“จ Email follow-up auto

Template configurabili ยท trigger su stage change ยท personalization tokens
Template attivi
โณ
`; document.getElementById('ae-new').addEventListener('click', () => _aeEdit(null)); await _aeLoad(); }; async function _aeLoad() { try { const r = await api('/api/hr/recruiting/email-templates/list?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)); const items = r.items || []; if (!items.length) { document.getElementById('ae-list').innerHTML = `
Nessun template. Crea il primo per automatizzare le email follow-up.
`; return; } document.getElementById('ae-list').innerHTML = `${items.map(t => ``).join('')}
NomeTriggerLinguaInviate 30ggOpen rate
${escHtml(t.name)} ${escHtml(t.trigger)} ${escHtml(t.lang)} ${t.sent_30d||0} ${t.open_rate!=null ? t.open_rate+'%' : 'โ€”'}
`; document.querySelectorAll('[data-ae-edit]').forEach(b => b.addEventListener('click', () => _aeEdit(b.dataset.aeEdit))); document.querySelectorAll('[data-ae-del]').forEach(b => b.addEventListener('click', () => _aeDel(b.dataset.aeDel))); } catch (e) { document.getElementById('ae-list').innerHTML = '
Errore: '+e.message+'
'; } } function _aeEdit(id) { const m = document.createElement('div'); m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'; m.onclick = (e) => { if (e.target === m) m.remove(); }; m.innerHTML = `

${id?'โœ๏ธ Modifica':'+ Nuovo'} template email

`; document.body.appendChild(m); if (id) { api(`/api/hr/recruiting/email-templates/list?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`).then(r => { const t = (r.items||[]).find(x => x.id === id) || {}; document.getElementById('ae-name').value = t.name||''; document.getElementById('ae-trigger').value = t.trigger||'application-received'; document.getElementById('ae-lang').value = t.lang||'it'; document.getElementById('ae-subject').value = t.subject||''; document.getElementById('ae-body').value = t.body||''; }).catch(e => toast('Errore caricamento template: '+e.message, 'error')); } document.getElementById('ae-save').addEventListener('click', async () => { const data = { id, name: document.getElementById('ae-name').value, trigger: document.getElementById('ae-trigger').value, lang: document.getElementById('ae-lang').value, subject: document.getElementById('ae-subject').value, body: document.getElementById('ae-body').value, }; if (!data.name || !data.subject) { toast('Nome + subject obbligatori', 'warning'); return; } try { await apiPost('/api/hr/recruiting/email-templates/save', data); toast('Template salvato', 'success'); m.remove(); _aeLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } }); } async function _aeDel(id) { if (!confirm('Eliminare template?')) return; try { await api(`/api/hr/recruiting/email-templates/${id}?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`, { method:'DELETE' }); toast('Eliminato','success'); _aeLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ๐Ÿ†• IMPL #11 ยท WORKFLOW AUTOMATIONS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• VIEWS.workflows = async function(main) { main.innerHTML = `

โšก Workflow automations

If-this-then-that recruiting ยท trigger custom ยท log esecuzioni
Crea automazioni per accelerare il processo: es. "Quando stage = offer โ†’ invia email + crea ticket onboarding draft + assegna a HR Manager".
Workflow attivi
โณ
Log esecuzioni recenti
โณ
`; document.getElementById('wf-new').addEventListener('click', () => _wfEdit(null)); await _wfLoad(); }; async function _wfLoad() { try { const [wf, log] = await Promise.all([ api('/api/hr/recruiting/workflows/list?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)), api('/api/hr/recruiting/workflows/log?project='+encodeURIComponent(STATE.tenant)+'&country='+encodeURIComponent(STATE.country)), ]); const items = wf.items || []; if (!items.length) { document.getElementById('wf-list').innerHTML = '
Nessun workflow. Crea il primo per automatizzare i processi recruiting.
'; } else { document.getElementById('wf-list').innerHTML = `${items.map(w => ``).join('')}
NomeTriggerAzioniStatoEseguito 30gg
${escHtml(w.name)} ${escHtml(w.trigger)} ${(w.actions||[]).length} ${w.enabled?'โœ“ active':'โธ paused'} ${w.runs_30d||0}
`; document.querySelectorAll('[data-wf-edit]').forEach(b => b.addEventListener('click', () => _wfEdit(b.dataset.wfEdit))); document.querySelectorAll('[data-wf-del]').forEach(b => b.addEventListener('click', () => _wfDel(b.dataset.wfDel))); document.querySelectorAll('[data-wf-toggle]').forEach(b => b.addEventListener('click', () => _wfToggle(b.dataset.wfToggle))); } const logItems = log.items || []; if (!logItems.length) { document.getElementById('wf-log').innerHTML = '
Nessuna esecuzione recente
'; return; } document.getElementById('wf-log').innerHTML = `${logItems.map(l => ``).join('')}
TimestampWorkflowTrigger eventStatusAzioni eseguite
${new Date(l.ts).toLocaleString('it-IT')} ${escHtml(l.workflow_name||l.workflow_id)} ${escHtml(l.trigger_event||'')} ${l.status} ${(l.actions_executed||[]).length}
`; } catch (e) { document.getElementById('wf-list').innerHTML = '
Errore: '+e.message+'
'; } } function _wfEdit(id) { const m = document.createElement('div'); m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'; m.onclick = (e) => { if (e.target === m) m.remove(); }; m.innerHTML = `

${id?'โœ๏ธ Modifica':'+ Nuovo'} workflow

`; document.body.appendChild(m); let actions = []; function renderActions() { document.getElementById('wf-actions').innerHTML = actions.length ? actions.map((a, i) => `
`).join('') : '
Nessuna azione
'; document.querySelectorAll('[data-wf-act]').forEach(s => s.addEventListener('change', e => { actions[+s.dataset.wfAct].type = e.target.value; })); document.querySelectorAll('[data-wf-param]').forEach(i => i.addEventListener('input', e => { actions[+i.dataset.wfParam].param = e.target.value; })); document.querySelectorAll('[data-wf-rm]').forEach(b => b.addEventListener('click', () => { actions.splice(+b.dataset.wfRm, 1); renderActions(); })); } renderActions(); document.getElementById('wf-add-action').addEventListener('click', () => { actions.push({ type:'send-email', param:'' }); renderActions(); }); if (id) { api(`/api/hr/recruiting/workflows/list?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`).then(r => { const w = (r.items||[]).find(x => x.id === id) || {}; document.getElementById('wf-name').value = w.name||''; document.getElementById('wf-trigger').value = w.trigger||''; document.getElementById('wf-enabled').checked = !!w.enabled; actions = w.actions || []; renderActions(); }).catch(e => toast('Errore caricamento workflow: '+e.message, 'error')); } document.getElementById('wf-save').addEventListener('click', async () => { const data = { id, name: document.getElementById('wf-name').value, trigger: document.getElementById('wf-trigger').value, enabled: document.getElementById('wf-enabled').checked, actions, }; if (!data.name || !actions.length) { toast('Nome + almeno 1 azione obbligatori', 'warning'); return; } try { await apiPost('/api/hr/recruiting/workflows/save', data); toast('Workflow salvato', 'success'); m.remove(); _wfLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } }); } async function _wfDel(id) { if (!confirm('Eliminare workflow?')) return; try { await api(`/api/hr/recruiting/workflows/${id}?project=${encodeURIComponent(STATE.tenant)}&country=${encodeURIComponent(STATE.country)}`, { method:'DELETE' }); toast('Eliminato', 'success'); _wfLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } } async function _wfToggle(id) { try { await apiPost(`/api/hr/recruiting/workflows/${id}/toggle?project=${STATE.tenant}`, {}); _wfLoad(); } catch (e) { toast('Errore: '+e.message, 'error'); } } // โ”€โ”€ Placeholder generico per le altre 30+ viste โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ VIEWS.placeholder = async function(main, view) { const titles = { editor: ['โœ๏ธ Editor annuncio (multi-lingua)', 'Crea annunci in 14 lingue ยท pubblica su uno o piรน portali ยท template personalizzabili.'], schedule: ['โฐ Schedule pubblicazioni', 'Pianifica pubblicazione automatica futura su tutti i canali abilitati.'], 'ad-perf': ['๐Ÿ“ˆ Performance annunci', 'Visualizzazioni ยท click ยท candidature ยท CTR ยท conversion per annuncio.'], sync: ['๐Ÿ”„ Sync candidature inbound', 'Cron auto orario ยท log sync ยท errori ยท candidature importate per source.'], 'email-parser': ['๐Ÿ“ง Email candidature parser', 'Setup indirizzo dedicato per ricezione automatica ยท AI parsing CV in arrivo.'], 'candidates-list': ['๐Ÿ“‹ Lista candidati', 'Tutti i candidati attivi e archiviati ยท filtri avanzati ยท search globale.'], 'bulk-import': ['๐Ÿ“ค Bulk import CV', 'Upload massivo file zip o cartella ยท parsing parallelo ยท review prima import.'], 'talent-pool': ['๐Ÿท Talent pool & tag', 'Candidati passivi ยท tagging custom ยท match futuri quando apre nuova posizione.'], 'ai-match': ['๐Ÿค– AI matching candidatoโ†”ruolo', 'Score automatico ranking ยท skill alignment ยท esperienza match.'], tests: ['๐Ÿ“ Test tecnici / logica', 'Template multi-choice ยท auto-scoring ยท risultati per candidato.'], 'video-screening': ['๐ŸŽฌ Video screening', 'Colloqui pre-registrati ยท domande standard ยท review asincrono.'], scoring: ['โญ Scoring & ranking', 'Visualizza top candidati per ogni posizione aperta.'], 'interview-feedback': ['๐Ÿ“ Feedback colloqui', 'Form strutturato ยท 5 dimensioni Likert ยท note libere ยท raccomandazione hire/no.'], recordings: ['๐ŸŽฌ Recording archive', 'Registrazioni colloqui ยท search per candidato/data ยท GDPR retention 6 mesi.'], offers: ['๐Ÿ“จ Offer letter', 'Generazione e invio offer letter ยท template tenant ยท firma digitale.'], 'offer-stats': ['๐Ÿ“Š Offer accepted/rejected', 'Tracking acceptance rate ยท motivazioni rifiuto ยท time-to-accept.'], 'onboard-trigger': ['๐Ÿš€ Trigger onboarding', 'Crea ticket HR-ONB su HR Ops ยท trasferisci dati candidato ยท notifica HR onboarding specialist.'], recruiters: ['๐Ÿ‘ค Recruiter del tenant', 'Lista recruiter ยท ruoli ยท candidati assegnati ยท permessi.'], assignments: ['๐Ÿ”„ Assegnazione candidati', 'Round-robin ยท manuale ยท per skill ยท workload bilanciato.'], 'internal-notes': ['๐Ÿ’ฌ Note interne (HR-only)', 'Note private del recruiter ยท non visibili al candidato ยท audit log.'], 'time-to-hire': ['โฑ Time-to-hire', 'Per ruolo ยท per recruiter ยท per source ยท trend ultimi 12 mesi.'], 'cost-per-hire': ['๐Ÿ’ฐ Cost-per-hire', 'Costo annuncio ยท referral bonus ยท tempo recruiter ยท totale per hire.'], 'source-roi': ['๐ŸŒ Source ROI / benchmark', 'ROI per portale ยท costo/candidatura ยท conversion rate ยท star rating.'], export: ['๐Ÿ“ค Export report', 'Genera report PDF/Excel/CSV ยท email schedulato ยท share link sicuro.'], 'auto-match': ['๐Ÿค– Match auto candidatoโ†”JR', 'AI suggerisce candidati per ogni posizione aperta nightly.'], 'auto-email': ['๐Ÿ“จ Email follow-up auto', 'Template configurabili ยท trigger su stage change ยท personalization tokens.'], 'cv-parsing': ['๐Ÿ“„ CV parsing AI', 'Anthropic Claude ยท estrazione skill/esperienza/contatti strutturati ยท cache 30gg.'], workflows: ['โšก Workflow automations', 'If-this-then-that ยท trigger custom ยท log esecuzioni.'], 'gdpr-export': ['๐Ÿ“ฅ Export dati candidato', 'Export GDPR completo ยท JSON + CV + comunicazioni ยท firma digitale.'], 'gdpr-forget': ['๐Ÿ—‘ Right-to-forget', 'Cancellazione completa dati candidato ยท audit log mantenuto ยท retention legale.'], 'audit-log': ['๐Ÿ“‹ Audit log', 'Log completo accessi/modifiche ยท filter per recruiter/candidato/azione ยท retention 90gg.'], }; const [title, desc] = titles[view] || [view, 'Vista in sviluppo']; main.innerHTML = `

${title}

${desc}
๐Ÿšง
Vista in sviluppo
${desc}

Implementazione prevista nella Fase 4 del piano. Per ora puoi usare la vecchia versione su HR Ops se esiste.
`; }; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function escHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]); } // โ”€โ”€ BOOT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ document.addEventListener('DOMContentLoaded', bootstrap);