/* app.js — Halaman daftar pelamar */ (function () { 'use strict'; var $ = function (s, r) { return (r || document).querySelector(s); }; var filters = { q: '', status: '', posisi: '', sumber: '', sort: 'tanggal-desc' }; var SUMBER = ['', 'Iklan', 'Referral', 'Walk-in', 'Job Portal', 'Lainnya']; var MAX_FILE = 5 * 1024 * 1024; // 5 MB var SLOTS = [ { key: 'cv', label: 'CV' }, { key: 'test', label: 'Hasil Test' }, { key: 'portofolio', label: 'Portofolio' } ]; /* ---------------- Init ---------------- */ function init() { Store.init(); bindEvents(); renderAll(); } function renderAll() { renderSummary(); fillFilters(); renderList(); } function esc(v) { return UI.escape(v); } function statusName(id) { var s = Store.statusById(id); return s ? s.nama : '-'; } /* ---------------- Summary ---------------- */ function renderSummary() { var list = Store.get().candidates; var statuses = Store.sortedStatuses(); var html = '
' + list.length + '
Total Pelamar
'; statuses.forEach(function (s) { var n = list.filter(function (c) { return c.statusId === s.id; }).length; html += '
' + '
' + n + '
' + esc(s.nama) + '
'; }); $('#summary').innerHTML = html; } /* ---------------- Filters ---------------- */ function fillFilters() { var candidates = Store.get().candidates; var statusSel = $('#fStatus'); statusSel.innerHTML = '' + Store.sortedStatuses().map(function (s) { return ''; }).join(''); var posisiSet = {}; candidates.forEach(function (c) { if (c.posisi) posisiSet[c.posisi] = 1; }); var posisiList = Object.keys(posisiSet).sort(); $('#fPosisi').innerHTML = '' + posisiList.map(function (p) { return ''; }).join(''); $('#fSumber').innerHTML = '' + SUMBER.slice(1).map(function (p) { return ''; }).join(''); $('#fSearch').value = filters.q; $('#fSort').value = filters.sort; } function applyFilters() { var list = Store.get().candidates.slice(); var q = filters.q.trim().toLowerCase(); if (q) { var digits = q.replace(/\D/g, ''); list = list.filter(function (c) { return (c.nama || '').toLowerCase().indexOf(q) >= 0 || (c.posisi || '').toLowerCase().indexOf(q) >= 0 || (c.email || '').toLowerCase().indexOf(q) >= 0 || (c.phoneRaw || '').toLowerCase().indexOf(q) >= 0 || (digits !== '' && (c.phoneNorm || '').indexOf(digits) >= 0); }); } if (filters.status) list = list.filter(function (c) { return c.statusId === filters.status; }); if (filters.posisi) list = list.filter(function (c) { return c.posisi === filters.posisi; }); if (filters.sumber) list = list.filter(function (c) { return c.sumber === filters.sumber; }); list.sort(function (a, b) { if (filters.sort === 'nama-asc') return (a.nama || '').localeCompare(b.nama || '', 'id'); var da = a.tanggalMelamar || '', db = b.tanggalMelamar || ''; return filters.sort === 'tanggal-asc' ? da.localeCompare(db) : db.localeCompare(da); }); return list; } /* ---------------- List ---------------- */ function renderList() { var list = applyFilters(); var tbody = $('#tbody'); var empty = $('#emptyState'); if (!list.length) { tbody.innerHTML = ''; var adaData = Store.get().candidates.length > 0; empty.innerHTML = adaData ? 'Tidak ada pelamar yang cocok dengan pencarian/filter.' : 'Belum ada data pelamar. Klik + Tambah Pelamar untuk memulai.'; empty.classList.remove('hidden'); $('.table-wrap').classList.add('hidden'); return; } empty.classList.add('hidden'); $('.table-wrap').classList.remove('hidden'); tbody.innerHTML = list.map(function (c) { var st = Store.statusById(c.statusId); var phone = c.phoneNorm ? Phone.formatDisplay(c.phoneNorm) : (c.phoneRaw || '-'); return '' + '' + esc(c.nama) + '' + (c.email ? '
' + esc(c.email) + '
' : '') + '' + '' + esc(c.posisi) + '' + '' + esc(phone) + '' + '' + UI.badge(st) + '' + '' + esc(UI.formatDate(c.tanggalMelamar)) + '' + '
' + '' + '' + '' + '' + '
' + ''; }).join(''); } /* ---------------- Form ---------------- */ function statusOptions(selected) { return Store.sortedStatuses().map(function (s) { return ''; }).join(''); } function sumberOptions(selected) { return SUMBER.map(function (s) { return ''; }).join(''); } function openForm(id) { var c = id ? Store.candidateById(id) : null; var iv = (c && c.interview) || {}; var defStatus = Store.sortedStatuses()[0]; var today = new Date().toISOString().slice(0, 10); var body = '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '

Jadwal Interview

' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
'; var footer = '' + ''; UI.openModal({ title: id ? 'Edit Pelamar' : 'Tambah Pelamar', size: 'lg', body: body, footer: footer, onMount: function (el) { var phoneInput = el.querySelector('#f-phone'); var hint = el.querySelector('#phoneHint'); function updHint() { var norm = Phone.normalize(phoneInput.value); if (!phoneInput.value.trim()) { hint.textContent = ''; return; } hint.innerHTML = Phone.isValid(norm) ? '✓ ' + Phone.formatDisplay(norm) + '' : 'Nomor belum valid'; } phoneInput.addEventListener('input', updHint); updHint(); var telpInput = el.querySelector('#i-telp'); var telpHint = el.querySelector('#telpHint'); function updTelpHint() { var norm = Phone.normalize(telpInput.value); if (!telpInput.value.trim()) { telpHint.textContent = ''; return; } telpHint.innerHTML = Phone.isValid(norm) ? '✓ ' + Phone.formatDisplay(norm) + '' : 'Nomor belum valid'; } telpInput.addEventListener('input', updTelpHint); updTelpHint(); el.querySelector('[data-close]').addEventListener('click', UI.closeModal); el.querySelector('#saveCandidate').addEventListener('click', function () { saveCandidate(id); }); } }); } function saveCandidate(id) { var el = document; var nama = $('#f-nama').value.trim(); var phoneRaw = $('#f-phone').value.trim(); var phoneNorm = Phone.normalize(phoneRaw); var posisi = $('#f-posisi').value.trim(); if (!nama) { UI.toast('Nama wajib diisi.', 'err'); return; } if (!phoneRaw) { UI.toast('No. WhatsApp wajib diisi.', 'err'); return; } if (!Phone.isValid(phoneNorm)) { UI.toast('Nomor WhatsApp tidak valid.', 'err'); return; } if (!posisi) { UI.toast('Posisi wajib diisi.', 'err'); return; } var data = { nama: nama, phoneRaw: phoneRaw, phoneNorm: phoneNorm, email: $('#f-email').value.trim(), posisi: posisi, statusId: $('#f-status').value, sumber: $('#f-sumber').value, domisili: $('#f-domisili').value.trim(), gajiEkspektasi: $('#f-gaji').value.trim(), tanggalMelamar: $('#f-tanggal').value, catatan: $('#f-catatan').value.trim(), interview: { tanggal: $('#i-tanggal').value, jam: $('#i-jam').value, metode: $('#i-metode').value, lokasi: $('#i-lokasi').value.trim(), pewawancara: $('#i-pewawancara').value.trim(), telpPewawancara: $('#i-telp').value.trim() } }; if (id) { Store.updateCandidate(id, data); UI.toast('Data pelamar diperbarui.', 'ok'); } else { Store.addCandidate(data); UI.toast('Pelamar berhasil ditambahkan.', 'ok'); } UI.closeModal(); renderAll(); } /* ---------------- Detail ---------------- */ function openDetail(id) { var c = Store.candidateById(id); if (!c) return; var st = Store.statusById(c.statusId); var iv = c.interview || {}; var info = '
' + '
Nama
' + esc(c.nama) + '
' + '
Posisi
' + esc(c.posisi) + '
' + '
No. WhatsApp
' + esc(c.phoneNorm ? Phone.formatDisplay(c.phoneNorm) : c.phoneRaw) + '
' + '
Status
' + UI.badge(st) + '
' + '
Email
' + esc(c.email || '-') + '
' + '
Domisili
' + esc(c.domisili || '-') + '
' + '
Sumber
' + esc(c.sumber || '-') + '
' + '
Gaji ekspektasi
' + esc(c.gajiEkspektasi || '-') + '
' + '
Tanggal melamar
' + esc(UI.formatDate(c.tanggalMelamar)) + '
' + '
Pewawancara
' + esc(iv.pewawancara || '-') + (iv.telpPewawancara ? '
' + esc(Phone.formatDisplay(Phone.normalize(iv.telpPewawancara))) + '
' : '') + '
' + '
'; var jadwal = iv.tanggal ? '
Jadwal Interview
' + esc(WA.tanggalLengkap(iv.tanggal)) + (iv.jam ? ' pukul ' + esc(iv.jam) : '') + (iv.metode ? ' (' + esc(iv.metode) + ')' : '') + (iv.lokasi ? '
' + esc(iv.lokasi) + '' : '') + '
' : '
Belum ada jadwal interview.
'; var catatan = c.catatan ? '
Catatan HR
' + esc(c.catatan) + '
' : ''; var berkas = '

Berkas

' + SLOTS.map(function (slot) { var b = c.berkas && c.berkas[slot.key]; var value = '-'; if (b && b.mode === 'link') value = '' + esc(b.url) + ''; else if (b && b.mode === 'file') value = esc(b.name) + ' (' + UI.formatBytes(b.size) + ')'; return '
' + '
' + slot.label + '
' + '
' + value + '
' + '
' + (b ? '' : '') + '' + '' + (b ? '' : '') + '
' + '
'; }).join(''); var history = (c.history || []).slice().reverse().map(function (h) { var dari = h.statusLama ? statusName(h.statusLama) : '(baru)'; return '
' + esc(UI.formatDateTime(h.at)) + ' — ' + esc(dari) + ' → ' + esc(statusName(h.statusBaru)) + '
'; }).join('') || '
Belum ada riwayat.
'; var footer = '' + '' + '' + ''; UI.openModal({ title: 'Detail Pelamar', size: 'lg', body: info + jadwal + catatan + berkas + '

Riwayat Status

' + history + '
', footer: footer, onMount: function (el) { el.querySelector('[data-close]').addEventListener('click', UI.closeModal); } }); } /* ---------------- WhatsApp ---------------- */ function openWaDialog(id, mode) { var c = Store.candidateById(id); if (!c) return; var targetPhone = c.phoneNorm; if (mode === 'pewawancara') { targetPhone = Phone.normalize((c.interview && c.interview.telpPewawancara) || ''); if (!Phone.isValid(targetPhone)) { UI.toast('Nomor WA pewawancara belum diisi / tidak valid. Isi lewat tombol Edit.', 'err'); return; } } else if (!Phone.isValid(targetPhone)) { UI.toast('Nomor WhatsApp tidak valid.', 'err'); return; } if (mode === 'reminder' && !(c.interview && c.interview.tanggal)) { if (!UI.confirmDialog('Jadwal interview belum diisi. Bagian hari/tanggal/jam pada pesan akan kosong. Tetap lanjutkan?')) return; } var templates = Store.get().templates.filter(function (t) { return t.isActive !== false; }); if (!templates.length) { UI.toast('Belum ada template aktif. Buka menu Template Pesan.', 'err'); return; } var preselect = null; var jenisCari = mode === 'reminder' ? 'reminder' : (mode === 'pewawancara' ? 'pewawancara' : null); if (jenisCari) preselect = templates.filter(function (t) { return t.jenis === jenisCari; })[0]; if (!preselect) preselect = templates.filter(function (t) { return t.statusId === c.statusId; })[0]; if (!preselect) preselect = templates[0]; var ctx = WA.context(c, Store.get().settings); ctx.status = statusName(c.statusId); var options = templates.map(function (t) { return ''; }).join(''); var body = '
' + '
' + '
Pesan akan terbuka di WhatsApp dan Anda tinggal menekan tombol kirim.
'; var footer = '' + ''; UI.openModal({ title: mode === 'reminder' ? 'Reminder Interview (Pelamar)' : (mode === 'pewawancara' ? 'Reminder Interview (Pewawancara)' : 'Kirim WhatsApp'), body: body, footer: footer, onMount: function (el) { var sel = el.querySelector('#wa-tpl'); var prev = el.querySelector('#wa-preview'); function update() { var t = Store.templateById(sel.value); prev.textContent = t ? WA.render(t.isi, ctx) : ''; } sel.addEventListener('change', update); update(); el.querySelector('[data-close]').addEventListener('click', UI.closeModal); el.querySelector('#waOpen').addEventListener('click', function () { var t = Store.templateById(sel.value); if (!t) return; WA.open(targetPhone, WA.render(t.isi, ctx)); }); } }); } /* ---------------- Berkas ---------------- */ function setBerkas(id, slot, value) { var c = Store.candidateById(id); if (!c) return; c.berkas = c.berkas || { cv: null, test: null, portofolio: null }; var old = c.berkas[slot]; if (old && old.mode === 'file' && old.fileId && (!value || value.fileId !== old.fileId)) { Store.deleteFile(old.fileId).catch(function () {}); } c.berkas[slot] = value; Store.save(); } function handleBerkasFile(input) { var file = input.files && input.files[0]; if (!file) return; var id = input.getAttribute('data-id'); var slot = input.getAttribute('data-berkas-file'); if (file.size > MAX_FILE) { UI.toast('Ukuran berkas maksimal 5 MB.', 'err'); input.value = ''; return; } UI.toast('Mengunggah berkas...'); Store.saveFile(file).then(function (meta) { setBerkas(id, slot, { mode: 'file', fileId: meta.id, name: meta.name, size: meta.size, type: meta.type }); UI.toast('Berkas tersimpan.', 'ok'); openDetail(id); }).catch(function (err) { UI.toast('Gagal menyimpan berkas: ' + err.message, 'err'); }); input.value = ''; } /* ---------------- Export / Import Excel ---------------- */ function doExportExcel() { var list = Store.get().candidates; if (!list.length) { UI.toast('Belum ada data pelamar untuk diexport.', 'err'); return; } try { Excel.exportCandidates(list, Store.get().statuses); UI.toast('File Excel diunduh.', 'ok'); } catch (err) { UI.toast('Gagal export Excel: ' + err.message, 'err'); } } function doImportExcel(file) { UI.toast('Membaca file Excel...'); Excel.readRows(file).then(function (json) { var mapped = Excel.mapRows(json, Store.get().statuses); var valid = [], invalid = 0; mapped.forEach(function (row) { var norm = Phone.normalize(row.phoneRaw || ''); if (!row.nama || !Phone.isValid(norm)) { invalid++; return; } row.phoneNorm = norm; valid.push(row); }); if (!valid.length) { UI.toast('Tidak ada baris valid. Kolom "Nama" dan "No WhatsApp" wajib terisi.', 'err'); return; } var pesan = 'Ditemukan ' + valid.length + ' baris valid' + (invalid ? ' dan ' + invalid + ' baris dilewati (nama/No WhatsApp kosong atau tidak valid)' : '') + '.\n\nTambahkan baris valid ke daftar pelamar?'; if (!UI.confirmDialog(pesan)) return; valid.forEach(function (row) { Store.addCandidate(row); }); UI.toast(valid.length + ' pelamar ditambahkan.', 'ok'); renderAll(); }).catch(function (err) { UI.toast('Gagal import Excel: ' + err.message, 'err'); }); } /* ---------------- Events ---------------- */ function bindEvents() { $('#btnAdd').addEventListener('click', function () { openForm(null); }); $('#fSearch').addEventListener('input', function (e) { filters.q = e.target.value; renderList(); }); $('#fStatus').addEventListener('change', function (e) { filters.status = e.target.value; renderList(); }); $('#fPosisi').addEventListener('change', function (e) { filters.posisi = e.target.value; renderList(); }); $('#fSumber').addEventListener('change', function (e) { filters.sumber = e.target.value; renderList(); }); $('#fSort').addEventListener('change', function (e) { filters.sort = e.target.value; renderList(); }); $('#btnExportExcel').addEventListener('click', doExportExcel); $('#btnImportExcel').addEventListener('click', function () { $('#importExcelFile').click(); }); $('#importExcelFile').addEventListener('change', function (e) { if (e.target.files && e.target.files[0]) doImportExcel(e.target.files[0]); e.target.value = ''; }); // Delegasi klik aksi document.addEventListener('click', function (e) { var btn = e.target.closest('[data-act]'); if (btn) { var id = btn.getAttribute('data-id'); var act = btn.getAttribute('data-act'); if (act === 'wa') openWaDialog(id, 'status'); else if (act === 'reminder') openWaDialog(id, 'reminder'); else if (act === 'pewawancara') openWaDialog(id, 'pewawancara'); else if (act === 'detail') openDetail(id); else if (act === 'edit') openForm(id); else if (act === 'del') { var c = Store.candidateById(id); if (c && UI.confirmDialog('Hapus pelamar "' + c.nama + '"? Tindakan ini tidak bisa dibatalkan.')) { Store.deleteCandidate(id); UI.toast('Pelamar dihapus.', 'ok'); UI.closeModal(); renderAll(); } } return; } var openBtn = e.target.closest('[data-berkas-open]'); if (openBtn) { var cid = openBtn.getAttribute('data-id'); var slot = openBtn.getAttribute('data-berkas-open'); var cand = Store.candidateById(cid); var b = cand && cand.berkas && cand.berkas[slot]; if (!b) return; if (b.mode === 'link') { window.open(b.url, '_blank', 'noopener'); return; } Store.getFileUrl(b.fileId).then(function (url) { window.open(url, '_blank', 'noopener'); }) .catch(function () { UI.toast('Berkas tidak ditemukan.', 'err'); }); return; } var linkBtn = e.target.closest('[data-berkas-link]'); if (linkBtn) { var cid2 = linkBtn.getAttribute('data-id'); var slot2 = linkBtn.getAttribute('data-berkas-link'); var url = window.prompt('Masukkan URL berkas (Google Drive, dll):'); if (url && url.trim()) { setBerkas(cid2, slot2, { mode: 'link', url: url.trim() }); UI.toast('Link tersimpan.', 'ok'); openDetail(cid2); } return; } var delBtn = e.target.closest('[data-berkas-del]'); if (delBtn) { var cid3 = delBtn.getAttribute('data-id'); var slot3 = delBtn.getAttribute('data-berkas-del'); if (UI.confirmDialog('Hapus berkas ini?')) { setBerkas(cid3, slot3, null); UI.toast('Berkas dihapus.', 'ok'); openDetail(cid3); } } }); // Delegasi input file berkas document.addEventListener('change', function (e) { var input = e.target.closest('[data-berkas-file]'); if (input) handleBerkasFile(input); }); } document.addEventListener('DOMContentLoaded', init); })();