807 lines
33 KiB
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">×</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>
|