V
VWP CRM Database
Sistem Manajemen Database B2B
Masuk
Masukkan kredensial untuk mengakses sistem
Belum ada field kustom.
';
}
function buildExtAutoSave() {
const el = document.getElementById('ext-autosave-list'); if (!el) return;
el.innerHTML = G.extFields.map((f, i) => `
${(u.name || u.username)[0]}
${sanitize(u.name || u.username)}
@${sanitize(u.username)}
${u.online ? 'Online' : 'Offline'}
${u.role === 'admin' ? 'Admin' : u.role === 'user' ? 'User' : 'Observer'}${u.evf ? 'Email OK' : 'Belum verif.'}
Email: ${sanitize(u.email || '-')}
Data: ${u.added || 0} entri
${u.username !== G.user.username ? `` : ''}
${!u.evf ? `` : ''}
`).join('');
}
async function rmUser(un) {
if (!confirm(`Hapus user @${un}?`)) return;
await api('DELETE', '/users/' + un);
G.users = G.users.filter(u => u.username !== un); buildUsersGrid();
notify(`@${un} dihapus`, 'ok'); pushLog('wn', 'User Dihapus', `@${un} dihapus oleh ${G.user.username}`, G.user.username);
pushTicker('wn', 'Hapus User', `@${un}`, G.user.username);
}
function resetAddUser() { ['au-nm', 'au-em', 'au-un'].forEach(id => document.getElementById(id).value = ''); }
async function addUser() {
const em = document.getElementById('au-em').value.trim(); const un = document.getElementById('au-un').value.trim().toLowerCase();
const nm = document.getElementById('au-nm').value.trim(); const role = document.getElementById('au-role').value;
if (!em || !un) { notify('Email dan username wajib diisi', 'wn'); return; }
if (!/^[a-zA-Z0-9_]{3,20}$/.test(un)) { notify('Username hanya boleh huruf, angka, underscore (3-20 karakter)', 'wn'); return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em)) { notify('Format email tidak valid', 'wn'); return; }
const r = await api('POST', '/users', { username: un, name: nm, email: em, role });
if (r && r.error && CFG.workerUrl) { notify(r.error, 'er'); return; }
G.users.push({ username: un, name: nm || un, email: em, role, online: false, evf: false, added: 0, color: 'linear-gradient(135deg,#818cf8,#a78bfa)' });
closeMo('mo-adduser'); buildUsersGrid(); notify(`Undangan dikirim ke ${em}`, 'ok');
pushLog('in', 'User Baru Dibuat', `@${un} (${role}) - email verifikasi dikirim`, G.user.username);
pushTicker('in', 'User Baru', `@${un} (${role})`, G.user.username);
}
/* ══ SETTINGS - PROFILE ══ */
function buildSettingsPf() {
if (!G.user) return;
document.getElementById('set-av').textContent = G.user.name[0]; document.getElementById('set-av').style.background = G.user.color || 'var(--grad)';
document.getElementById('set-nm').textContent = G.user.name; document.getElementById('set-em').textContent = G.user.email;
const eb = document.getElementById('set-eb'); eb.textContent = G.user.evf ? 'Terverifikasi' : 'Belum Diverifikasi'; eb.className = 'bdg ' + (G.user.evf ? 'bdg-ok' : 'bdg-er');
document.getElementById('pf-nama').value = G.user.name; document.getElementById('pf-user').value = G.user.username;
document.getElementById('pf-email').value = G.user.email; document.getElementById('pf-role').value = G.user.role === 'admin' ? 'Administrator' : G.user.role === 'user' ? 'User' : 'Observer';
}
function openEditPf() { document.getElementById('ep-nm').value = G.user.name; document.getElementById('ep-un').value = G.user.username; document.getElementById('ep-em').value = G.user.email; openMo('mo-editpf'); }
async function savePf() {
const nm = document.getElementById('ep-nm').value.trim(); const em = document.getElementById('ep-em').value.trim();
if (!nm || !em) { notify('Nama dan email wajib diisi', 'wn'); return; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em)) { notify('Format email tidak valid', 'wn'); return; }
const ec = em !== G.user.email;
G.user.name = nm; G.user.email = em;
if (UL[G.user.username]) { UL[G.user.username].name = nm; UL[G.user.username].email = em; }
if (ec) { G.user.evf = false; document.getElementById('verif-banner').style.display = 'flex'; }
const r = await api('PUT', '/users/' + G.user.username + '/profile', { name: nm, email: em });
closeMo('mo-editpf'); buildSettingsPf(); applyRole();
notify(ec ? 'Profil disimpan. Verifikasi email baru dikirim.' : 'Profil berhasil disimpan', 'ok');
pushLog('in', 'Profil Diubah', `${G.user.username} memperbarui profil`, G.user.username);
}
async function changePw() {
const o = document.getElementById('pw-old').value;
const n1 = document.getElementById('pw-n1').value;
const n2 = document.getElementById('pw-n2').value;
if (!o || !n1 || !n2) { notify('Lengkapi semua field password', 'wn'); return; }
// Validate old password
let oldOk = false;
if (UL[G.user.username]) oldOk = UL[G.user.username].pw === o;
else { const r = await api('POST', '/auth/verify-pw', { password: o }); oldOk = !r.error; }
if (!oldOk) { notify('Password lama tidak sesuai', 'er'); return; }
if (n1 !== n2) { notify('Konfirmasi password tidak cocok', 'er'); return; }
const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*\-_]).{8,15}$/;
if (!re.test(n1)) { notify('Password: 8-15 karakter dengan huruf besar, kecil, angka, dan simbol', 'wn'); return; }
// Save password
if (UL[G.user.username]) UL[G.user.username].pw = n1;
const r = await api('PUT', '/users/' + G.user.username + '/password', { oldPw: o, newPw: n1 });
if (r && r.error && CFG.workerUrl) { notify('Gagal ubah password: ' + r.error, 'er'); return; }
['pw-old', 'pw-n1', 'pw-n2'].forEach(id => document.getElementById(id).value = '');
document.getElementById('pw-fb').classList.add('hidden');
notify('Password berhasil diubah', 'ok');
pushLog('ok', 'Password Diubah', `${G.user.username} mengubah password`, G.user.username);
}
function chkPw() {
const n1 = document.getElementById('pw-n1').value; const n2 = document.getElementById('pw-n2').value;
const fb = document.getElementById('pw-fb');
const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*\-_]).{8,15}$/;
if (!n1 && !n2) { fb.classList.add('hidden'); return; }
fb.classList.remove('hidden');
if (n1 && !re.test(n1)) { fb.className = 'ib ib-er text-xs mb-3'; fb.textContent = '8-15 karakter: huruf besar, kecil, angka, dan simbol (!@#$%^&*-_)'; return; }
if (n1 && n2 && n1 !== n2) { fb.className = 'ib ib-er text-xs mb-3'; fb.textContent = 'Konfirmasi password tidak cocok'; return; }
if (n1 && n2 && n1 === n2 && re.test(n1)) { fb.className = 'ib ib-ok text-xs mb-3'; fb.textContent = 'Password kuat dan cocok'; }
else fb.classList.add('hidden');
}
function saveSecSettings() {
G.secSettings.maxFail = parseInt(document.getElementById('sec-maxfail').value) || 5;
G.secSettings.lockDur = parseInt(document.getElementById('sec-lockdur').value) || 15;
notify('Pengaturan keamanan disimpan', 'ok');
}
function sendVerif() { notify('Email verifikasi dikirim', 'ok'); }
/* ══ API KEYS ══ */
function detProv(v, dId) {
const el = document.getElementById(dId); if (!el || !v) { if (el) el.innerHTML = ''; return; }
let p = '', c = '';
if (v.startsWith('gsk_')) { p = 'Groq'; c = 'var(--ok)'; }
else if (v.startsWith('sk-ant-')) { p = 'Anthropic Claude'; c = 'var(--inf)'; }
else if (v.startsWith('sk-proj-') || v.startsWith('sk-')) { p = 'OpenAI'; c = 'var(--ok)'; }
else if (v.startsWith('AIzaSy')) { p = 'Google Gemini'; c = '#ea4335'; }
else if (v.startsWith('xkeysib-')) { p = 'Brevo'; c = 'var(--inf)'; }
else { p = 'Provider tidak dikenali'; c = 'var(--t3)'; }
el.innerHTML = `