729 lines
29 KiB
PHP
729 lines
29 KiB
PHP
<?php
|
|
$occ_id = isset($_GET['occ_id']) ? $_GET['occ_id'] : '';
|
|
$token = isset($_GET['token']) ? $_GET['token'] : '';
|
|
|
|
if (empty($occ_id) || empty($token)) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$usedTokensFile = __DIR__ . '/used_report_tokens.json';
|
|
$usedTokens = [];
|
|
if (file_exists($usedTokensFile)) {
|
|
$usedTokens = json_decode(file_get_contents($usedTokensFile), true) ?: [];
|
|
}
|
|
|
|
if (isset($usedTokens[$token])) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
|
<title>Activity Report — Identia</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
|
|
<style>
|
|
:root {
|
|
--primary: #4747a1;
|
|
--primary-light: #e6e6f2;
|
|
--primary-glow: rgba(71,71,161,0.3);
|
|
--success: #10b981;
|
|
--danger: #ef4444;
|
|
--warning: #f59e0b;
|
|
--bg: #f8fafc;
|
|
--card: #ffffff;
|
|
--text: #0f172a;
|
|
--muted: #64748b;
|
|
--border: #e2e8f0;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
body {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
padding: 20px 16px 40px;
|
|
}
|
|
|
|
|
|
|
|
/* ── MAIN CARD ── */
|
|
.card {
|
|
background: var(--card);
|
|
border-radius: 20px;
|
|
width: 100%; max-width: 460px;
|
|
padding: 32px 28px 36px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
/* ── HEADER ── */
|
|
.card-header {
|
|
display: flex; align-items: center; gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.header-icon {
|
|
width: 52px; height: 52px; border-radius: 16px;
|
|
background: var(--primary-light);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.header-icon i { font-size: 26px; color: var(--primary); }
|
|
.header-text h1 { font-size: 20px; font-weight: 800; color: var(--text); letter-spacing: -0.3px; }
|
|
.header-text p { font-size: 12px; color: var(--muted); margin-top: 3px; font-weight: 500; }
|
|
|
|
/* ── SECTION LABEL ── */
|
|
.section-label {
|
|
font-size: 11px; font-weight: 700; color: var(--primary);
|
|
text-transform: uppercase; letter-spacing: 1px;
|
|
margin-bottom: 10px; display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.section-label::after { content: ''; flex: 1; height: 1px; background: var(--primary-light); }
|
|
|
|
/* ── CAMERA AREA ── */
|
|
.cam-wrapper {
|
|
width: 100%; position: relative;
|
|
border-radius: 16px; overflow: hidden;
|
|
background: #1a1e20;
|
|
aspect-ratio: 3/4;
|
|
margin-bottom: 8px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
#cam-video {
|
|
width: 100%; height: 100%;
|
|
object-fit: cover; transform: scaleX(-1);
|
|
display: block;
|
|
}
|
|
#cam-canvas { display: none; }
|
|
#cam-photo {
|
|
display: none; width: 100%; height: 100%;
|
|
object-fit: cover; position: absolute; inset: 0; z-index: 3;
|
|
}
|
|
|
|
/* Scan Line — moves up & down */
|
|
#scan-line {
|
|
position: absolute; top: 0; left: 0;
|
|
width: 100%; height: 3px;
|
|
background: #ffffff;
|
|
box-shadow: 0 0 10px #ffffff, 0 0 20px rgba(255,255,255,0.5);
|
|
z-index: 4;
|
|
animation: scanMove 2.8s ease-in-out infinite;
|
|
transition: background 0.4s ease, box-shadow 0.4s ease;
|
|
}
|
|
#scan-line.face-ok {
|
|
background: #a855f7;
|
|
box-shadow: 0 0 10px #a855f7, 0 0 24px rgba(168,85,247,0.4);
|
|
}
|
|
@keyframes scanMove {
|
|
0% { top: 0%; }
|
|
50% { top: calc(100% - 3px); }
|
|
100% { top: 0%; }
|
|
}
|
|
|
|
/* Cam loading overlay */
|
|
#cam-loading {
|
|
position: absolute; inset: 0; background: #ffffff;
|
|
display: flex; flex-direction: column; align-items: center;
|
|
justify-content: center; gap: 16px; z-index: 10;
|
|
}
|
|
.ld-ring {
|
|
width: 44px; height: 44px; border-radius: 50%;
|
|
border: 3px solid var(--primary-light); border-top-color: var(--primary);
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
#cam-loading p { font-size: 13px; color: var(--muted); font-weight: 600; }
|
|
|
|
/* Status pill */
|
|
#face-pill {
|
|
position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
|
|
display: none; align-items: center; gap: 8px;
|
|
padding: 8px 18px; border-radius: 24px;
|
|
font-size: 12px; font-weight: 700; white-space: nowrap; z-index: 5;
|
|
background: rgba(255,255,255,0.9); color: var(--text);
|
|
backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.5);
|
|
transition: all 0.3s;
|
|
}
|
|
#face-pill.detected { background: #a855f7; color: #fff; border-color: #a855f7; }
|
|
#face-pill.detected i { color: #fff; }
|
|
#face-pill.warn { background: var(--danger); color: #fff; border-color: var(--danger); }
|
|
#face-pill i { font-size: 16px; }
|
|
|
|
/* Live metadata at bottom-left corner of camera */
|
|
#live-meta {
|
|
position: absolute; bottom: 0; left: 0; z-index: 5;
|
|
padding: 10px 12px;
|
|
background: linear-gradient(to top right, rgba(0,0,0,0.7) 0%, transparent 100%);
|
|
display: flex; flex-direction: column; gap: 2px;
|
|
pointer-events: none;
|
|
text-align: left;
|
|
border-bottom-left-radius: 12px;
|
|
}
|
|
#live-meta span {
|
|
font-size: 10px; color: rgba(255,255,255,0.95); font-weight: 600;
|
|
text-shadow: 0 1px 3px rgba(0,0,0,0.8); letter-spacing: 0.1px;
|
|
}
|
|
|
|
/* Capture / retake controls */
|
|
#cam-controls {
|
|
position: absolute; bottom: 20px; left: 0; width: 100%;
|
|
display: flex; justify-content: center; z-index: 6;
|
|
}
|
|
.btn-shutter {
|
|
width: 68px; height: 68px; border-radius: 50%;
|
|
border: 4px solid rgba(255,255,255,0.5);
|
|
background: rgba(255,255,255,0.15);
|
|
display: flex; align-items: center; justify-content: center;
|
|
cursor: pointer; transition: all 0.3s;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.btn-shutter::after {
|
|
content: ''; width: 50px; height: 50px; border-radius: 50%;
|
|
background: #fff; transition: all 0.2s;
|
|
}
|
|
.btn-shutter:hover::after { transform: scale(0.95); }
|
|
.btn-shutter:active::after { transform: scale(0.8); background: var(--primary); }
|
|
.btn-shutter:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
|
|
|
|
.btn-retake {
|
|
display: none; position: absolute; bottom: 20px; left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(239,68,68,0.9); color: #fff;
|
|
border: none; border-radius: 24px; padding: 10px 28px;
|
|
font-size: 13px; font-weight: 700; cursor: pointer;
|
|
font-family: 'Inter', sans-serif; z-index: 6;
|
|
backdrop-filter: blur(4px);
|
|
transition: background 0.2s;
|
|
}
|
|
.btn-retake i { margin-right: 6px; }
|
|
.btn-retake:hover { background: rgba(220,38,38,0.95); }
|
|
|
|
/* Hint below camera */
|
|
.cam-hint {
|
|
font-size: 11px; color: var(--muted); text-align: center;
|
|
font-weight: 500; margin-top: 6px; margin-bottom: 20px;
|
|
}
|
|
|
|
/* ── FORM FIELDS ── */
|
|
.form-group { margin-bottom: 20px; }
|
|
.form-group label {
|
|
display: block; font-size: 12px; font-weight: 700;
|
|
color: var(--text); margin-bottom: 8px;
|
|
}
|
|
.form-group label span { color: var(--danger); }
|
|
|
|
.input-wrap { position: relative; }
|
|
.input-wrap i {
|
|
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
|
|
color: var(--muted); font-size: 18px;
|
|
}
|
|
textarea {
|
|
width: 100%; padding: 12px 14px 12px 42px;
|
|
background: #f8fafc; border: 1.5px solid var(--border);
|
|
border-radius: 12px; color: var(--text);
|
|
font-size: 14px; font-family: 'Inter', sans-serif;
|
|
font-weight: 500; transition: all 0.2s; resize: vertical;
|
|
min-height: 90px; line-height: 1.6;
|
|
}
|
|
textarea:focus {
|
|
outline: none; border-color: var(--primary);
|
|
background: #fff; box-shadow: 0 0 0 3px var(--primary-glow);
|
|
}
|
|
textarea::placeholder { color: #94a3b8; }
|
|
|
|
|
|
|
|
/* ── SUBMIT BTN ── */
|
|
.btn-submit {
|
|
width: 100%; padding: 16px;
|
|
background: var(--primary); color: #fff;
|
|
border: none; border-radius: 14px;
|
|
font-size: 15px; font-weight: 700;
|
|
cursor: pointer; font-family: 'Inter', sans-serif;
|
|
display: flex; align-items: center; justify-content: center; gap: 10px;
|
|
transition: all 0.2s;
|
|
margin-top: 8px;
|
|
}
|
|
.btn-submit:hover { transform: translateY(-2px); }
|
|
.btn-submit:active { transform: translateY(0); }
|
|
.btn-submit:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
|
|
|
/* ── CUSTOM ALERT ── */
|
|
.custom-alert {
|
|
position: fixed; top: 20px; right: 20px;
|
|
background: #fff; border-left: 4px solid #fcd34d;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
padding: 14px 18px; border-radius: 8px; z-index: 99999;
|
|
display: none; align-items: center; gap: 12px;
|
|
min-width: 280px; max-width: 380px;
|
|
animation: slideIn 0.3s ease-out;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
.custom-alert.error { border-left-color: var(--danger); }
|
|
.custom-alert.success { border-left-color: var(--success); }
|
|
@keyframes slideIn { from { transform: translateX(110%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
.ca-icon { font-size: 22px; }
|
|
.custom-alert.error .ca-icon { color: var(--danger); }
|
|
.custom-alert.success .ca-icon { color: var(--success); }
|
|
.custom-alert.warning .ca-icon { color: var(--warning); }
|
|
.ca-content { flex: 1; }
|
|
.ca-title { font-weight: 700; font-size: 13px; color: #1e293b; }
|
|
.ca-msg { font-size: 12px; color: #64748b; margin-top: 2px; line-height: 1.4; }
|
|
.ca-close { cursor: pointer; color: #94a3b8; font-size: 16px; }
|
|
.ca-close:hover { color: #475569; }
|
|
|
|
@media (max-width: 480px) {
|
|
body { padding: 12px 10px 32px; }
|
|
.card { padding: 24px 18px 28px; border-radius: 16px; }
|
|
.custom-alert { top: 10px; right: 10px; left: 10px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
|
|
|
|
<!-- Main Form -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="header-icon"><i class="mdi mdi-clipboard-text"></i></div>
|
|
<div class="header-text">
|
|
<h1>Activity Report</h1>
|
|
<p>Face verification is required</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-label"><i class="mdi mdi-camera-account"></i> Face Verification</div>
|
|
|
|
<!-- Camera Wrapper -->
|
|
<div class="cam-wrapper" id="cam-wrapper">
|
|
<!-- Loading -->
|
|
<div id="cam-loading">
|
|
<div class="ld-ring"></div>
|
|
<p id="cam-load-text">Initializing camera...</p>
|
|
</div>
|
|
|
|
<!-- Scan Line -->
|
|
<div id="scan-line"></div>
|
|
|
|
<!-- Video feed -->
|
|
<video id="cam-video" autoplay playsinline muted></video>
|
|
|
|
<!-- Canvas (hidden, used for capture) -->
|
|
<canvas id="cam-canvas"></canvas>
|
|
|
|
<!-- Face status pill -->
|
|
<div id="face-pill">
|
|
<i class="mdi mdi-face-recognition" id="face-pill-icon"></i>
|
|
<span id="face-pill-text">Searching for face...</span>
|
|
</div>
|
|
|
|
<!-- Live metadata overlay (hidden during capture) -->
|
|
<div id="live-meta">
|
|
<span id="meta-time">--</span>
|
|
</div>
|
|
|
|
<!-- Captured photo -->
|
|
<img id="cam-photo" alt="Captured Photo">
|
|
|
|
<!-- Controls -->
|
|
<div id="cam-controls">
|
|
<button class="btn-shutter" id="btn-shutter" disabled title="Take Photo"></button>
|
|
</div>
|
|
<button class="btn-retake" id="btn-retake"><i class="mdi mdi-refresh"></i>Retake</button>
|
|
</div>
|
|
<div class="cam-hint" id="cam-hint"><i class="mdi mdi-information-outline"></i> Position your face and wait for detection before capturing</div>
|
|
|
|
|
|
|
|
<form id="reportForm" action="#" method="POST">
|
|
<input type="hidden" name="photo_data" id="photo_data">
|
|
|
|
|
|
<div class="section-label"><i class="mdi mdi-text-box-edit"></i> Activity Details</div>
|
|
|
|
<div class="form-group">
|
|
<label for="activity">Activity Description <span>*</span></label>
|
|
<div class="input-wrap">
|
|
<i class="mdi mdi-pencil-outline"></i>
|
|
<textarea id="activity" name="activity" rows="3"
|
|
placeholder="Describe your current activity (e.g., AC Maintenance, Safety Inspection)..." required></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-submit" id="btn-submit">
|
|
<i class="mdi mdi-send"></i> Submit Report
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Custom Alert -->
|
|
<div class="custom-alert" id="customAlert">
|
|
<i class="mdi mdi-alert-circle ca-icon" id="ca-icon"></i>
|
|
<div class="ca-content">
|
|
<div class="ca-title" id="ca-title">Notice</div>
|
|
<div class="ca-msg" id="ca-msg"></div>
|
|
</div>
|
|
<i class="mdi mdi-close ca-close" onclick="closeAlert()"></i>
|
|
</div>
|
|
|
|
<script src="js/face-api/face-api.min.js"></script>
|
|
<script>
|
|
(function() {
|
|
// ── DOM refs ──
|
|
|
|
const camLoading = document.getElementById('cam-loading');
|
|
const camLoadText = document.getElementById('cam-load-text');
|
|
const camVideo = document.getElementById('cam-video');
|
|
const camCanvas = document.getElementById('cam-canvas');
|
|
const camPhoto = document.getElementById('cam-photo');
|
|
const scanLine = document.getElementById('scan-line');
|
|
const facePill = document.getElementById('face-pill');
|
|
const facePillIcon = document.getElementById('face-pill-icon');
|
|
const facePillText = document.getElementById('face-pill-text');
|
|
const liveMeta = document.getElementById('live-meta');
|
|
const metaTime = document.getElementById('meta-time');
|
|
|
|
const camControls = document.getElementById('cam-controls');
|
|
const btnShutter = document.getElementById('btn-shutter');
|
|
const btnRetake = document.getElementById('btn-retake');
|
|
const camHint = document.getElementById('cam-hint');
|
|
const photoDataInput= document.getElementById('photo_data');
|
|
|
|
// ── State ──
|
|
let stream = null;
|
|
let detectLoop = null;
|
|
let faceDetected = false;
|
|
|
|
let modelsLoaded = false;
|
|
let timeInterval = null;
|
|
|
|
// ── Custom Alert ──
|
|
function showAlert(title, msg, type = 'warning') {
|
|
const box = document.getElementById('customAlert');
|
|
const icon = document.getElementById('ca-icon');
|
|
box.className = 'custom-alert ' + type;
|
|
if (type === 'error') icon.className = 'mdi mdi-close-circle ca-icon';
|
|
else if (type === 'success') icon.className = 'mdi mdi-check-circle ca-icon';
|
|
else icon.className = 'mdi mdi-alert-circle ca-icon';
|
|
document.getElementById('ca-title').textContent = title;
|
|
document.getElementById('ca-msg').textContent = msg;
|
|
box.style.display = 'flex';
|
|
setTimeout(closeAlert, 5000);
|
|
}
|
|
function closeAlert() {
|
|
const box = document.getElementById('customAlert');
|
|
box.style.animation = 'fadeOut 0.3s ease-out forwards';
|
|
setTimeout(() => { box.style.display = 'none'; box.style.animation = ''; }, 300);
|
|
}
|
|
window.closeAlert = closeAlert;
|
|
|
|
// ── Time ticker ──
|
|
function updateTime() {
|
|
const now = new Date();
|
|
const opts = { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };
|
|
metaTime.textContent = '🕐 ' + now.toLocaleDateString('en-US', opts);
|
|
}
|
|
updateTime();
|
|
timeInterval = setInterval(updateTime, 1000);
|
|
|
|
// Start camera immediately
|
|
initCamera();
|
|
|
|
// ── Init Camera ──
|
|
async function initCamera() {
|
|
camLoading.style.display = 'flex';
|
|
camLoadText.textContent = 'Loading face detection models...';
|
|
|
|
try {
|
|
if (!modelsLoaded) {
|
|
await Promise.all([
|
|
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),
|
|
faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models')
|
|
]);
|
|
modelsLoaded = true;
|
|
}
|
|
camLoadText.textContent = 'Starting camera...';
|
|
|
|
const constraints = {
|
|
video: { facingMode: 'user', width: { ideal: 720 }, height: { ideal: 960 } },
|
|
audio: false
|
|
};
|
|
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
camVideo.srcObject = stream;
|
|
await new Promise((resolve) => {
|
|
camVideo.onloadedmetadata = () => camVideo.play().then(resolve).catch(resolve);
|
|
setTimeout(resolve, 8000);
|
|
});
|
|
|
|
camLoading.style.display = 'none';
|
|
facePill.style.display = 'flex';
|
|
startDetection();
|
|
|
|
} catch (err) {
|
|
camLoading.style.display = 'none';
|
|
let msg = 'Camera failed to start.';
|
|
if (err.name === 'NotAllowedError') msg = 'Camera permission denied. Please allow camera access.';
|
|
else if (err.name === 'NotFoundError') msg = 'No camera found on this device.';
|
|
showAlert('Camera Error', msg, 'error');
|
|
}
|
|
}
|
|
|
|
// ── Face Detection ──
|
|
function startDetection() {
|
|
const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.45 });
|
|
detectLoop = setInterval(async () => {
|
|
if (!camVideo.videoWidth || camVideo.paused) return;
|
|
const results = await faceapi.detectAllFaces(camVideo, opts).run();
|
|
|
|
if (results.length === 1) {
|
|
const box = results[0].box;
|
|
const vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
|
const area = (box.width * box.height) / (vw * vh);
|
|
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
|
const dx = Math.abs(cx - vw / 2) / vw, dy = Math.abs(cy - vh / 2) / vh;
|
|
|
|
if (area < 0.04) {
|
|
setFacePill('warn', 'mdi-arrow-up-bold', 'Move closer');
|
|
setFaceState(false);
|
|
} else if (area > 0.65) {
|
|
setFacePill('warn', 'mdi-arrow-down-bold', 'Move back');
|
|
setFaceState(false);
|
|
} else if (dx > 0.22 || dy > 0.22) {
|
|
setFacePill('warn', 'mdi-arrow-all', 'Center your face');
|
|
setFaceState(false);
|
|
} else {
|
|
setFacePill('detected', 'mdi-check-circle', 'Face Detected — Ready');
|
|
setFaceState(true);
|
|
}
|
|
} else if (results.length > 1) {
|
|
setFacePill('warn', 'mdi-account-multiple', 'Only 1 face allowed');
|
|
setFaceState(false);
|
|
} else {
|
|
setFacePill(null, 'mdi-face-recognition', 'Searching for face...');
|
|
setFaceState(false);
|
|
}
|
|
}, 350);
|
|
}
|
|
|
|
function setFacePill(type, icon, text) {
|
|
facePill.className = 'face-pill';
|
|
if (type) facePill.classList.add(type);
|
|
facePillIcon.className = 'mdi ' + icon;
|
|
facePillText.textContent = text;
|
|
// Also style the id-based pill
|
|
facePill.id = 'face-pill';
|
|
if (type === 'detected') {
|
|
facePill.style.cssText = 'display:flex; position:absolute; top:16px; left:50%; transform:translateX(-50%); align-items:center; gap:8px; padding:8px 18px; border-radius:24px; font-size:12px; font-weight:700; white-space:nowrap; z-index:5; background:#a855f7; color:#fff; backdrop-filter:blur(8px); border:1px solid #a855f7; transition:all 0.3s;';
|
|
} else if (type === 'warn') {
|
|
facePill.style.cssText = 'display:flex; position:absolute; top:16px; left:50%; transform:translateX(-50%); align-items:center; gap:8px; padding:8px 18px; border-radius:24px; font-size:12px; font-weight:700; white-space:nowrap; z-index:5; background:var(--danger); color:#fff; backdrop-filter:blur(8px); border:1px solid var(--danger); transition:all 0.3s;';
|
|
} else {
|
|
facePill.style.cssText = 'display:flex; position:absolute; top:16px; left:50%; transform:translateX(-50%); align-items:center; gap:8px; padding:8px 18px; border-radius:24px; font-size:12px; font-weight:700; white-space:nowrap; z-index:5; background:rgba(255,255,255,0.9); color:#0f172a; backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.5); transition:all 0.3s;';
|
|
}
|
|
}
|
|
|
|
function setFaceState(ok) {
|
|
faceDetected = ok;
|
|
if (ok) {
|
|
scanLine.classList.add('face-ok');
|
|
btnShutter.disabled = false;
|
|
} else {
|
|
scanLine.classList.remove('face-ok');
|
|
btnShutter.disabled = true;
|
|
}
|
|
}
|
|
|
|
// ── Capture ──
|
|
btnShutter.addEventListener('click', () => {
|
|
if (!faceDetected) return;
|
|
clearInterval(detectLoop);
|
|
|
|
// Hide alert pill immediately on capture
|
|
facePill.style.display = 'none';
|
|
|
|
const vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
|
camCanvas.width = vw; camCanvas.height = vh;
|
|
const ctx = camCanvas.getContext('2d');
|
|
|
|
// Mirror flip (match camera display)
|
|
ctx.save();
|
|
ctx.translate(vw, 0);
|
|
ctx.scale(-1, 1);
|
|
ctx.drawImage(camVideo, 0, 0, vw, vh);
|
|
ctx.restore();
|
|
|
|
// Overlay: dark gradient at bottom
|
|
const gradH = Math.max(120, Math.floor(vh * 0.22));
|
|
const grad = ctx.createLinearGradient(0, vh - gradH, 0, vh);
|
|
grad.addColorStop(0, 'rgba(0,0,0,0)');
|
|
grad.addColorStop(1, 'rgba(0,0,0,0.75)');
|
|
ctx.fillStyle = grad;
|
|
ctx.fillRect(0, vh - gradH, vw, gradH);
|
|
|
|
// Overlay: date/time + location text
|
|
const now = new Date();
|
|
const dateStr = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
|
|
const baseFontSize = Math.max(12, Math.floor(vw * 0.030)); // slightly smaller
|
|
const smallFont = Math.max(10, Math.floor(vw * 0.024));
|
|
|
|
// Calculate total text block height to bottom-align
|
|
const textBlockHeight = baseFontSize + 4;
|
|
const startY = vh - textBlockHeight - 16; // 16px bottom padding
|
|
|
|
ctx.textBaseline = 'alphabetic';
|
|
|
|
// Draw time
|
|
ctx.font = `bold ${baseFontSize}px Inter, Arial, sans-serif`;
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillText(`${dateStr} ${timeStr}`, 26, startY);
|
|
|
|
const dataURL = camCanvas.toDataURL('image/jpeg', 0.88);
|
|
camPhoto.src = dataURL;
|
|
camPhoto.style.display = 'block';
|
|
photoDataInput.value = dataURL;
|
|
|
|
// UI updates
|
|
camVideo.style.display = 'none';
|
|
scanLine.style.display = 'none';
|
|
liveMeta.style.display = 'none';
|
|
camControls.style.display = 'none';
|
|
facePill.style.display = 'none';
|
|
btnRetake.style.display = 'block';
|
|
camHint.innerHTML = '<i class="mdi mdi-check-circle" style="color:#10b981;"></i> Photo captured! Review and retake if needed.';
|
|
|
|
if (stream) stream.getTracks().forEach(t => t.stop());
|
|
});
|
|
|
|
function wrapText(ctx, text, maxWidth, lineWidth) {
|
|
const words = text.split(' ');
|
|
const lines = [];
|
|
let current = '';
|
|
for (const w of words) {
|
|
const test = current ? current + ' ' + w : w;
|
|
if (ctx.measureText(test).width > lineWidth && current) {
|
|
lines.push(current);
|
|
current = w;
|
|
} else {
|
|
current = test;
|
|
}
|
|
}
|
|
if (current) lines.push(current);
|
|
return lines;
|
|
}
|
|
|
|
// ── Retake ──
|
|
btnRetake.addEventListener('click', async () => {
|
|
camPhoto.style.display = 'none';
|
|
camPhoto.src = '';
|
|
photoDataInput.value = '';
|
|
btnRetake.style.display = 'none';
|
|
camControls.style.display = 'flex';
|
|
scanLine.style.display = 'block';
|
|
liveMeta.style.display = 'flex';
|
|
facePill.style.display = 'flex';
|
|
camHint.innerHTML = '<i class="mdi mdi-information-outline"></i> Position your face and wait for detection before capturing';
|
|
faceDetected = false;
|
|
scanLine.classList.remove('face-ok');
|
|
btnShutter.disabled = true;
|
|
|
|
// Restart camera
|
|
camLoading.style.display = 'flex';
|
|
camLoadText.textContent = 'Restarting camera...';
|
|
camVideo.style.display = 'block';
|
|
try {
|
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: 'user', width: { ideal: 720 }, height: { ideal: 960 } },
|
|
audio: false
|
|
});
|
|
camVideo.srcObject = stream;
|
|
await new Promise(resolve => {
|
|
camVideo.onloadedmetadata = () => camVideo.play().then(resolve).catch(resolve);
|
|
setTimeout(resolve, 6000);
|
|
});
|
|
camLoading.style.display = 'none';
|
|
startDetection();
|
|
} catch (e) {
|
|
camLoading.style.display = 'none';
|
|
showAlert('Camera Error', 'Failed to restart camera.', 'error');
|
|
}
|
|
});
|
|
|
|
// ── Submit ──
|
|
document.getElementById('reportForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
if (!photoDataInput.value) {
|
|
showAlert('Photo Required', 'Please capture a photo before submitting.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const activity = document.getElementById('activity').value.trim();
|
|
if (!activity) {
|
|
showAlert('Activity Required', 'Please describe your activity.', 'warning');
|
|
return;
|
|
}
|
|
// If all good — submit
|
|
const btnSubmit = document.getElementById('btn-submit');
|
|
btnSubmit.disabled = true;
|
|
btnSubmit.innerHTML = '<i class="mdi mdi-loading mdi-spin"></i> Submitting...';
|
|
|
|
const fd = new FormData();
|
|
fd.append('activity', activity);
|
|
fd.append('fotoData', photoDataInput.value);
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has('occ_id')) {
|
|
fd.append('occ_id', urlParams.get('occ_id'));
|
|
}
|
|
if (urlParams.has('token')) {
|
|
fd.append('token', urlParams.get('token'));
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('save_report.php', { method: 'POST', body: fd });
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
const overlay = document.createElement('div');
|
|
overlay.style.cssText = 'position:fixed; inset:0; z-index:9999; background:#ffffff; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; animation:fadeIn 0.3s ease; padding: 20px;';
|
|
overlay.innerHTML = `
|
|
<div style="width:80px; height:80px; border-radius:50%; background:#10b981; display:flex; align-items:center; justify-content:center; margin-bottom:24px; color:#fff; font-size:40px;"><i class="mdi mdi-check"></i></div>
|
|
<h2 style="font-weight:800; color:#0f172a; font-size:24px; margin-bottom:8px;">Report Submitted Successfully</h2>
|
|
<p style="color:#64748b; font-size:15px; margin-bottom:0;">Your activity has been safely recorded in the system.</p>
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
|
|
} else {
|
|
showAlert('Submit Error', result.message || 'Unknown error occurred.', 'error');
|
|
btnSubmit.disabled = false;
|
|
btnSubmit.innerHTML = '<i class="mdi mdi-send"></i> Submit Report';
|
|
}
|
|
} catch (err) {
|
|
showAlert('Network Error', 'Failed to reach the server. Please try again.', 'error');
|
|
btnSubmit.disabled = false;
|
|
btnSubmit.innerHTML = '<i class="mdi mdi-send"></i> Submit Report';
|
|
}
|
|
});
|
|
|
|
// ── Prevent right-click ──
|
|
document.addEventListener('contextmenu', e => e.preventDefault());
|
|
|
|
|
|
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|