TKK_E32230273/access.php

807 lines
33 KiB
PHP

<?php
include 'header.php';
include 'koneksi.php';
?>
<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">Access Control</h3>
<h6 class="font-weight-normal mb-0">Manage occupant access and IoT door, <span class="text-primary">Manage access & door!</span></h6>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Tabel Occupant -->
<div class="col-md-8 grid-margin">
<div class="card" style="border: 1px solid #e2e8f0; border-radius: 10px;">
<div class="card-body">
<p class="card-title mb-0">Data Access Occupant</p>
<br>
<div class="table-responsive">
<table class="table table-borderless" id="myTable">
<thead>
<tr>
<th>No</th>
<th>Name</th>
<th>Access</th>
<th style="width:60px; text-align:center;">Action</th>
</tr>
</thead>
<?php
$stmt = $pdo->query("SELECT * FROM occupant WHERE status = 'approved' ORDER BY id_occupant ASC");
$occupants = $stmt->fetchAll();
?>
<tbody>
<?php
$no = 1;
foreach($occupants as $row):
$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; display:inline-flex; align-items:center;"><?= htmlspecialchars($row['name']); ?><?php
$isVerified = !empty($row['name']) && !empty($row['nik_nip']) && !empty($row['no_hp']) && !empty($row['pin']) && !empty($row['foto']) && !empty($row['finger_id']) && $row['finger_id'] !== '-' && !empty($row['code_register']) && $row['progress'] === 'used' && $row['status'] === 'approved' && $row['access'] === 'active';
if($isVerified): ?><i class="mdi mdi-check-decagram" style="color: #1da1f2; font-size: 16px; margin-left: 4px;" title="Verified Approved"></i><?php endif; ?></span></div></td>
<td style="vertical-align:middle;">
<select class="form-control form-control-sm access-dropdown" data-id="<?= $row['id_occupant']; ?>" style="border-radius: 6px; width: 120px; height: 35px; padding: 2px 10px; font-size: 13px;">
<option value="active" <?= ($row['access'] == 'active') ? 'selected' : ''; ?>>Active</option>
<option value="pending" <?= ($row['access'] == 'pending') ? 'selected' : ''; ?>>Pending</option>
<option value="suspend" <?= ($row['access'] == 'suspend') ? 'selected' : ''; ?>>Suspend</option>
</select>
</td>
<td style="vertical-align:middle; text-align:center;">
<button class="btn btn-sm btn-success text-white btn-confirm-access" data-id="<?= $row['id_occupant']; ?>" style="border-radius:6px; font-weight:bold; width:32px; padding: 4px 0; font-size:14px; display:inline-flex; align-items:center; justify-content:center;" disabled title="Click to save changes">
<i class="mdi mdi-check"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- IoT Door Control -->
<div class="col-md-4 grid-margin">
<!-- SMART LOCK SYSTEM CARD -->
<div class="card iot-card" style="border: 1px solid #e2e8f0; border-radius: 10px; background: linear-gradient(145deg, #ffffff, #ffffffff); position: relative; overflow: hidden;">
<!-- Decorative Background Elements -->
<div style="position: absolute; top: -30px; right: -30px; width: 100px; height: 100px; border-radius: 50%; background: #ffffffff; opacity: 0.05;"></div>
<div style="position: absolute; bottom: -20px; left: -20px; width: 80px; height: 80px; border-radius: 50%; background: #ffffffff; opacity: 0.05;"></div>
<div class="card-body d-flex flex-column align-items-center text-center py-4" style="z-index: 1;">
<div style="background: #4b49ac; border-radius: 20px; padding: 6px 16px; margin-bottom: 24px;">
<span style="color: #ffffffff; font-weight: 700; font-size: 11px; letter-spacing: 1px;">SMART LOCK SYSTEM</span>
</div>
<div class="door-illustration mb-3 mt-2" style="position: relative;">
<div class="door-frame" style="width: 120px; height: 150px; border: 4px solid #e2e8f0; border-bottom: none; border-radius: 8px 8px 0 0; margin: 0 auto; position: relative; background: #f1f5f9; display: flex; align-items: center; justify-content: center; overflow: hidden;">
<i class="mdi mdi-door-closed text-primary door-icon-main" id="doorIcon" style="font-size: 100px; color: #4b49ac !important; transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), color 0.3s; line-height: 1;"></i>
<div class="door-glow" id="doorGlow" style="position: absolute; bottom: 0; left: 0; right: 0; height: 4px; background: transparent; transition: background 0.3s;"></div>
</div>
<div style="width: 140px; height: 4px; background: #cbd5e1; margin: 0 auto; border-radius: 2px;"></div>
</div>
<p class="text-muted px-2" style="font-size: 13px; line-height: 1.4; margin-bottom: 15px;">Confirmation is required to unlock the IoT door access.</p>
<div class="info-alert mb-4 w-100" style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 12px; color: #1e293b; text-align: left;">
<div class="info-alert-icon" style="width: 24px; height: 24px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"><i class="mdi mdi-information-variant" style="color: #e0ecff; font-size: 16px; line-height: 1;"></i></div>
<div class="info-alert-text" style="flex: 1; line-height: 1.4;">
Please click the <strong>Unlock Door</strong> button below to open the FTM Room Door.
</div>
</div>
<button class="btn btn-primary btn-lg mt-auto w-100 unlock-btn" id="btnOpenDoor" style="border-radius: 10px; font-weight: 700; font-size: 14px; letter-spacing: 1px; padding: 12px; transition: all 0.2s; position: relative; overflow: hidden;">
<span class="btn-text d-flex align-items-center justify-content-center" style="position: relative; z-index: 2;">
<i class="mdi mdi-key mr-2" style="font-size: 20px;"></i> UNLOCK DOOR
</span>
<div class="btn-hover-effect" style="position: absolute; top: 0; left: -100%; width: 50%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); transform: skewX(-20deg); transition: 0.5s;"></div>
</button>
<div id="scanStatus" class="mt-3 text-sm font-weight-bold" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== MODAL: CONFIRM UNLOCK ==================== -->
<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-door-open mr-1 text-primary"></i> Confirm Unlock</h6>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body text-center">
<i class="mdi mdi-shield-check-outline" style="font-size:40px;color:#10b981;"></i>
<p class="mt-2 mb-3" style="font-size:13px;color:#666;">Are you sure you want to unlock the door?</p>
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-light btn-sm" data-dismiss="modal" style="width: 48%;">No</button>
<button type="button" class="btn btn-primary btn-sm" id="btnConfirmUnlock" style="width: 48%;"><i class="mdi mdi-check mr-1"></i>Yes</button>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== MODAL: SECURITY VERIFICATION ==================== -->
<div class="fp-modal-overlay" id="fpModal" style="display:none;">
<div class="fp-modal-content">
<!-- Countdown timer -->
<div class="fp-countdown" id="fpCountdown" style="display:none;">
<svg class="fp-countdown-svg" viewBox="0 0 36 36">
<circle class="fp-countdown-bg" cx="18" cy="18" r="15.5"/>
<circle class="fp-countdown-ring" id="fpCountdownRing" cx="18" cy="18" r="15.5"/>
</svg>
<span class="fp-countdown-num" id="fpCountdownNum">3</span>
</div>
<div class="fp-icon-wrapper" id="fpIconWrapper">
<i class="mdi mdi-fingerprint" id="fpIcon"></i>
</div>
<div class="fp-title" id="fpTitle">Unlock Door</div>
<div class="fp-text" id="fpStatusText">Touch your fingerprint sensor to unlock the door.</div>
<div id="fpAlert" style="display:none; padding:10px; border-radius:8px; margin-bottom:16px; font-size:13px; text-align:center; border:1px solid transparent;"></div>
<button class="fp-cancel-btn" type="button" id="fpCancelBtn">Cancel</button>
</div>
</div>
<!-- ==================== MODAL: DOOR UNLOCKED ==================== -->
<div class="fp-modal-overlay" id="doorModal" style="display:none;">
<div class="fp-modal-content">
<div class="fp-icon-wrapper fp-door-icon">
<div class="fp-door-circle">
<i class="mdi mdi-door-open"></i>
</div>
</div>
<div class="fp-title" style="color:#10b981;">Door Unlocked</div>
<div class="fp-text"><span id="doorWelcomeName"></span>The door has been successfully unlocked.<br>Remember to lock the door when you're done.</div>
<button class="fp-lock-btn" type="button" id="btnLockDoor"><i class="mdi mdi-lock"></i> Lock Door</button>
</div>
</div>
<!-- Content Wrapper Ends -->
<?php include 'footer.php'; ?>
<?php include 'copy.php'; ?>
<!-- Scripts -->
<script>
$(document).ready(function() {
// Initialize DataTable mimicking new_account.php
if ($.fn.DataTable.isDataTable('#myTable')) {
$('#myTable').DataTable().destroy();
}
$('#myTable').DataTable({ "order": [[0, 'asc']] });
// Track original values to enable/disable button
var originalAccessValues = {};
$('.access-dropdown').each(function() {
originalAccessValues[$(this).data('id')] = $(this).val();
});
// Handle Access Dropdown Change - Enable corresponding confirm button
$('#myTable').on('change', '.access-dropdown', function() {
var idOccupant = $(this).data('id');
var currentValue = $(this).val();
var confirmBtn = $('.btn-confirm-access[data-id="'+idOccupant+'"]');
if (currentValue !== originalAccessValues[idOccupant]) {
confirmBtn.prop('disabled', false).removeClass('btn-secondary').addClass('btn-success');
// Add a subtle pulse to attract attention
confirmBtn.addClass('pulse-btn');
} else {
confirmBtn.prop('disabled', true).removeClass('pulse-btn');
}
});
// Handle Confirm Access Button Click
$('#myTable').on('click', '.btn-confirm-access', function() {
var btn = $(this);
var idOccupant = btn.data('id');
var selectElement = $('.access-dropdown[data-id="'+idOccupant+'"]');
var accessValue = selectElement.val();
btn.prop('disabled', true).removeClass('pulse-btn').html('<i class="mdi mdi-loading mdi-spin"></i>');
selectElement.prop('disabled', true);
$.ajax({
url: 'update_access_occupant.php',
type: 'POST',
dataType: 'json',
data: {
id_occupant: idOccupant,
access: accessValue
},
success: function(response) {
selectElement.prop('disabled', false);
btn.html('<i class="mdi mdi-check"></i>');
if(response.status === 'success') {
originalAccessValues[idOccupant] = accessValue; // Update original value
showCustomAlert('Access successfully updated to ' + accessValue);
// Visual feedback on row
var row = selectElement.closest('tr');
row.css('background-color', '#ecfdf5');
setTimeout(function() {
row.css('background-color', '');
}, 1000);
} else {
showCustomAlert(response.message || 'Failed to update access.');
selectElement.val(originalAccessValues[idOccupant]); // Revert
btn.prop('disabled', true);
}
},
error: function() {
selectElement.prop('disabled', false);
selectElement.val(originalAccessValues[idOccupant]); // Revert
btn.prop('disabled', true).html('<i class="mdi mdi-check"></i>');
showCustomAlert('Connection failed!');
}
});
});
function setGlobalDoorLocked() {
$.post('api_door_status.php', { action: 'lock' }, function(d) {
if(d.status === 'success') {
showCustomAlert('Door locked successfully!');
closeDoorModal();
updateUIFromState('locked');
}
}, 'json');
}
function setGlobalDoorUnlocked(name) {
$.post('api_door_status.php', { action: 'unlock' }, function(d) {
if(d.status === 'success') {
// Close fingerprint modal and go straight to door modal
closeFpModal();
setTimeout(function() {
showDoorUnlockedModal(name);
updateUIFromState('unlocked');
}, 300);
}
}, 'json');
}
$('.unlock-btn').hover(
function() { if(!window.isDoorUnlocked) $(this).find('.btn-hover-effect').css('left', '100%'); },
function() { if(!window.isDoorUnlocked) { $(this).find('.btn-hover-effect').css({'left': '-100%', 'transition': '0s'}); setTimeout(() => {$(this).find('.btn-hover-effect').css('transition', '0.5s');}, 50); } }
);
// Handle main Door Button Click
$('#btnOpenDoor').on('click', function() {
if (window.isDoorUnlocked) {
// Door is already unlocked, clicking it forces a LOCK
setGlobalDoorLocked();
} else {
// Door is locked, proceed with unlock sequence
$('#modalVerifyPin').modal('show');
}
});
// Handle Confirm Unlock Yes Button
$('#btnConfirmUnlock').on('click', function() {
var btn = $(this);
var origHtml = btn.html();
btn.prop('disabled', true).html('<i class="mdi mdi-loading mdi-spin"></i> Unlocking...');
$('#modalVerifyPin').modal('hide');
btn.prop('disabled', false).html(origHtml);
setTimeout(function() {
setGlobalDoorUnlocked('<?= isset($_SESSION["name"]) ? addslashes($_SESSION["name"]) : "" ?>');
}, 300);
});
// === Fingerprint Modal Functions ===
var fpCountdownTimer = null;
function startCountdown(numEl, ringEl, containerEl, seconds, onComplete) {
var remaining = seconds;
$(containerEl).show();
$(numEl).text(remaining);
// Reset ring animation
var $ring = $(ringEl);
$ring.css({ 'animation': 'none', 'stroke-dashoffset': '0' });
// Force reflow
$ring[0].getBoundingClientRect();
$ring.css('animation', 'fpCountdownStroke ' + seconds + 's linear forwards');
var timer = setInterval(function() {
remaining--;
if (remaining <= 0) {
clearInterval(timer);
$(containerEl).hide();
if (onComplete) onComplete();
} else {
$(numEl).text(remaining);
}
}, 1000);
return timer;
}
function openFpModal() {
// Reset state
var $icon = $('#fpIcon');
$icon.removeClass('mdi-check-circle mdi-close-circle mdi-loading mdi-spin fp-success fp-error').addClass('mdi-fingerprint fp-scanning');
$icon.removeAttr('style');
$('#fpIconWrapper').removeClass('fp-result-success fp-result-error');
$('#fpTitle').text('Unlock Door').css('color', '#1f2937');
$('#fpStatusText').text('Touch your fingerprint sensor to unlock the door.').show();
$('#fpAlert').hide().html('');
$('#fpCancelBtn').show();
$('#fpCountdown').hide();
if (fpCountdownTimer) { clearInterval(fpCountdownTimer); fpCountdownTimer = null; }
// Show modal
var $modal = $('#fpModal');
$modal.css('display', 'flex').css('animation', 'fpFadeIn 0.2s ease-out forwards');
}
function closeFpModal() {
if (fpCountdownTimer) { clearInterval(fpCountdownTimer); fpCountdownTimer = null; }
var $modal = $('#fpModal');
$modal.css('animation', 'fpFadeOut 0.2s ease-out forwards');
setTimeout(function() { $modal.css('display', 'none'); }, 200);
}
function openDoorModal() {
var $modal = $('#doorModal');
$modal.css('display', 'flex').css('animation', 'fpFadeIn 0.2s ease-out forwards');
}
function closeDoorModal() {
var $modal = $('#doorModal');
$modal.css('animation', 'fpFadeOut 0.2s ease-out forwards');
setTimeout(function() { $modal.css('display', 'none'); }, 200);
}
// Cancel / close handlers
$('#fpCancelBtn').on('click', function() { closeFpModal(); });
function showFingerprintModal() {
openFpModal();
// Start WebAuthn after short delay
setTimeout(startWebAuthnVerification, 600);
}
// Switch to spin loader state
function showVerifyingState() {
var $icon = $('#fpIcon');
$icon.removeClass('mdi-fingerprint fp-scanning').addClass('mdi-loading mdi-spin');
// Reset gradient to solid color for spinner
$icon.css({
'background': 'none',
'-webkit-background-clip': 'unset',
'background-clip': 'unset',
'-webkit-text-fill-color': '#4B49AC',
'animation': 'none'
});
$icon.addClass('mdi-spin');
$('#fpStatusText').text('Verifying your identity to unlock the door...');
$('#fpCancelBtn').hide();
}
async function startWebAuthnVerification() {
if (!window.PublicKeyCredential) {
showScanResult('error', 'Not Supported', 'Your browser does not support biometric unlock.');
return;
}
try {
const challenge = new Uint8Array(32);
window.crypto.getRandomValues(challenge);
const publicKey = {
challenge: challenge,
rpId: window.location.hostname,
userVerification: "required",
timeout: 60000
};
const credential = await navigator.credentials.get({ publicKey });
if (!credential) {
throw new Error("No fingerprint detected");
}
const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));
// Switch to spin loader
showVerifyingState();
verifyFingerprintServer(rawIdBase64);
} catch (err) {
showScanResult('error', 'Cancelled', 'Door unlock was cancelled.');
}
}
function verifyFingerprintServer(credentialId) {
$.ajax({
url: 'verify_door_fingerprint.php',
type: 'POST',
dataType: 'json',
data: { credential_id: credentialId },
success: function(response) {
if(response.status === 'success') {
setGlobalDoorUnlocked(response.name);
} else {
showScanResult('error', 'Unlock Failed', response.message || 'You are not authorized to unlock this door.');
}
},
error: function() {
showScanResult('error', 'Connection Lost', 'Unable to reach the server. Please try again.');
}
});
}
function showScanResult(type, title, message) {
var $icon = $('#fpIcon');
$icon.removeClass('mdi-fingerprint mdi-loading mdi-spin fp-scanning').css({
'background': 'none',
'-webkit-background-clip': 'unset',
'background-clip': 'unset',
'animation': 'none'
});
$('#fpCancelBtn').hide();
// Only error state uses this modal now
$icon.addClass('mdi-close-circle fp-error');
$icon.css('-webkit-text-fill-color', '#ef4444');
$('#fpIconWrapper').addClass('fp-result-error');
$('#fpTitle').text(title).css('color', '#ef4444');
$('#fpStatusText').text(message);
// Start 3s countdown + auto close
fpCountdownTimer = startCountdown('#fpCountdownNum', '#fpCountdownRing', '#fpCountdown', 3, function() {
closeFpModal();
});
}
function showDoorUnlockedModal(name) {
if (name) {
$('#doorWelcomeName').text('Welcome, ' + name + '! ').show();
} else {
$('#doorWelcomeName').hide();
}
openDoorModal();
}
// Handle Lock Door Button Click from Modal
$(document).on('click', '#btnLockDoor', function() {
setGlobalDoorLocked();
});
// API Polling specific for Door State
window.isDoorUnlocked = false; // Initial presumed state
function updateUIFromState(state) {
if (state === 'unlocked') {
window.isDoorUnlocked = true;
// Change Button to Red "LOCK DOOR"
$('#btnOpenDoor').removeClass('btn-primary').addClass('btn-danger').css('box-shadow', 'none');
$('#btnOpenDoor .btn-text').html('<i class="mdi mdi-lock mr-2" style="font-size: 20px;"></i> LOCK DOOR');
// Animate Door Icon to Open state
$('#doorIcon').removeClass('mdi-door-closed').addClass('mdi-door-open');
// If we are showing the fingerprint modal by accident, hide it
if ($('#fpModal').is(':visible') && $('#scanStatus').text() === '') {
closeFpModal();
}
// Always show door modal when door is unlocked (persists on refresh)
if (!$('#doorModal').is(':visible')) {
openDoorModal();
}
} else {
window.isDoorUnlocked = false;
// Revert Button to Standard Blue "UNLOCK DOOR"
$('#btnOpenDoor').removeClass('btn-danger').addClass('btn-primary').css('box-shadow', '');
$('#btnOpenDoor .btn-text').html('<i class="mdi mdi-key mr-2" style="font-size: 20px;"></i> UNLOCK DOOR');
// Animate Door Icon Back
$('#doorIcon').removeClass('mdi-door-open').addClass('mdi-door-closed');
// Hide door modal if visible (door was locked externally)
if ($('#doorModal').is(':visible')) {
closeDoorModal();
}
}
}
function pollDoorStatus() {
$.get('api_door_status.php', function(data) {
if (data && data.status === 'success') {
updateUIFromState(data.door_state);
}
}, 'json');
}
// Poll every 2.5 seconds
setInterval(pollDoorStatus, 2500);
// Run once immediately on load
pollDoorStatus();
});
</script>
<style>
/* ── MANUAL STRIPES & BORDERS ── */
#myTable { border-spacing: 0 12px !important; border-collapse: separate !important; }
#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; padding-bottom: 8px !important; }
#myTable tbody tr td { border-bottom: none !important; padding-top: 6px !important; padding-bottom: 6px !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; }
/* Button Pulse Animation */
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
.pulse-btn {
animation: pulse-green 2s infinite;
}
/* ── Modal Styles (matching login.php) ── */
.fp-modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 10000;
}
.fp-modal-content {
background: #ffffff;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
border-radius: 12px;
padding: 30px 24px;
text-align: center;
color: #333;
max-width: 320px;
width: 90%;
animation: fpPopIn 0.2s ease-out forwards;
position: relative;
}
.fp-icon-wrapper {
position: relative;
width: 120px;
height: 120px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
}
/* Fingerprint scanning state — solid color + light sweep + pulse */
.fp-icon-wrapper i.fp-scanning {
font-size: 100px;
background: linear-gradient(180deg, #4B49AC 40%, rgba(255,255,255,0.5) 50%, #4B49AC 60%);
background-size: 100% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: fpSweep 2.5s linear infinite, fpPulse 2s infinite ease-in-out;
}
/* Spin loader state */
.fp-icon-wrapper i.mdi-loading {
font-size: 80px;
}
/* Success icon */
.fp-icon-wrapper i.fp-success {
font-size: 100px;
animation: fpSuccessPop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.fp-result-success {
animation: fpGlowGreen 0.5s ease-out forwards;
}
/* Error icon */
.fp-icon-wrapper i.fp-error {
font-size: 100px;
animation: fpShake 0.5s ease forwards;
}
.fp-result-error {
animation: fpGlowRed 0.5s ease-out forwards;
}
.fp-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
color: #1f2937;
}
.fp-text {
font-size: 14px;
color: #6b7280;
margin-bottom: 20px;
line-height: 1.5;
}
.fp-cancel-btn {
background: #f3f4f6;
border: 1px solid #e5e7eb;
color: #4b5563;
padding: 8px 24px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.fp-cancel-btn:hover {
background: #e5e7eb;
color: #1f2937;
}
/* Door icon style */
.fp-door-icon {
margin-bottom: 24px !important;
}
.fp-door-circle {
width: 90px;
height: 90px;
border-radius: 50%;
background: #10b981;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
animation: fpSuccessPop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.fp-door-circle i {
font-size: 44px;
color: #fff;
}
.fp-lock-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 10px;
background: #ef4444;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.fp-lock-btn:hover {
background: #dc2626;
}
/* ── Countdown Timer ── */
.fp-countdown {
position: absolute;
top: 12px;
right: 14px;
width: 32px;
height: 32px;
z-index: 10;
}
.fp-countdown-svg {
width: 32px;
height: 32px;
transform: rotate(-90deg);
}
.fp-countdown-bg {
fill: none;
stroke: #e5e7eb;
stroke-width: 2.5;
}
.fp-countdown-ring {
fill: none;
stroke: #4B49AC;
stroke-width: 2.5;
stroke-dasharray: 97.4;
stroke-dashoffset: 0;
stroke-linecap: round;
}
.fp-countdown-num {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 700;
color: #4B49AC;
line-height: 1;
}
@keyframes fpCountdownStroke {
from { stroke-dashoffset: 0; }
to { stroke-dashoffset: 97.4; }
}
/* ── Keyframes ── */
@keyframes fpFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fpFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fpPopIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes fpSweep {
0% { background-position: 0 120%; }
100% { background-position: 0 -20%; }
}
@keyframes fpPulse {
0% { transform: scale(0.95); opacity: 0.8; }
50% { transform: scale(1.05); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.8; }
}
@keyframes fpSuccessPop {
0% { transform: scale(0.3); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fpShake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
@keyframes fpGlowGreen {
0% { background: transparent; }
50% { background: radial-gradient(circle, rgba(16,185,129,0.1) 0%, transparent 70%); }
100% { background: transparent; }
}
@keyframes fpGlowRed {
0% { background: transparent; }
50% { background: radial-gradient(circle, rgba(239,68,68,0.08) 0%, transparent 70%); }
100% { background: transparent; }
}
</style>
</div>