TKK_E32230273/new_account.php

970 lines
42 KiB
PHP

<?php include 'header.php'; ?>
<!-- partial -->
<div class="main-panel">
<div class="content-wrapper">
<div class="row">
<div class="col-md-12 grid-margin">
<div class="row">
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
<h3 class="font-weight-bold">New Account</h3>
<h6 class="font-weight-normal mb-0">Register a new account, <span class="text-primary">Registering a new
account!</span></h6>
</div>
<div class="col-12 col-xl-4">
<div class="justify-content-end d-flex flex-wrap" style="gap:15px; align-items:center;">
<div class="justify-content-end d-flex">
<div class="dropdown flex-md-grow-1 flex-xl-grow-0">
<button class="btn btn-primary mb-3" data-toggle="modal" data-target="#modalTambah">
<i class="mdi mdi-account-plus mr-1"></i> Add
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 grid-margin stretch-card">
<div class="card" style="border: 1px solid #e2e8f0; border-radius: 10px;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="card-title mb-0">Data Account</p>
<div>
<button class="btn btn-sm btn-outline-light" id="btnSelectMode"
style="font-weight:bold; border-radius:6px; padding: 6px 10px; font-size:16px;"
title="Select Multiple"><i class="mdi mdi-checkbox-multiple-marked-outline"></i></button>
<button class="btn btn-sm btn-danger ml-2" id="btnBulkDelete"
style="display:none; font-weight:bold; border-radius:6px; padding: 6px 10px; font-size:16px;"
title="Delete Selected"><i class="mdi mdi-delete"></i></button>
</div>
</div>
<div class="table-responsive">
<table class="table table-borderless" id="myTable">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th>Status</th>
<th style="width:60px; text-align:center;">Action</th>
</tr>
</thead>
<?php
include 'koneksi.php';
$stmt = $pdo->query("SELECT * FROM account WHERE status = 'rejected' ORDER BY id_account ASC");
$accounts = $stmt->fetchAll();
?>
<tbody>
<?php
$no = 1;
foreach ($accounts as $row):
$isChecked = ($row['status'] === 'approved') ? 'checked' : '';
$statusToggle = '<label class="ios-switch mb-0" onclick="event.stopPropagation();" title="Toggle Status">
<input type="checkbox" onchange="toggleStatus(\'' . $row['id_account'] . '\')" ' . $isChecked . '>
<span class="ios-slider"></span>
</label>';
$sc = ($no % 2 == 1) ? 'row-odd' : 'row-even';
?>
<tr class="<?= $sc; ?>">
<td style="vertical-align:middle; font-weight:600; color:#555;"><?= $no++; ?></td>
<td style="vertical-align:middle;">
<div class="d-flex align-items-center"><span
style="font-weight:500; font-size:14px; color:#212529;"><?= htmlspecialchars($row['name']); ?></span>
</div>
</td>
<td style="vertical-align:middle;"><?= $statusToggle; ?></td>
<td style="vertical-align:middle; text-align:center;">
<button class="btn btn-sm btn-danger text-white btn-row-delete"
onclick="openDeleteModal('<?= $row['id_account']; ?>', '<?= addslashes(htmlspecialchars($row['name'])); ?>')"
style="border-radius:6px; font-weight:bold; width:32px; padding: 4px 0; font-size:14px; display:inline-flex; align-items:center; justify-content:center;">
<i class="mdi mdi-delete"></i>
</button>
<input type="checkbox" class="bulk-cb" value="<?= $row['id_account']; ?>"
style="display:none; transform: scale(1.5); cursor:pointer; margin-top:5px;">
</td>
</tr>
<?php
endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== MODAL: TAMBAH ==================== -->
<div class="modal fade" id="modalTambah" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content mdl-card">
<form id="formTambahAccount" enctype="multipart/form-data">
<div class="modal-header">
<h6 class="modal-title font-weight-bold"><i class="mdi mdi-account-plus mr-1 text-primary"></i> Add Account
</h6>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body py-3">
<div class="form-group mb-2">
<label class="frm-lbl">Profile Photo</label>
<div class="custom-file-upload form-control form-control-sm">
<input type="file" name="foto" id="fotoInputAdd" class="d-none" accept="image/*" required onchange="var txt=document.getElementById('fotoTextAdd'); if(this.files.length>0){ txt.textContent=this.files[0].name; txt.style.color='#495057'; }else{ txt.textContent='Tidak ada file yang dipilih'; txt.style.color='#c9c8c8'; }">
<label for="fotoInputAdd" class="file-btn">Pilih File</label>
<span class="file-text" id="fotoTextAdd">Tidak ada file yang dipilih</span>
</div>
<div id="faceDetectPreviewAdd" style="display:none; margin-top:8px;">
<div id="faceDetectBadgeAdd"
style="padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center;">
</div>
</div>
<input type="hidden" id="faceVerifiedFlagAdd" value="0">
</div>
<div class="row">
<div class="col-6">
<div class="form-group mb-2"><label class="frm-lbl">Full Name</label><input type="text" name="name"
class="form-control form-control-sm" placeholder="Full name" required></div>
</div>
<div class="col-6">
<div class="form-group mb-2"><label class="frm-lbl">Username</label><input type="text" name="username"
class="form-control form-control-sm" placeholder="Username" required></div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group mb-2">
<label class="frm-lbl">Password</label>
<div class="input-group">
<input type="password" name="password" id="inputPasswordField" class="form-control form-control-sm"
placeholder="Password" required>
<div class="input-group-append">
<span class="input-group-text eye-toggle" data-target="inputPasswordField"><i
class="mdi mdi-eye-off"></i></span>
</div>
</div>
<div class="progress mt-1" style="height: 5px; display: none;" id="pwStrengthMeterAdd">
<div class="progress-bar" id="pwStrengthBarAdd" role="progressbar" style="width: 0%;"></div>
</div>
<small id="pwStrengthTextAdd" class="form-text mt-1 mb-1 font-weight-bold"
style="font-size: 11px;"></small>
<small class="text-muted d-block mt-0" style="font-size: 10px; line-height: 1.2;">*Minimum 8
characters, uppercase, lowercase, numbers & symbols.</small>
</div>
</div>
<div class="col-6">
<div class="form-group mb-2">
<label class="frm-lbl">Confirm Password</label>
<div class="input-group">
<input type="password" id="confirmPasswordField" class="form-control form-control-sm"
placeholder="Repeat Password" required>
<div class="input-group-append">
<span class="input-group-text eye-toggle" data-target="confirmPasswordField"><i
class="mdi mdi-eye-off"></i></span>
</div>
</div>
</div>
</div>
</div>
<div id="passwordMismatch" class="text-danger mb-2" style="display:none; font-size:12px; font-weight:600;">
<i class="mdi mdi-alert-circle mr-1"></i>Password and Confirm Password do not match!</div>
<div class="form-group mb-1 mt-2">
<label class="frm-lbl">PIN (6 Digit)</label>
<input type="password" name="pin" id="inputPinField" maxlength="6" class="form-control form-control-sm"
placeholder="6 digit PIN" pattern="\d{6}" required style="letter-spacing:4px;">
<small class="text-muted" style="font-size:10px;"><i class="mdi mdi-information mr-1"></i>PIN is required
to view the password on the operations page.</small>
</div>
</div>
<div class="modal-footer py-2">
<button type="button" class="btn btn-light btn-sm" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm" id="btnSubmitAccount"><i
class="mdi mdi-content-save mr-1"></i>Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- ==================== MODAL: DELETE ==================== -->
<div class="modal fade" id="modalDeleteAccount" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content mdl-card">
<form id="formDeleteAccount">
<div class="modal-body p-4" style="text-align: left;">
<input type="hidden" name="id_account" id="delete_id_account">
<h5 class="font-weight-bold mb-3">Delete Account</h5>
<p style="font-size:14px;color:#666;margin-bottom:20px;">Are you sure you want to delete <strong id="delete_account_name"></strong>?</p>
<div class="custom-control custom-checkbox mb-4" style="display:flex; align-items:center;">
<input type="checkbox" class="custom-control-input delete-confirm-cb" id="deleteConfirmCheckAccount" required>
<label class="custom-control-label" for="deleteConfirmCheckAccount" style="font-size:14px; color:#1e1f23; padding-top:2px;">I understand that all of my data will be deleted</label>
</div>
<div class="d-flex justify-content-end" style="gap:8px;">
<button type="button" class="btn btn-light font-weight-bold" style="padding:8px 18px;font-size:13px;" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger font-weight-bold" style="padding:8px 18px;font-size:13px;" id="btnDeleteAccount" disabled>Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script>
(function(){
var cb = document.getElementById('deleteConfirmCheckAccount');
var btn = document.getElementById('btnDeleteAccount');
if(cb && btn){
cb.addEventListener('change', function(){ btn.disabled = !this.checked; });
}
$('#modalDeleteAccount').on('hidden.bs.modal', function(){ if(cb){cb.checked=false;} if(btn){btn.disabled=true;} });
})();
</script>
<!-- ==================== MODAL: FINGERPRINT REGISTRATION ==================== -->
<div class="modal fade" id="modalFingerprint" tabindex="-1" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog">
<div class="modal-content mdl-card">
<div class="modal-header">
<h6 class="modal-title font-weight-bold" style="font-size:14px;"><i
class="mdi mdi-fingerprint mr-1 text-primary"></i> Fingerprint Registration</h6>
<button type="button" class="close" data-dismiss="modal" onclick="location.reload();">&times;</button>
</div>
<div class="modal-body" style="padding: 15px 20px;">
<div class="info-alert mb-3">
<div class="info-alert-icon" style="width:20px; height:20px;"><i class="mdi mdi-check-circle-outline"
style="font-size:12px;"></i></div>
<div class="info-alert-text"
style="flex:1;display:flex;align-items:center;justify-content:space-between; font-size:11px;">
<span><strong>Data Saved Successfully!</strong> Please register a fingerprint for this account.<span
id="fingerprintCountdown" style="color:#dc2626; font-weight:700; margin-left:5px;">30:00</span></span>
</div>
</div>
<input type="hidden" id="fingerprintAccountId">
<div class="row">
<!-- Pilihan 1: Scan Langsung via WebAuthn -->
<div class="col-md-6 mb-3 mb-md-0 border-right text-center">
<label class="frm-lbl" style="font-size:12px; margin-bottom:2px;"><i
class="mdi mdi-laptop-mac mr-1"></i>Scan on This Device</label>
<p style="font-size:10px; color:#777; height:25px; line-height:1.2; margin-bottom:15px;">Use the biometric
sensor on your current device.</p>
<i class="mdi mdi-fingerprint"
style="font-size:72px; color:#4b49ac; display:block; margin-bottom:15px; text-shadow: 0 4px 10px rgba(75,73,172,0.2);"></i>
<button type="button" class="btn btn-primary d-flex align-items-center justify-content-center mx-auto"
style="border-radius:8px; padding:8px 16px; font-weight:bold; font-size:13px; letter-spacing:0.5px;"
id="btnScanFingerHere" onclick="startWebAuthnRegistration()">
Scan Finger Here
</button>
<div id="webAuthnStatusWrapper" style="display:none;" class="mt-3 text-left">
<div
style="background-color:#f0fdf4; border:1px solid #22c55e; border-radius:8px; padding:12px; display:flex; gap:10px; align-items:center;">
<i class="mdi mdi-check-circle" style="color:#22c55e; font-size:20px; flex-shrink:0;"></i>
<div style="font-size:11px; line-height:1.4; color:#1e293b;">
<span style="font-weight:bold;">Fingerprint successfully added.</span> Please click 'X' to close
this window.
</div>
</div>
</div>
<div id="webAuthnStatus" class="mt-2 text-primary"
style="display:none; font-size:11px; font-weight:bold;"></div>
<div id="webAuthnError" class="mt-2 text-danger" style="display:none; font-size:11px; font-weight:bold;">
</div>
</div>
<!-- Pilihan 2: Opsi QR Code -->
<div class="col-md-6 text-center">
<label class="frm-lbl" style="font-size:12px; margin-bottom:2px;"><i
class="mdi mdi-qrcode-scan mr-1"></i>Other Device Option</label>
<p style="font-size:10px; color:#777; line-height:1.2; margin-bottom:10px;">Scan QR Code below to register
via smartphone.</p>
<div id="fingerprintQrLoadingView" style="margin:20px 0;">
<div class="spinner-border text-primary" role="status" style="width:1.5rem; height:1.5rem;"><span
class="sr-only">Loading...</span></div>
<div style="font-size:11px; margin-top:5px; color:#666;">Generating QR Code...</div>
</div>
<div id="fingerprintQrDefaultView"
style="display:none; padding:15px; border:1px dashed #ccc; border-radius:8px; margin-bottom:15px;">
<i class="mdi mdi-qrcode-off text-muted" style="font-size:24px;"></i>
<div style="font-size:11px; color:#888;">Failed to load QR</div>
</div>
<div id="fingerprintQrView" style="display:none;">
<div class="text-center">
<div id="fingerprintQrCanvasWrapper"
style="display:inline-block; padding:8px; background:#fff; border-radius:10px; border:2px solid #e0e7ff; box-shadow:0 4px 12px rgba(0,0,0,0.08); margin-bottom:10px;">
<div id="fingerprintQrCanvas"></div>
</div>
</div>
<div class="input-group mb-2">
<input type="text" class="form-control form-control-sm" id="fingerQrLinkText" readonly
style="font-size:11px; padding:4px;">
<div class="input-group-append">
<button class="btn btn-outline-light text-dark btn-sm" type="button" onclick="copyFingerQrLink()"
style="padding:4px 10px;"><i class="mdi mdi-content-copy mr-1"></i>Copy</button>
</div>
</div>
<button type="button" class="btn btn-primary btn-sm btn-block mt-2" onclick="downloadFingerQr()"
style="padding:10px; border-radius:8px; font-weight:bold;"><i
class="mdi mdi-download mr-1"></i>Download QR Code</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ========================== CSS ========================== -->
<style>
.ios-switch {
position: relative; display: inline-block; width: 32px; height: 18px; margin: 0;
}
.ios-switch input { opacity: 0; width: 0; height: 0; }
.ios-slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #e5e5ea;
transition: .4s cubic-bezier(0.4, 0.0, 0.2, 1); border-radius: 26px;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
.ios-slider:before {
position: absolute; content: ""; height: 14px; width: 14px; left: 2px; bottom: 2px;
background-color: white; transition: .4s cubic-bezier(0.4, 0.0, 0.2, 1); border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.ios-switch input:checked + .ios-slider { background-color: #1e1f23; }
.ios-switch input:checked + .ios-slider:before { transform: translateX(14px); }
/* ── MANUAL STRIPES & BORDERS ── */
#myTable thead th {
border-bottom: 1px solid #e2e8f0 !important;
}
#myTable tbody tr td {
border-bottom: none !important;
}
.pill {
display: inline-flex;
align-items: center;
font-weight: 600;
font-size: 11px;
padding: 3px 12px;
border-radius: 20px;
}
.pill-green {
background: #e8f5e9;
color: #2e7d32;
}
.pill-red {
background: #ffebee;
color: #c62828;
}
.pill-amber {
background: #fff8e1;
color: #f57f17;
}
.pill-gray {
background: #f5f5f5;
color: #757575;
}
.mdl-card {
border: none;
border-radius: 6px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
}
.confirm-circle {
width: 50px;
height: 50px;
border-radius: 50%;
border: 2.5px solid #ef5350;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
}
.confirm-circle span {
font-size: 28px;
color: #ef5350;
line-height: 1;
}
.delete-ok-msg {
display: none;
margin-top: 12px;
font-size: 13px;
color: #43a047;
font-weight: 500;
}
.frm-lbl {
font-size: 12px;
font-weight: 600;
color: #555;
margin-bottom: 3px;
}
.pin-warning {
display: flex;
align-items: flex-start;
gap: 8px;
background: #fffbeb;
border: 1px solid #fcd34d;
border-radius: 6px;
padding: 10px 12px;
font-size: 12px;
color: #92400e;
}
.pin-warning i {
font-size: 18px;
color: #f59e0b;
flex-shrink: 0;
margin-top: 1px;
}
.eye-toggle {
cursor: pointer;
background: #fff;
border-left: none;
}
.eye-toggle:hover i {
color: #4b7bec;
}
.info-alert {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 13px;
color: #1e293b;
}
.info-alert-icon {
width: 22px;
height: 22px;
border-radius: 50%;
background: #1d4ed8;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.info-alert-icon i {
color: #e0ecff;
font-size: 14px;
line-height: 1;
}
.custom-file-upload {
display: flex;
align-items: center;
background: #fff;
overflow: hidden;
padding: 0 !important;
}
.custom-file-upload .file-btn {
background: #f8f9fa;
border-right: 1px solid #CED4DA;
padding: 0 12px;
margin: 0;
height: 100%;
display: flex;
align-items: center;
color: #495057;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.custom-file-upload .file-text {
padding: 0 12px;
color: #c9c8c8;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
display: flex;
align-items: center;
height: 100%;
}
.custom-file-upload .file-btn:hover {
background: #e2e6ea;
}
.delete-confirm-cb:checked ~ .custom-control-label::before {
background-color: #ff4747 !important;
border-color: #ff4747 !important;
}
</style>
<!-- content-wrapper ends -->
<?php include 'footer.php'; ?>
<?php include 'copy.php'; ?>
<!-- ========================== JS ========================== -->
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
<script src="js/qr_card_designer.js"></script>
<script src="js/face-api/face-api.min.js"></script>
<script>
document.querySelectorAll('.eye-toggle').forEach(function (b) {
b.addEventListener('click', function () {
var t = document.getElementById(this.dataset.target), i = this.querySelector('i');
if (t.type === 'password') { t.type = 'text'; i.classList.replace('mdi-eye-off', 'mdi-eye'); }
else { t.type = 'password'; i.classList.replace('mdi-eye', 'mdi-eye-off'); }
});
});
function copyFingerQrLink() {
navigator.clipboard.writeText(document.getElementById('fingerQrLinkText').value);
showCustomAlert('Fingerprint verification link copied successfully!');
}
function downloadFingerQr() {
var qrCanvas = document.querySelector('#fingerprintQrCanvas canvas');
if (!qrCanvas) { alert("QR Code has not been generated yet."); return; }
var imgUrl = createCompositeQrImage(qrCanvas);
var dlLink = document.createElement('a');
dlLink.download = 'QR_Fingerprint_Account.png';
dlLink.href = imgUrl;
dlLink.click();
}
function showFingerprintModal(accountId) {
document.getElementById("fingerprintAccountId").value = accountId;
document.getElementById("webAuthnStatusWrapper").style.display = "none";
document.getElementById("webAuthnStatus").style.display = "none";
document.getElementById("webAuthnError").style.display = "none";
$("#modalFingerprint").modal("show");
// Generate QR Code untuk akun ini (kita perlu endpoint generate_finger_token_account.php)
document.getElementById("fingerprintQrLoadingView").style.display = 'block';
document.getElementById("fingerprintQrView").style.display = 'none';
document.getElementById("fingerprintQrDefaultView").style.display = 'none';
fetch("generate_finger_token_account.php?id=" + accountId)
.then(r => r.json())
.then(d => {
document.getElementById("fingerprintQrLoadingView").style.display = 'none';
if (d.status === 'success') {
document.getElementById("fingerprintQrView").style.display = 'block';
document.getElementById("fingerQrLinkText").value = d.url;
var canvas = document.getElementById("fingerprintQrCanvas");
canvas.innerHTML = '';
new QRCode(canvas, {
text: d.url, width: 150, height: 150,
colorDark: '#0f172a', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H
});
} else {
document.getElementById("fingerprintQrDefaultView").style.display = 'block';
}
}).catch(e => {
document.getElementById("fingerprintQrLoadingView").style.display = 'none';
document.getElementById("fingerprintQrDefaultView").style.display = 'block';
});
}
async function startWebAuthnRegistration() {
var btn = document.getElementById("btnScanFingerHere");
var statusEl = document.getElementById("webAuthnStatus");
var errorEl = document.getElementById("webAuthnError");
var successWrapper = document.getElementById("webAuthnStatusWrapper");
var accountId = document.getElementById("fingerprintAccountId").value;
btn.disabled = true;
errorEl.style.display = "none";
successWrapper.style.display = "none";
statusEl.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Waiting for Sensor...';
statusEl.style.display = "block";
if (!window.PublicKeyCredential) {
statusEl.style.display = "none";
errorEl.innerHTML = '<i class="mdi mdi-alert mr-1"></i>WebAuthn is not supported in this browser.';
errorEl.style.display = "block";
btn.disabled = false;
return;
}
try {
const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);
const userId = new Uint8Array(16); crypto.getRandomValues(userId);
<?php
$stmtEx = $pdo->query("SELECT finger_id FROM account WHERE finger_id IS NOT NULL AND finger_id != ''");
$allCredsArray = $stmtEx->fetchAll(PDO::FETCH_COLUMN);
?>
const allExistingCreds = <?= json_encode($allCredsArray) ?>;
const excludeList = allExistingCreds.map(idBase64 => {
return {
type: "public-key",
id: Uint8Array.from(atob(idBase64), c => c.charCodeAt(0))
};
});
const publicKey = {
challenge: challenge,
excludeCredentials: excludeList,
rp: { name: "Identia App", id: window.location.hostname },
user: { id: userId, name: "Identia_user", displayName: "Identia_user" },
pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -257 }],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
residentKey: "required",
requireResidentKey: true
},
timeout: 60000,
attestation: "none"
};
const credential = await navigator.credentials.create({ publicKey });
const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));
statusEl.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Saving Fingerprint...';
// Simpan credential ID
let fd = new FormData();
fd.append('id_account', accountId);
fd.append('credential_id', rawIdBase64);
fetch("save_fingerprint_account.php", { method: "POST", body: fd })
.then(r => r.json())
.then(d => {
if (d.status === 'success') {
statusEl.style.display = "none";
successWrapper.style.display = "block";
btn.style.display = "none";
} else {
statusEl.style.display = "none";
errorEl.innerHTML = '<i class="mdi mdi-alert mr-1"></i>' + (d.message || 'Failed to save to server');
errorEl.style.display = "block";
btn.disabled = false;
}
}).catch(e => {
statusEl.style.display = "none";
errorEl.innerHTML = '<i class="mdi mdi-alert mr-1"></i>A Network Error Occurred.';
errorEl.style.display = "block";
btn.disabled = false;
});
} catch (err) {
statusEl.style.display = "none";
errorEl.innerHTML = '<i class="mdi mdi-alert mr-1"></i>Registration Cancelled / Failed (' + err.message + ')';
errorEl.style.display = "block";
btn.disabled = false;
}
}
// Timer Logic for Modal
var fingerprintTimerId = null;
function startFingerprintTimer() {
var remaining = 30 * 60; // 30 minutes in seconds
var countdownEl = document.getElementById('fingerprintCountdown');
function updateDisplay() {
var m = Math.floor(remaining / 60);
var s = remaining % 60;
countdownEl.innerHTML = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
updateDisplay();
fingerprintTimerId = setInterval(function () {
remaining--;
updateDisplay();
if (remaining <= 0) {
clearInterval(fingerprintTimerId);
fingerprintTimerId = null;
$('#modalFingerprint').modal('hide');
location.reload();
}
}, 1000);
}
function stopFingerprintTimer() {
if (fingerprintTimerId) {
clearInterval(fingerprintTimerId);
fingerprintTimerId = null;
}
}
// Tambah Account
// The original formTambahAccount submit handler is replaced by the new one below.
// document.getElementById("formTambahAccount").addEventListener("submit", function(e) {
// e.preventDefault();
// var btn = document.getElementById("btnSubmitAccount");
// btn.disabled = true;
// btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Menyimpan...';
// fetch("tambah_account.php", { method: "POST", body: new FormData(this) })
// .then(function(r){ return r.json(); })
// .then(function(d) {
// btn.disabled = false; btn.innerHTML = '<i class="mdi mdi-content-save mr-1"></i>Simpan';
// if (d.status === "success") {
// $("#modalTambah").modal("hide");
// document.getElementById("formTambahAccount").reset();
// showCustomAlert("Akun berhasil ditambahkan.");
// setTimeout(function(){ location.reload(); }, 1500);
// } else {
// showCustomAlert(d.message || "Gagal menyimpan data.");
// }
// }).catch(function(){
// btn.disabled=false;
// btn.innerHTML='<i class="mdi mdi-content-save mr-1"></i>Simpan';
// showCustomAlert("Koneksi ke server gagal.");
// });
// });
function openDeleteModal(id, name) {
document.getElementById("delete_id_account").value = id;
document.getElementById("delete_account_name").innerText = name;
$("#modalDeleteAccount").modal("show");
}
function toggleStatus(id) {
$.post("toggle_status_account.php", { id_account: id }, function (d) {
if (d.status === "success") location.reload();
else showCustomAlert(d.message || "Failed to process.");
}, "json");
}
$(document).ready(function () {
window.isSelectMode = false;
$('#btnSelectMode').on('click', function () {
window.isSelectMode = !window.isSelectMode;
if (window.isSelectMode) {
$(this).html('<i class="mdi mdi-close"></i>').removeClass('btn-outline-secondary').addClass('btn-light').attr('title', 'Cancel Selection');
$('#btnBulkDelete').css('display', 'inline-block');
$('.btn-row-delete').hide();
$('.bulk-cb').css('display', 'inline-block').prop('checked', false);
} else {
$(this).html('<i class="mdi mdi-checkbox-multiple-marked-outline"></i>').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');
$('#btnBulkDelete').hide();
$('.btn-row-delete').css('display', 'inline-flex');
$('.bulk-cb').hide().prop('checked', false);
}
});
$('#btnBulkDelete').on('click', function () {
const selected = $('.bulk-cb:checked').map(function () { return $(this).val(); }).get();
if (selected.length === 0) {
showCustomAlert('Please select at least one item to delete!', 'warning');
return;
}
window.bulkDeleteIds = selected;
document.getElementById("delete_id_account").value = "BULK";
document.getElementById("delete_account_name").innerText = selected.length + " items selected";
$("#modalDeleteAccount").modal("show");
});
$("#formTambahAccount").submit(function (e) {
e.preventDefault();
// Check face verification
if (document.getElementById('faceVerifiedFlagAdd').value !== '1') {
showCustomAlert('Please upload a profile photo with a detectable face first.', 'warning');
return;
}
var pw1 = document.getElementById("inputPasswordField").value;
var pw2 = document.getElementById("confirmPasswordField").value;
const pwValid = (pw1.length >= 8 && /[a-z]/.test(pw1) && /[A-Z]/.test(pw1) && /\d/.test(pw1) && /[^A-Za-z0-9]/.test(pw1));
if (!pwValid) {
showCustomAlert("Password does not meet the criteria (min. 8 characters, uppercase, lowercase, numbers, and symbols)!", "warning");
document.getElementById("inputPasswordField").focus();
return;
}
if (pw1 !== pw2) {
document.getElementById("passwordMismatch").style.display = "block";
return;
} else {
document.getElementById("passwordMismatch").style.display = "none";
}
var btn = document.getElementById("btnSubmitAccount");
btn.disabled = true;
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Saving...';
fetch("tambah_account.php", { method: "POST", body: new FormData(this) })
.then(function (r) { return r.json(); })
.then(function (d) {
btn.disabled = false; btn.innerHTML = '<i class="mdi mdi-content-save mr-1"></i>Save';
if (d.status === "success") {
$("#modalTambah").modal("hide");
// Langsung tampilkan modal fingerprint dan proses akun id nya
showFingerprintModal(d.id_account);
} else {
showCustomAlert(d.message || "Failed to save account.");
}
}).catch(function () {
btn.disabled = false;
btn.innerHTML = '<i class="mdi mdi-content-save mr-1"></i>Save';
showCustomAlert("Connection to server failed.");
});
});
$("#formDeleteAccount").submit(function (e) {
e.preventDefault();
var val = $("#delete_id_account").val();
if (val === "BULK") {
$("#btnDeleteAccount").prop("disabled", true);
$("#btnDeleteAccount").html('<i class="mdi mdi-loading mdi-spin mr-1"></i>Deleting...');
let promises = window.bulkDeleteIds.map(id => {
let fd = new FormData();
fd.append('id_account', id);
return fetch('delete_account.php', { method: 'POST', body: fd }).then(r => r.json());
});
Promise.all(promises).then(() => {
$("#deleteSuccessAlert").css("display", "block");
setTimeout(function () { $("#modalDeleteAccount").modal("hide"); location.reload(); }, 1200);
});
} else {
$.post("delete_account.php", $(this).serialize(), function (d) {
if (d.status === "success") {
$("#deleteSuccessAlert").css("display", "block");
$("#btnDeleteAccount").prop("disabled", true);
setTimeout(function () { $("#modalDeleteAccount").modal("hide"); location.reload(); }, 1200);
} else {
showCustomAlert(d.message);
}
}, "json");
}
});
$('#modalTambah').on('hidden.bs.modal', function () {
document.getElementById("formTambahAccount").reset();
});
$('#modalFingerprint').on('shown.bs.modal', function () { startFingerprintTimer(); });
$('#modalFingerprint').on('hidden.bs.modal', function () { stopFingerprintTimer(); location.reload(); });
if ($.fn.DataTable.isDataTable('#myTable')) {
$('#myTable').DataTable().destroy();
}
$('#myTable').DataTable({ "order": [[0, 'asc']] });
$('#inputPasswordField').on('input', function () {
var pass = $(this).val();
var meter = $('#pwStrengthMeterAdd');
var bar = $('#pwStrengthBarAdd');
var text = $('#pwStrengthTextAdd');
if (pass.length > 0) {
meter.show();
var hasLower = /[a-z]/.test(pass);
var hasUpper = /[A-Z]/.test(pass);
var hasNumber = /\d/.test(pass);
var hasSymbol = /[^A-Za-z0-9]/.test(pass);
var typesCount = (hasLower ? 1 : 0) + (hasUpper ? 1 : 0) + (hasNumber ? 1 : 0) + (hasSymbol ? 1 : 0);
if (pass.length >= 8 && typesCount === 4) {
bar.css({ width: '100%', backgroundColor: '#10b981' }); // Kuat
text.text('Strong Password').css('color', '#10b981');
} else if (pass.length >= 6 && typesCount >= 2) {
bar.css({ width: '66%', backgroundColor: '#f59e0b' }); // Sedang
text.text('Moderate Password').css('color', '#f59e0b');
} else {
bar.css({ width: '33%', backgroundColor: '#ef4444' }); // Lemah
text.text('Weak Password').css('color', '#ef4444');
}
} else {
meter.hide();
text.text('');
}
});
// ── Face Detection on Photo Upload (Add Modal) ──
var faceModelLoadedAdd = false;
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function () {
faceModelLoadedAdd = true;
});
$('#fotoInputAdd').on('change', function () {
var file = this.files[0];
if (!file) {
document.getElementById('faceDetectPreviewAdd').style.display = 'none';
return;
}
var preview = document.getElementById('faceDetectPreviewAdd');
var badge = document.getElementById('faceDetectBadgeAdd');
preview.style.display = 'block';
badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#e2e8f0; color:#475569; border:1px solid #cbd5e1;';
badge.innerHTML = '<div class="spinner-border spinner-border-sm text-secondary mr-2" role="status"></div> Face Verification...';
document.getElementById('faceVerifiedFlagAdd').value = '0';
var reader = new FileReader();
reader.onload = function (ev) {
var img = new Image();
img.src = ev.target.result;
img.onload = function () {
function doDetect() {
faceapi.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.3 })).then(function (dets) {
if (dets.length === 1) {
badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#f0fdf4; color:#16a34a; border:1px solid #86efac;';
badge.innerHTML = '<i class="mdi mdi-check-circle"></i> Face Detected';
document.getElementById('faceVerifiedFlagAdd').value = '1';
} else if (dets.length > 1) {
badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#fff7ed; color:#ea580c; border:1px solid #fdba74;';
badge.innerHTML = '<i class="mdi mdi-alert"></i> ' + dets.length + ' Faces Detected (Please use 1 face)';
document.getElementById('faceVerifiedFlagAdd').value = '0';
} else {
badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#fef2f2; color:#dc2626; border:1px solid #fca5a5;';
badge.innerHTML = '<i class="mdi mdi-close-circle"></i> No Face Detected';
document.getElementById('faceVerifiedFlagAdd').value = '0';
}
}).catch(function () {
badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#fef2f2; color:#dc2626; border:1px solid #fca5a5;';
badge.innerHTML = '<i class="mdi mdi-alert-circle"></i> Detection Error';
});
}
if (!faceModelLoadedAdd) {
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function () {
faceModelLoadedAdd = true;
doDetect();
});
} else {
doDetect();
}
};
};
reader.readAsDataURL(file);
});
// Reset face verification when modal closes
$('#modalTambah').on('hidden.bs.modal', function () {
document.getElementById('faceVerifiedFlagAdd').value = '0';
document.getElementById('faceDetectPreviewAdd').style.display = 'none';
document.getElementById('faceDetectBadgeAdd').style.display = 'none';
});
});
</script>