1069 lines
48 KiB
PHP
1069 lines
48 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">Operations Account</h3>
|
|
<h6 class="font-weight-normal mb-0">Manage operational account data, <span class="text-primary">Managing
|
|
active accounts!</span></h6>
|
|
</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>Username</th>
|
|
<th>Finger ID</th>
|
|
<th>Status</th>
|
|
<th style="width:80px; text-align:center;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<?php
|
|
include 'koneksi.php';
|
|
$stmt = $pdo->query("SELECT * FROM account WHERE status = 'approved' 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>';
|
|
|
|
$fotoSrc = (!empty($row['foto']) && file_exists($row['foto'])) ? $row['foto'] : 'images/faces/user.jpg';
|
|
$hasFoto = (!empty($row['foto']) && file_exists($row['foto'])) ? '1' : '0';
|
|
$fingerId = !empty($row['finger_id']) ? htmlspecialchars($row['finger_id']) : '';
|
|
$fingerOk = !empty($row['finger_id']);
|
|
$sc = ($no % 2 == 1) ? 'row-odd' : 'row-even';
|
|
?>
|
|
<tr class="<?= $sc; ?>">
|
|
<td style="vertical-align:middle; font-weight:600; color:#555;">
|
|
<span style="position: relative; display: inline-block;">
|
|
<?= $no++; ?>
|
|
<?php if(isset($_SESSION['user_id']) && $row['id_account'] == $_SESSION['user_id']): ?>
|
|
<i class="mdi mdi-star" style="color: #ffda07ff; font-size: 10px; position: absolute; top: -4px; right: -8px; line-height: 1;" title="Your Account"></i>
|
|
<?php endif; ?>
|
|
</span>
|
|
</td>
|
|
<td style="vertical-align:middle;">
|
|
<div class="d-flex align-items-center">
|
|
<img src="<?= htmlspecialchars($fotoSrc); ?>"
|
|
style="width: 32px; height: 32px; border-radius: 6px; object-fit: cover; margin-right:10px; border: 1px solid #e2e8f0; cursor: pointer;"
|
|
onclick="previewPhoto('<?= htmlspecialchars(addslashes($fotoSrc)); ?>')">
|
|
<span
|
|
style="font-weight:500; font-size:14px; color:#212529; display:inline-flex; align-items:center;"><?= htmlspecialchars($row['name']); ?><i
|
|
class="mdi mdi-check-decagram" style="color: #1da1f2; font-size: 16px; margin-left: 4px;"
|
|
title="Verified Approved"></i></span>
|
|
</div>
|
|
</td>
|
|
<td style="vertical-align:middle; font-size:13px; color:#495057;">
|
|
<?= htmlspecialchars($row['username'] ?: '-'); ?></td>
|
|
<td
|
|
style="vertical-align:middle; font-family:'Courier New', monospace; font-size:13px; font-weight:600; color:#6c757d;">
|
|
<?php if ($fingerOk): ?>
|
|
<span
|
|
title="<?= $fingerId; ?>"><?= strlen($fingerId) > 15 ? substr($fingerId, 0, 15) . '...' : $fingerId; ?></span>
|
|
<?php else: ?>
|
|
<button class="btn btn-custom-dashboard font-weight-bold"
|
|
style="padding:4px 12px; font-size:12px; border-radius:6px; font-family:sans-serif; height:32px; display:inline-flex; align-items:center; justify-content:center;"
|
|
onclick="showFingerprintModal('<?= $row['id_account']; ?>')"><i class="mdi mdi-qrcode mr-1"
|
|
style="font-size:16px;"></i>QR Code</button>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="vertical-align:middle;"><?= $statusToggle; ?></td>
|
|
<td style="vertical-align:middle; text-align:center; white-space:nowrap;">
|
|
<div class="d-flex align-items-center justify-content-center">
|
|
<div class="dropdown btn-row-delete">
|
|
<button class="btn btn-sm dropdown-dots-btn" type="button" data-toggle="dropdown"
|
|
aria-haspopup="true" aria-expanded="false" style="padding: 4px 8px;">
|
|
<i class="mdi mdi-dots-vertical"></i>
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-right"
|
|
style="border-radius: 5px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); border: 1px solid #f1f2f4; min-width: 150px; padding: 0; overflow: hidden;">
|
|
<a href="dashboard_account.php?id=<?= $row['id_account']; ?>" class="dropdown-item d-flex align-items-center py-3 px-4 btn-custom-dropdown" style="text-decoration:none;">
|
|
<span style="font-size: 14px; font-weight: 400; color: #1f2125;">Dashboard</span>
|
|
</a>
|
|
<button class="dropdown-item d-flex align-items-center py-3 px-4 btn-custom-dropdown" type="button"
|
|
onclick="openEditModal('<?= $row['id_account']; ?>','<?= htmlspecialchars(addslashes($row['name'])); ?>','<?= htmlspecialchars(addslashes($row['username'])); ?>')">
|
|
<span style="font-size: 14px; font-weight: 400; color: #1f2125;">Edit Data</span>
|
|
</button>
|
|
<button class="dropdown-item d-flex align-items-center py-3 px-4 btn-custom-dropdown" type="button"
|
|
onclick="openDeleteModal('<?= $row['id_account']; ?>','<?= htmlspecialchars(addslashes($row['name'])); ?>')">
|
|
<span style="font-size: 14px; font-weight: 400; color: #ff4d4f;">Delete</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<input type="checkbox" class="bulk-cb" value="<?= $row['id_account']; ?>"
|
|
style="display:none; transform: scale(1.5); cursor:pointer; margin-left:10px; margin-top:0;">
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== MODAL: EDIT ==================== -->
|
|
<div class="modal fade" id="modalEdit" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content mdl-card">
|
|
<form id="formEditAccount" enctype="multipart/form-data">
|
|
<input type="hidden" name="id_account" id="edit_id">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title font-weight-bold"><i class="mdi mdi-pencil mr-1 text-primary"></i> Edit Account</h6>
|
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
|
</div>
|
|
<div class="modal-body py-3">
|
|
<div class="form-group mb-2">
|
|
<label class="frm-lbl">Profile Photo (Optional)</label>
|
|
<div class="custom-file-upload form-control form-control-sm">
|
|
<input type="file" name="foto" id="edit_foto" class="d-none" accept="image/*" onchange="var txt=document.getElementById('fotoTextEdit'); 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="edit_foto" class="file-btn">Pilih File</label>
|
|
<span class="file-text" id="fotoTextEdit">Tidak ada file yang dipilih</span>
|
|
</div>
|
|
<small class="text-muted" style="font-size:10px;">Leave empty if you don't want to change the
|
|
photo.</small>
|
|
<div id="faceDetectPreviewEdit" style="display:none; margin-top:8px;">
|
|
<div id="faceDetectBadgeEdit"
|
|
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="faceVerifiedFlagEdit" value="1">
|
|
</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" id="edit_name"
|
|
class="form-control form-control-sm" required></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="form-group mb-2"><label class="frm-lbl">Username</label><input type="text" name="username" id="edit_username"
|
|
class="form-control form-control-sm" required></div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-6">
|
|
<div class="form-group mb-2">
|
|
<label class="frm-lbl">New Password (Optional)</label>
|
|
<div class="input-group">
|
|
<input type="password" name="password" id="edit_password" class="form-control form-control-sm"
|
|
placeholder="Leave empty if no change">
|
|
<div class="input-group-append">
|
|
<span class="input-group-text eye-toggle" data-target="edit_password"><i
|
|
class="mdi mdi-eye-off"></i></span>
|
|
</div>
|
|
</div>
|
|
<div class="progress mt-1" style="height: 5px; display: none;" id="pwStrengthMeterEdit">
|
|
<div class="progress-bar" id="pwStrengthBarEdit" role="progressbar" style="width: 0%;"></div>
|
|
</div>
|
|
<small id="pwStrengthTextEdit" 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 if changing.</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="form-group mb-2">
|
|
<label class="frm-lbl">Confirm New Password</label>
|
|
<div class="input-group">
|
|
<input type="password" id="edit_confirm_password" class="form-control form-control-sm"
|
|
placeholder="Repeat New Password">
|
|
<div class="input-group-append">
|
|
<span class="input-group-text eye-toggle" data-target="edit_confirm_password"><i
|
|
class="mdi mdi-eye-off"></i></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="editPasswordMismatch" class="text-danger mb-2"
|
|
style="display:none; font-size:12px; font-weight:600;"><i class="mdi mdi-alert-circle mr-1"></i>New
|
|
Passwords do not match!</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="btnEditAccount"><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 QR CODE ==================== -->
|
|
<div class="modal fade" id="modalFingerprint" tabindex="-1" data-backdrop="static" data-keyboard="false">
|
|
<div class="modal-dialog " style="max-width:380px;">
|
|
<div class="modal-content mdl-card">
|
|
<div class="modal-header py-2 bg-light">
|
|
<h6 class="modal-title font-weight-bold" style="font-size:14px;"><i
|
|
class="mdi mdi-qrcode-scan mr-1 text-primary"></i> Fingerprint Registration QR Code</h6>
|
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
|
</div>
|
|
<div class="modal-body text-center" style="padding: 20px;">
|
|
<input type="hidden" id="fingerprintAccountId">
|
|
|
|
<p style="font-size:11px; color:#666; line-height:1.4; margin-bottom:15px;">Scan the QR Code below to register
|
|
fingerprint via your 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:12px; background:#fff; border-radius:12px; border:2px solid #e0e7ff; box-shadow:0 4px 16px rgba(99,102,241,0.1); margin-bottom:15px;">
|
|
<div id="fingerprintQrCanvas"></div>
|
|
</div>
|
|
</div>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control form-control-sm text-center" 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-3" 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>
|
|
|
|
|
|
<!-- ==================== MODAL: VERIFY PIN ==================== -->
|
|
<div class="modal fade" id="modalVerifyPin" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content mdl-card">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title font-weight-bold"><i class="mdi mdi-lock mr-1 text-warning"></i> Verify PIN</h6>
|
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<i class="mdi mdi-shield-lock-outline" style="font-size:40px;color:#f59e0b;"></i>
|
|
<p class="mt-2 mb-3" style="font-size:12px;color:#666;">Enter 6-digit PIN to view the original password.</p>
|
|
<input type="hidden" id="verifyPinAccountId">
|
|
<input type="password" id="verifyPinInput" class="form-control text-center" maxlength="6"
|
|
placeholder="● ● ● ● ● ●" style="letter-spacing:8px;font-size:18px;font-weight:700;">
|
|
<div id="verifyPinError" class="text-danger mt-2" style="display:none;font-size:12px;"><i
|
|
class="mdi mdi-alert-circle mr-1"></i><span id="verifyPinErrorMsg"></span></div>
|
|
<button type="button" class="btn btn-primary btn-sm btn-block mt-3" id="btnVerifyPin"
|
|
onclick="submitPinVerify()"><i class="mdi mdi-check mr-1"></i>Verify</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== MODAL: SHOW PASSWORD ==================== -->
|
|
<div class="modal fade" id="modalShowPassword" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content mdl-card">
|
|
<div class="modal-header">
|
|
<h6 class="modal-title font-weight-bold"><i class="mdi mdi-key mr-1 text-success"></i> Original Password</h6>
|
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="pin-warning mb-2" style="background:#fff3cd;border-color:#ffc107;">
|
|
<i class="mdi mdi-alert" style="color:#d97706;"></i>
|
|
<div style="color:#92400e;font-size:11px;">Do not share this password with anyone!</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="text" id="revealedPassword" class="form-control" readonly
|
|
style="font-size:15px;font-weight:600;letter-spacing:1px;">
|
|
<div class="input-group-append"><span class="input-group-text" style="cursor:pointer;"
|
|
onclick="copyRevealed()"><i class="fas fa-copy" id="btnCopyRevealed" style="color:#6c757d;"></i></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== MODAL: PHOTO PREVIEW ==================== -->
|
|
<div class="modal fade" id="modalPhotoPreview" tabindex="-1" data-backdrop="static">
|
|
<div class="modal-dialog" style="max-width: 400px;">
|
|
<div class="modal-content" style="border-radius:12px; overflow:hidden;">
|
|
<div class="modal-header py-2" style="border-bottom: none; position:relative; background:#f8fafc;">
|
|
<h6 class="modal-title font-weight-bold" style="color:#1e293b;"><i class="mdi mdi-image-outline mr-1 text-primary"></i> Photo Preview</h6>
|
|
<button type="button" class="close" data-dismiss="modal" style="position:absolute; right:15px; top:10px; color:#64748b;">×</button>
|
|
</div>
|
|
<div class="modal-body text-center" style="padding:0 20px 20px 20px; background:#f8fafc;">
|
|
<img id="previewModalImage" src="" style="width:100%; max-height:350px; object-fit:cover; border-radius:8px; border:2px solid #e2e8f0; margin-bottom:15px; box-shadow:0 4px 12px rgba(0,0,0,0.05);">
|
|
<a id="btnDownloadPreview" href="#" download="Account_Photo.jpg" class="btn btn-sm btn-primary w-100" style="border-radius:8px; font-weight:bold; letter-spacing:0.5px; padding:10px;">
|
|
<i class="mdi mdi-download mr-1"></i>Download Photo
|
|
</a>
|
|
</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: #4b49ac; }
|
|
.ios-switch input:checked + .ios-slider:before { transform: translateX(14px); }
|
|
.btn-custom-dashboard {
|
|
background-color: #fff !important;
|
|
border: 1px solid #e5e9f0 !important;
|
|
color: #4b49ac !important;
|
|
}
|
|
|
|
.btn-custom-dashboard:hover,
|
|
.btn-custom-dashboard:focus,
|
|
.btn-custom-dashboard:active {
|
|
background-color: #f8f9fa !important;
|
|
border: 1px solid #e5e9f0 !important;
|
|
color: #4b49ac !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.dropdown-dots-btn {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.dropdown-dots-btn:hover,
|
|
.dropdown-dots-btn:focus,
|
|
.dropdown-dots-btn:active {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
outline: none !important;
|
|
}
|
|
|
|
.dropdown-dots-btn i {
|
|
font-size: 28px;
|
|
color: #495057;
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
|
|
.btn-custom-edit {
|
|
background: #e2e6ea;
|
|
border: none;
|
|
color: #212529;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.btn-custom-edit:hover,
|
|
.btn-custom-edit:focus {
|
|
background: #cbd3da;
|
|
color: #212529;
|
|
}
|
|
|
|
.btn-custom-dropdown:hover,
|
|
.btn-custom-dropdown:focus {
|
|
background-color: #ededed !important;
|
|
}
|
|
|
|
.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>
|
|
function copyFingerQrLink() {
|
|
var v = document.getElementById("fingerQrLinkText").value;
|
|
navigator.clipboard.writeText(v).then(function () { showCustomAlert('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;
|
|
$("#modalFingerprint").modal("show");
|
|
|
|
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" },
|
|
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" },
|
|
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...';
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function openPinVerifikasiModal(id) {
|
|
document.getElementById("verifyPinAccountId").value = id;
|
|
document.getElementById("verifyPinInput").value = '';
|
|
document.getElementById("verifyPinError").style.display = 'none';
|
|
$("#modalVerifyPin").modal("show");
|
|
}
|
|
|
|
function submitPinVerify() {
|
|
var id = document.getElementById("verifyPinAccountId").value;
|
|
var pin = document.getElementById("verifyPinInput").value;
|
|
var btn = document.getElementById("btnVerifyPin");
|
|
|
|
if (pin.length !== 6) {
|
|
document.getElementById("verifyPinErrorMsg").innerText = "PIN must be 6 digits";
|
|
document.getElementById("verifyPinError").style.display = "block";
|
|
return;
|
|
}
|
|
|
|
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Verifying...';
|
|
btn.disabled = true;
|
|
|
|
$.post("verify_pin_account.php", { id_account: id, pin: pin }, function (d) {
|
|
btn.innerHTML = '<i class="mdi mdi-check mr-1"></i>Verify';
|
|
btn.disabled = false;
|
|
|
|
if (d.status === "success") {
|
|
$("#modalVerifyPin").modal("hide");
|
|
document.getElementById("revealedPassword").value = d.password;
|
|
$("#modalShowPassword").modal("show");
|
|
} else {
|
|
document.getElementById("verifyPinErrorMsg").innerText = d.message || "Incorrect PIN!";
|
|
document.getElementById("verifyPinError").style.display = "block";
|
|
}
|
|
}, "json").fail(function () {
|
|
btn.innerHTML = '<i class="mdi mdi-check mr-1"></i>Verify';
|
|
btn.disabled = false;
|
|
document.getElementById("verifyPinErrorMsg").innerText = "Connection to server failed.";
|
|
document.getElementById("verifyPinError").style.display = "block";
|
|
});
|
|
}
|
|
|
|
function previewPhoto(url) {
|
|
document.getElementById('previewModalImage').src = url;
|
|
document.getElementById('btnDownloadPreview').href = url;
|
|
$('#modalPhotoPreview').modal('show');
|
|
}
|
|
|
|
function copyRevealed() {
|
|
var v = document.getElementById("revealedPassword").value;
|
|
var b = document.getElementById("btnCopyRevealed");
|
|
navigator.clipboard.writeText(v).then(function () {
|
|
b.classList.replace("fa-copy", "fa-check");
|
|
showCustomAlert('Password copied successfully!');
|
|
setTimeout(function () { b.classList.replace("fa-check", "fa-copy"); }, 2000);
|
|
});
|
|
}
|
|
|
|
function openEditModal(id, name, username) {
|
|
document.getElementById("edit_id").value = id;
|
|
document.getElementById("edit_name").value = name;
|
|
document.getElementById("edit_username").value = username;
|
|
$("#modalEdit").modal("show");
|
|
}
|
|
|
|
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 () {
|
|
// Close dropdown on scroll
|
|
window.addEventListener('scroll', function() {
|
|
if ($('.dropdown-menu.show').length > 0) {
|
|
$('[data-toggle="dropdown"]').attr('aria-expanded', 'false');
|
|
$('.dropdown-menu').removeClass('show');
|
|
$('.dropdown').removeClass('show');
|
|
}
|
|
}, true);
|
|
|
|
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-block');
|
|
$('.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");
|
|
});
|
|
|
|
$("#formEditAccount").submit(function (e) {
|
|
e.preventDefault();
|
|
|
|
// Check face if a new photo was uploaded
|
|
var editFoto = document.getElementById('edit_foto');
|
|
if (editFoto.files.length > 0 && document.getElementById('faceVerifiedFlagEdit').value !== '1') {
|
|
showCustomAlert('The uploaded photo does not have a detectable face. Please use a photo with a clear face.', 'warning');
|
|
return;
|
|
}
|
|
|
|
var pw1 = document.getElementById("edit_password").value;
|
|
var pw2 = document.getElementById("edit_confirm_password").value;
|
|
|
|
if (pw1 !== "") {
|
|
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("New password does not meet the criteria (min. 8 characters, uppercase, lowercase, numbers, and symbols)!", "warning");
|
|
document.getElementById("edit_password").focus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (pw1 !== pw2) {
|
|
document.getElementById("editPasswordMismatch").style.display = "block";
|
|
return;
|
|
} else {
|
|
document.getElementById("editPasswordMismatch").style.display = "none";
|
|
}
|
|
|
|
var btn = document.getElementById("btnEditAccount");
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Saving...';
|
|
|
|
fetch("edit_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") {
|
|
$("#modalEdit").modal("hide");
|
|
showCustomAlert("Account successfully updated.");
|
|
setTimeout(function () { location.reload(); }, 1000);
|
|
} else {
|
|
showCustomAlert(d.message || "Failed to save changes.");
|
|
}
|
|
}).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");
|
|
}
|
|
});
|
|
|
|
if ($.fn.DataTable.isDataTable('#myTable')) {
|
|
$('#myTable').DataTable().destroy();
|
|
}
|
|
var table = $('#myTable').DataTable({
|
|
"columnDefs": [{ "orderable": false, "targets": 4 }, { "width": "40px", "targets": 4 }],
|
|
"order": [[0, 'asc']]
|
|
});
|
|
|
|
$('#edit_password').on('input', function () {
|
|
var pass = $(this).val();
|
|
var meter = $('#pwStrengthMeterEdit');
|
|
var bar = $('#pwStrengthBarEdit');
|
|
var text = $('#pwStrengthTextEdit');
|
|
|
|
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('');
|
|
}
|
|
});
|
|
|
|
// Eye Toggle Logic
|
|
$('.eye-toggle').on('click', function () {
|
|
var targetId = $(this).data('target');
|
|
var input = $('#' + targetId);
|
|
var icon = $(this).find('i');
|
|
|
|
if (input.attr('type') === 'password') {
|
|
input.attr('type', 'text');
|
|
icon.removeClass('mdi-eye-off').addClass('mdi-eye');
|
|
} else {
|
|
input.attr('type', 'password');
|
|
icon.removeClass('mdi-eye').addClass('mdi-eye-off');
|
|
}
|
|
});
|
|
|
|
// ── Face Detection on Photo Upload (Edit Modal) ──
|
|
var faceModelLoadedEdit = false;
|
|
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function () {
|
|
faceModelLoadedEdit = true;
|
|
});
|
|
|
|
$('#edit_foto').on('change', function () {
|
|
var file = this.files[0];
|
|
if (!file) {
|
|
document.getElementById('faceVerifiedFlagEdit').value = '1';
|
|
document.getElementById('faceDetectPreviewEdit').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
var preview = document.getElementById('faceDetectPreviewEdit');
|
|
var badge = document.getElementById('faceDetectBadgeEdit');
|
|
|
|
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('faceVerifiedFlagEdit').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('faceVerifiedFlagEdit').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('faceVerifiedFlagEdit').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('faceVerifiedFlagEdit').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 (!faceModelLoadedEdit) {
|
|
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function () {
|
|
faceModelLoadedEdit = true;
|
|
doDetect();
|
|
});
|
|
} else {
|
|
doDetect();
|
|
}
|
|
};
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
// Reset face detection when edit modal closes
|
|
$('#modalEdit').on('hidden.bs.modal', function () {
|
|
document.getElementById('faceVerifiedFlagEdit').value = '1';
|
|
document.getElementById('faceDetectPreviewEdit').style.display = 'none';
|
|
document.getElementById('faceDetectBadgeEdit').style.display = 'none';
|
|
});
|
|
|
|
});
|
|
</script>
|