569 lines
26 KiB
JavaScript
569 lines
26 KiB
JavaScript
/* 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 = '<div class="stat" style="--accent:linear-gradient(135deg,#2563eb,#10b981)"><div class="num">' + list.length +
|
|
'</div><div class="lbl">Total Pelamar</div></div>';
|
|
statuses.forEach(function (s) {
|
|
var n = list.filter(function (c) { return c.statusId === s.id; }).length;
|
|
html += '<div class="stat" style="--accent:' + esc(s.warna) + '">' +
|
|
'<div class="num">' + n + '</div><div class="lbl">' + esc(s.nama) + '</div></div>';
|
|
});
|
|
$('#summary').innerHTML = html;
|
|
}
|
|
|
|
/* ---------------- Filters ---------------- */
|
|
function fillFilters() {
|
|
var candidates = Store.get().candidates;
|
|
|
|
var statusSel = $('#fStatus');
|
|
statusSel.innerHTML = '<option value="">Semua Status</option>' +
|
|
Store.sortedStatuses().map(function (s) {
|
|
return '<option value="' + esc(s.id) + '"' + (filters.status === s.id ? ' selected' : '') + '>' + esc(s.nama) + '</option>';
|
|
}).join('');
|
|
|
|
var posisiSet = {};
|
|
candidates.forEach(function (c) { if (c.posisi) posisiSet[c.posisi] = 1; });
|
|
var posisiList = Object.keys(posisiSet).sort();
|
|
$('#fPosisi').innerHTML = '<option value="">Semua Posisi</option>' +
|
|
posisiList.map(function (p) {
|
|
return '<option value="' + esc(p) + '"' + (filters.posisi === p ? ' selected' : '') + '>' + esc(p) + '</option>';
|
|
}).join('');
|
|
|
|
$('#fSumber').innerHTML = '<option value="">Semua Sumber</option>' +
|
|
SUMBER.slice(1).map(function (p) {
|
|
return '<option value="' + esc(p) + '"' + (filters.sumber === p ? ' selected' : '') + '>' + esc(p) + '</option>';
|
|
}).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 <strong>+ Tambah Pelamar</strong> 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 '<tr>' +
|
|
'<td><strong>' + esc(c.nama) + '</strong>' +
|
|
(c.email ? '<div class="muted small">' + esc(c.email) + '</div>' : '') + '</td>' +
|
|
'<td>' + esc(c.posisi) + '</td>' +
|
|
'<td>' + esc(phone) + '</td>' +
|
|
'<td>' + UI.badge(st) + '</td>' +
|
|
'<td>' + esc(UI.formatDate(c.tanggalMelamar)) + '</td>' +
|
|
'<td><div class="actions">' +
|
|
'<button class="btn btn-warn btn-sm" data-act="reminder" data-id="' + c.id + '">Reminder Pelamar</button>' +
|
|
'<button class="btn btn-ghost btn-sm" data-act="detail" data-id="' + c.id + '">Detail</button>' +
|
|
'<button class="btn btn-ghost btn-sm" data-act="edit" data-id="' + c.id + '">Edit</button>' +
|
|
'<button class="btn btn-danger btn-sm" data-act="del" data-id="' + c.id + '">Hapus</button>' +
|
|
'</div></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
}
|
|
|
|
/* ---------------- Form ---------------- */
|
|
function statusOptions(selected) {
|
|
return Store.sortedStatuses().map(function (s) {
|
|
return '<option value="' + esc(s.id) + '"' + (s.id === selected ? ' selected' : '') + '>' + esc(s.nama) + '</option>';
|
|
}).join('');
|
|
}
|
|
|
|
function sumberOptions(selected) {
|
|
return SUMBER.map(function (s) {
|
|
return '<option value="' + esc(s) + '"' + (s === selected ? ' selected' : '') + '>' + (s || '- Pilih -') + '</option>';
|
|
}).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 =
|
|
'<div class="grid cols-2">' +
|
|
'<div class="field"><label for="f-nama">Nama lengkap *</label><input type="text" id="f-nama" value="' + esc(c ? c.nama : '') + '"></div>' +
|
|
'<div class="field"><label for="f-phone">No. WhatsApp *</label><input type="text" id="f-phone" placeholder="08xxxxxxxxxx" value="' + esc(c ? c.phoneRaw : '') + '"><div class="hint" id="phoneHint"></div></div>' +
|
|
'</div>' +
|
|
'<div class="grid cols-2">' +
|
|
'<div class="field"><label for="f-posisi">Posisi dilamar *</label><input type="text" id="f-posisi" value="' + esc(c ? c.posisi : '') + '"></div>' +
|
|
'<div class="field"><label for="f-status">Status</label><select id="f-status">' + statusOptions(c ? c.statusId : (defStatus && defStatus.id)) + '</select></div>' +
|
|
'</div>' +
|
|
'<div class="grid cols-2">' +
|
|
'<div class="field"><label for="f-email">Email</label><input type="email" id="f-email" value="' + esc(c ? c.email : '') + '"></div>' +
|
|
'<div class="field"><label for="f-domisili">Domisili</label><input type="text" id="f-domisili" value="' + esc(c ? c.domisili : '') + '"></div>' +
|
|
'</div>' +
|
|
'<div class="grid cols-3">' +
|
|
'<div class="field"><label for="f-sumber">Sumber</label><select id="f-sumber">' + sumberOptions(c ? c.sumber : '') + '</select></div>' +
|
|
'<div class="field"><label for="f-gaji">Gaji ekspektasi</label><input type="text" id="f-gaji" placeholder="cth: 5.000.000" value="' + esc(c ? c.gajiEkspektasi : '') + '"></div>' +
|
|
'<div class="field"><label for="f-tanggal">Tanggal melamar</label><input type="date" id="f-tanggal" value="' + esc(c ? c.tanggalMelamar : today) + '"></div>' +
|
|
'</div>' +
|
|
'<h4 class="mt2">Jadwal Interview</h4>' +
|
|
'<div class="grid cols-3">' +
|
|
'<div class="field"><label for="i-tanggal">Tanggal</label><input type="date" id="i-tanggal" value="' + esc(iv.tanggal || '') + '"></div>' +
|
|
'<div class="field"><label for="i-jam">Jam</label><input type="time" id="i-jam" value="' + esc(iv.jam || '') + '"></div>' +
|
|
'<div class="field"><label for="i-metode">Metode</label><select id="i-metode">' +
|
|
['', 'Online', 'Offline'].map(function (m) {
|
|
return '<option value="' + m + '"' + (iv.metode === m ? ' selected' : '') + '>' + (m || '- Pilih -') + '</option>';
|
|
}).join('') + '</select></div>' +
|
|
'</div>' +
|
|
'<div class="grid cols-3">' +
|
|
'<div class="field"><label for="i-pewawancara">Nama pewawancara</label><input type="text" id="i-pewawancara" value="' + esc(iv.pewawancara || '') + '"></div>' +
|
|
'<div class="field"><label for="i-telp">No. WA pewawancara</label><input type="text" id="i-telp" placeholder="08xxxxxxxxxx" value="' + esc(iv.telpPewawancara || '') + '"><div class="hint" id="telpHint"></div></div>' +
|
|
'<div class="field"><label for="i-lokasi">Lokasi / Link meeting</label><input type="text" id="i-lokasi" value="' + esc(iv.lokasi || '') + '"></div>' +
|
|
'</div>' +
|
|
'<div class="field"><label for="f-catatan">Catatan HR (internal)</label><textarea id="f-catatan">' + esc(c ? c.catatan : '') + '</textarea></div>';
|
|
|
|
var footer =
|
|
'<button class="btn btn-ghost" type="button" data-close>Batal</button>' +
|
|
'<button class="btn btn-primary" type="button" id="saveCandidate">' + (id ? 'Simpan Perubahan' : 'Tambah Pelamar') + '</button>';
|
|
|
|
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)
|
|
? '<span style="color:#16a34a">✓ ' + Phone.formatDisplay(norm) + '</span>'
|
|
: '<span style="color:#dc2626">Nomor belum valid</span>';
|
|
}
|
|
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)
|
|
? '<span style="color:#16a34a">✓ ' + Phone.formatDisplay(norm) + '</span>'
|
|
: '<span style="color:#dc2626">Nomor belum valid</span>';
|
|
}
|
|
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 =
|
|
'<div class="grid cols-2">' +
|
|
'<div><div class="muted small">Nama</div><div><strong>' + esc(c.nama) + '</strong></div></div>' +
|
|
'<div><div class="muted small">Posisi</div><div>' + esc(c.posisi) + '</div></div>' +
|
|
'<div><div class="muted small">No. WhatsApp</div><div>' + esc(c.phoneNorm ? Phone.formatDisplay(c.phoneNorm) : c.phoneRaw) + '</div></div>' +
|
|
'<div><div class="muted small">Status</div><div>' + UI.badge(st) + '</div></div>' +
|
|
'<div><div class="muted small">Email</div><div>' + esc(c.email || '-') + '</div></div>' +
|
|
'<div><div class="muted small">Domisili</div><div>' + esc(c.domisili || '-') + '</div></div>' +
|
|
'<div><div class="muted small">Sumber</div><div>' + esc(c.sumber || '-') + '</div></div>' +
|
|
'<div><div class="muted small">Gaji ekspektasi</div><div>' + esc(c.gajiEkspektasi || '-') + '</div></div>' +
|
|
'<div><div class="muted small">Tanggal melamar</div><div>' + esc(UI.formatDate(c.tanggalMelamar)) + '</div></div>' +
|
|
'<div><div class="muted small">Pewawancara</div><div>' + esc(iv.pewawancara || '-') +
|
|
(iv.telpPewawancara ? '<div class="muted small">' + esc(Phone.formatDisplay(Phone.normalize(iv.telpPewawancara))) + '</div>' : '') + '</div></div>' +
|
|
'</div>';
|
|
|
|
var jadwal = iv.tanggal
|
|
? '<div class="mt1"><div class="muted small">Jadwal Interview</div><div>' +
|
|
esc(WA.tanggalLengkap(iv.tanggal)) + (iv.jam ? ' pukul ' + esc(iv.jam) : '') +
|
|
(iv.metode ? ' (' + esc(iv.metode) + ')' : '') +
|
|
(iv.lokasi ? '<br><span class="muted">' + esc(iv.lokasi) + '</span>' : '') +
|
|
'</div></div>'
|
|
: '<div class="mt1 muted">Belum ada jadwal interview.</div>';
|
|
|
|
var catatan = c.catatan ? '<div class="mt1"><div class="muted small">Catatan HR</div><div>' + esc(c.catatan) + '</div></div>' : '';
|
|
|
|
var berkas = '<h4 class="mt2">Berkas</h4>' + SLOTS.map(function (slot) {
|
|
var b = c.berkas && c.berkas[slot.key];
|
|
var value = '-';
|
|
if (b && b.mode === 'link') value = '<a href="' + esc(b.url) + '" target="_blank" rel="noopener">' + esc(b.url) + '</a>';
|
|
else if (b && b.mode === 'file') value = esc(b.name) + ' <span class="muted small">(' + UI.formatBytes(b.size) + ')</span>';
|
|
return '<div class="berkas-row">' +
|
|
'<div class="berkas-label">' + slot.label + '</div>' +
|
|
'<div class="berkas-value">' + value + '</div>' +
|
|
'<div class="berkas-actions">' +
|
|
(b ? '<button class="btn btn-ghost btn-sm" data-berkas-open="' + slot.key + '" data-id="' + c.id + '">Lihat</button>' : '') +
|
|
'<label class="btn btn-ghost btn-sm" style="margin:0">Upload<input type="file" hidden data-berkas-file="' + slot.key + '" data-id="' + c.id + '" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"></label>' +
|
|
'<button class="btn btn-ghost btn-sm" data-berkas-link="' + slot.key + '" data-id="' + c.id + '">Link</button>' +
|
|
(b ? '<button class="btn btn-danger btn-sm" data-berkas-del="' + slot.key + '" data-id="' + c.id + '">Hapus</button>' : '') +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
var history = (c.history || []).slice().reverse().map(function (h) {
|
|
var dari = h.statusLama ? statusName(h.statusLama) : '(baru)';
|
|
return '<div class="small muted">' + esc(UI.formatDateTime(h.at)) + ' — ' + esc(dari) + ' → <strong>' + esc(statusName(h.statusBaru)) + '</strong></div>';
|
|
}).join('') || '<div class="small muted">Belum ada riwayat.</div>';
|
|
|
|
var footer =
|
|
'<button class="btn btn-ghost" type="button" data-close>Tutup</button>' +
|
|
'<button class="btn btn-warn" type="button" data-act="reminder" data-id="' + c.id + '">Reminder Pelamar</button>' +
|
|
'<button class="btn btn-warn" type="button" data-act="pewawancara" data-id="' + c.id + '">Reminder Pewawancara</button>' +
|
|
'<button class="btn btn-primary" type="button" data-act="edit" data-id="' + c.id + '">Edit</button>';
|
|
|
|
UI.openModal({
|
|
title: 'Detail Pelamar',
|
|
size: 'lg',
|
|
body: info + jadwal + catatan + berkas +
|
|
'<h4 class="mt2">Riwayat Status</h4><div>' + history + '</div>',
|
|
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 '<option value="' + t.id + '"' + (t.id === preselect.id ? ' selected' : '') + '>' + esc(t.nama) + '</option>';
|
|
}).join('');
|
|
|
|
var body =
|
|
'<div class="field"><label for="wa-tpl">Template pesan</label><select id="wa-tpl">' + options + '</select></div>' +
|
|
'<div class="field"><label>Preview pesan</label><div class="preview-box" id="wa-preview"></div></div>' +
|
|
'<div class="hint">Pesan akan terbuka di WhatsApp dan Anda tinggal menekan tombol kirim.</div>';
|
|
|
|
var footer =
|
|
'<button class="btn btn-ghost" type="button" data-close>Batal</button>' +
|
|
'<button class="btn btn-wa" type="button" id="waOpen">Buka WhatsApp</button>';
|
|
|
|
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);
|
|
})();
|