« Back to History
attendance_form.php
|
20260723_000646.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================ Module: Attendance (Form/UI) + Map Preview + Redirect after Submit File : /erp/modules/attendance/attendance_form.php Scope: Module-only (NO global/base changes) ============================================================================ */ error_reporting(E_ALL); ini_set('display_errors',1); require __DIR__ . '/../auth/auth.php'; require_login(); $u = auth_user(); $company_id = (int)$u['company_id']; $user_id = (int)$u['id']; $name = htmlspecialchars((string)($u['name'] ?? ($u['email'] ?? 'User')), ENT_QUOTES, 'UTF-8'); $today = (new DateTime('today'))->format('Y-m-d'); /* Change if your home URL is different */ $HOME_URL = '/erp/public/index.php'; ?> <!doctype html> <html> <head> <meta charset="utf-8"/> <title>Attendance</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> <!-- Leaflet (map preview) --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="" ></script> <style> :root{ --brand:#344F73; --accent:#E86C5D; --bg:#f7f9fb; --card:#fff; --txt:#222; --muted:#666; --ok:#2e7d32; --bad:#c62828; } *{box-sizing:border-box} body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:var(--bg);margin:0;padding:12px;color:var(--txt)} .wrap{max-width:560px;margin:0 auto} .card{background:var(--card);border:1px solid #eaeaea;border-radius:16px;padding:16px;margin:12px 0;box-shadow:0 2px 10px rgba(0,0,0,.03)} h1{font-size:20px;margin:0 0 4px} .muted{color:var(--muted)} .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap} .pill{padding:6px 10px;border-radius:999px;background:#eef1f6} button{padding:12px 16px;border-radius:12px;border:0;cursor:pointer;font-weight:600} .primary{background:var(--brand);color:#fff} .ghost{background:#eef; color:#234} .ok{color:#fff;background:var(--ok)} .bad{color:#fff;background:var(--bad)} input, .pin{padding:12px;border:1px solid #ddd;border-radius:10px;width:100%} video, canvas{width:100%;border-radius:12px;background:#000;max-height:300px} .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px} .switch{display:inline-flex;align-items:center;gap:8px} .switch input{width:auto} .hint{font-size:12px;color:#666} .disabled{opacity:.5;pointer-events:none} .status{padding:10px;border-radius:10px;background:#f1f5f9} #map{width:100%;height:300px;border-radius:12px;overflow:hidden;border:1px solid #eaeaea} </style> </head> <body> <div class="wrap"> <div class="card"> <h1>Attendance</h1> <div class="row"> <div class="pill">👤 <?= $name ?></div> <div class="pill">đź“… <?= htmlspecialchars($today) ?></div> </div> <div id="status" class="status" style="margin-top:10px">Loading current status…</div> </div> <div class="card"> <div class="row" style="justify-content:space-between"> <strong>Location Verify</strong> <label class="switch"><input type="checkbox" id="autoSubmit"> Auto submit</label> </div> <p class="hint">Enable GPS. We’ll only allow punching inside the company geo-fence.</p> <div class="row"> <button id="btnLocate" class="ghost">Get Current Location</button> <div id="locBadge" class="pill">📍 waiting…</div> </div> <!-- Map Preview --> <div id="map" style="margin-top:12px"></div> </div> <div class="card"> <strong>Visual verification (Selfie)</strong> <p class="hint">Face should be visible. We store this picture with the punch.</p> <video id="video" autoplay playsinline muted></video> <canvas id="canvas" style="display:none;"></canvas> <div class="grid" style="margin-top:10px"> <button id="btnSnap" class="ghost">Capture Selfie</button> <button id="btnRetake" class="ghost disabled">Retake</button> </div> <div id="snapMsg" class="hint"></div> </div> <div class="card"> <strong>Supervisor PIN (optional)</strong> <input id="supPin" class="pin" placeholder="Enter supervisor/guard PIN if required"/> <p class="hint">If your company requires supervisor verification, ask them to enter the PIN before punching.</p> </div> <div class="card"> <div class="grid"> <button id="btnIn" class="primary disabled">Mark IN</button> <button id="btnOut" class="primary disabled">Mark OUT</button> </div> <div id="msg" style="margin-top:10px" class="hint"></div> </div> </div> <script> /* ------------------ State ------------------ */ let state = { geofences: [], inside: false, nearest: null, distance_m: null, coords: null, accuracy: null, photoDataURL: null, record: null }; const HOME_URL = <?= json_encode($HOME_URL) ?>; const statusEl = document.getElementById('status'); const locBadge = document.getElementById('locBadge'); const msgEl = document.getElementById('msg'); const btnLocate = document.getElementById('btnLocate'); const btnIn = document.getElementById('btnIn'); const btnOut = document.getElementById('btnOut'); const autoSubmit= document.getElementById('autoSubmit'); const video = document.getElementById('video'); const canvas = document.getElementById('canvas'); const btnSnap= document.getElementById('btnSnap'); const btnRetake = document.getElementById('btnRetake'); const snapMsg = document.getElementById('snapMsg'); const supPin = document.getElementById('supPin'); /* ------------------ Map (Leaflet) ------------------ */ let map, userMarker, accCircle; let fenceCircles = []; let firstFitDone = false; function initMap(){ map = L.map('map'); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{ maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); map.setView([22.9734, 78.6569], 5); // India fallback } function renderGeofences(geofences){ fenceCircles.forEach(c=>map.removeLayer(c)); fenceCircles = []; const group = []; geofences.forEach(g=>{ const lat = parseFloat(g.latitude), lng = parseFloat(g.longitude), r = parseInt(g.radius_m); const circle = L.circle([lat,lng], { radius: r, color: '#344F73', weight: 2, fillOpacity: 0.08 }).addTo(map); circle.bindTooltip(`${g.name} • ${r} m`); fenceCircles.push(circle); group.push(circle); }); if (group.length && !firstFitDone){ const fg = L.featureGroup(group); map.fitBounds(fg.getBounds().pad(0.3)); firstFitDone = true; } } function updateUserMarker(lat,lng,accuracy,inside){ if (!map) return; if (!userMarker){ userMarker = L.marker([lat,lng]).addTo(map); } else { userMarker.setLatLng([lat,lng]); } if (accCircle){ map.removeLayer(accCircle); accCircle = null; } if (accuracy && isFinite(accuracy)){ accCircle = L.circle([lat,lng], { radius: accuracy, color: inside ? '#2e7d32' : '#c62828', weight: 1, fillOpacity: 0.05 }).addTo(map); } if (!firstFitDone){ const bounds = L.latLngBounds([lat,lng]); fenceCircles.forEach(c=>bounds.extend(c.getBounds())); map.fitBounds(bounds.pad(0.3)); firstFitDone = true; } } /* ------------------ UI helpers ------------------ */ function fmt(m){ return Math.round(m); } function setButtons() { const haveSelfie = !!state.photoDataURL; const inAllowed = state.inside && haveSelfie; const outAllowed = state.inside && haveSelfie; toggle(btnIn, inAllowed); toggle(btnOut, outAllowed); if (!state.inside) { msg('Outside company location. Move inside the geo-fence to enable buttons.'); } else if (!haveSelfie) { msg('Please capture selfie to enable punch.'); } else { msg('Ready to punch.'); } } function toggle(el, on){ el.classList.toggle('disabled', !on); } function msg(t, ok=false){ msgEl.textContent = t; msgEl.style.color = ok ? '#2e7d32' : '#666'; } function setLocBadge(text, good){ locBadge.textContent = text; locBadge.style.background = good ? '#e6f4ea' : '#fff3e0'; locBadge.style.color = good ? '#1b5e20' : '#e65100'; } /* ------------------ Status + Location ------------------ */ async function fetchStatus(){ const r = await fetch('attendance_api.php?action=status', {credentials:'same-origin'}); const j = await r.json(); state.geofences = j.geofences || []; state.record = j.record || null; let s = 'No punch yet today.'; if (state.record){ s = `IN: ${state.record.in_time||'-'} | OUT: ${state.record.out_time||'-'}`; } statusEl.textContent = s; renderGeofences(state.geofences); } function getLoc(){ if (!navigator.geolocation) { setLocBadge('Geolocation not supported', false); return; } navigator.geolocation.getCurrentPosition(onLoc, onLocErr, {enableHighAccuracy:true, timeout:15000, maximumAge:0}); } function onLoc(pos){ const {latitude, longitude, accuracy} = pos.coords; state.coords = {lat: latitude, lng: longitude}; state.accuracy = accuracy; let nearest=null, min=1e12; for(const g of state.geofences){ const d = haversine(latitude, longitude, parseFloat(g.latitude), parseFloat(g.longitude)); if (d < min){ min=d; nearest=g; } } state.nearest = nearest; state.distance_m = min; const allowed = nearest ? (min <= (parseInt(nearest.radius_m)+25)) : false; state.inside = !!allowed; setLocBadge(nearest ? `Nearest: ${nearest.name} | ${fmt(min)}m | acc ${fmt(accuracy)}m` : 'No geofence', allowed); updateUserMarker(latitude, longitude, accuracy, state.inside); setButtons(); if (autoSubmit.checked && state.inside && state.photoDataURL){ const wantType = (state.record && state.record.in_time && !state.record.out_time) ? 'out' : 'in'; doMark(wantType); } } function onLocErr(err){ setLocBadge('Location error: ' + err.message, false); setButtons(); } function haversine(lat1,lon1,lat2,lon2){ const R=6371000; const toRad = d=>d*Math.PI/180; const dLat=toRad(lat2-lat1), dLon=toRad(lon2-lon1); const a=Math.sin(dLat/2)**2 + Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2; return 2*R*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); } /* ------------------ Camera/Selfie ------------------ */ async function startCamera(){ try{ const stream = await navigator.mediaDevices.getUserMedia({video:true,audio:false}); video.srcObject = stream; }catch(e){ snapMsg.textContent = 'Camera permission denied. You can still punch without selfie if company config allows.'; } } function stopCamera(){ try{ const s = video.srcObject; if (s) s.getTracks().forEach(t=>t.stop()); }catch(e){} } function snap(){ const w = video.videoWidth, h = video.videoHeight; if (!w || !h){ snapMsg.textContent='Camera not ready yet'; return; } canvas.width=w; canvas.height=h; const ctx = canvas.getContext('2d'); ctx.drawImage(video,0,0,w,h); state.photoDataURL = canvas.toDataURL('image/jpeg', 0.85); snapMsg.textContent = 'Selfie captured âś”'; btnRetake.classList.remove('disabled'); setButtons(); } function retake(){ state.photoDataURL = null; snapMsg.textContent = 'Retake the selfie.'; btnRetake.classList.add('disabled'); setButtons(); } /* ------------------ Punch + Redirect ------------------ */ async function doMark(type){ if (!state.inside){ msg('You are outside the permitted location.', false); return; } if (!state.photoDataURL){ msg('Please capture selfie first.', false); return; } msg('Submitting…'); const form = new FormData(); form.append('action','mark'); form.append('type', type); form.append('lat', state.coords?.lat ?? ''); form.append('lng', state.coords?.lng ?? ''); form.append('accuracy', state.accuracy ?? ''); form.append('photo_base64', state.photoDataURL || ''); if (supPin.value.trim()) form.append('sup_pin', supPin.value.trim()); const r = await fetch('attendance_api.php', {method:'POST', body:form, credentials:'same-origin'}); const j = await r.json(); if (!j.ok){ msg(j.error || 'Failed', false); return; } state.record = j.record || null; const s = `IN: ${state.record?.in_time || '-'} | OUT: ${state.record?.out_time || '-'}`; statusEl.textContent = s; msg(type.toUpperCase()+' saved âś” Redirecting…', true); // Disable buttons and stop camera, then redirect to Home toggle(btnIn, false); toggle(btnOut, false); stopCamera(); setTimeout(()=>{ window.location.href = HOME_URL; }, 800); } /* ------------------ Wire up ------------------ */ btnLocate.addEventListener('click', getLoc); btnIn.addEventListener('click', ()=>doMark('in')); btnOut.addEventListener('click',()=>doMark('out')); btnSnap.addEventListener('click', snap); btnRetake.addEventListener('click', retake); initMap(); fetchStatus().then(()=>{ renderGeofences(state.geofences); startCamera(); }); </script> </body> </html>