Fix attendance location prompt and duration handling
This commit is contained in:
+107
-21
@@ -10,12 +10,25 @@
|
|||||||
Attendance not marked for today.
|
Attendance not marked for today.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
{% if today_attendance %}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">
|
||||||
|
Approval: {{ today_attendance.approval_status.replace('_',' ').title() }} ·
|
||||||
|
Geo: {{ today_attendance.punch_in_geo_status.replace('_',' ').title() if today_attendance.punch_in_geo_status else 'Not Captured' }}
|
||||||
|
{% if today_attendance.punch_out_geo_status %} / {{ today_attendance.punch_out_geo_status.replace('_',' ').title() }}{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<a href="/employee/attendance" class="af-btn af-btn-secondary">Full View</a>
|
<a href="/employee/attendance" class="af-btn af-btn-secondary">Full View</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if employee %}
|
{% if employee %}
|
||||||
<div id="employee-geo-status" class="mt-4 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">Location not captured yet.</div>
|
<div id="employee-geo-status" class="mt-4 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||||
|
Click Punch In/Punch Out. The app will ask for browser location before submitting attendance.
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||||
|
If branch geofence/IP verification is not configured, attendance may be approved without location check. Configure branch attendance geo/IP settings to enforce verification.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-1">
|
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-1">
|
||||||
<form method="post" action="/employee/attendance/punch-in" class="employee-attendance-geo-form rounded-2xl border border-slate-200 p-4">
|
<form method="post" action="/employee/attendance/punch-in" class="employee-attendance-geo-form rounded-2xl border border-slate-200 p-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
@@ -36,6 +49,29 @@
|
|||||||
<button class="mt-3 w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50" {% if not today_attendance or not today_attendance.punch_in_utc or today_attendance.punch_out_utc %}disabled{% endif %}>Punch Out</button>
|
<button class="mt-3 w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50" {% if not today_attendance or not today_attendance.punch_in_utc or today_attendance.punch_out_utc %}disabled{% endif %}>Punch Out</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="employee-location-consent-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-900/60 px-4" aria-hidden="true">
|
||||||
|
<div class="w-full max-w-lg rounded-3xl bg-white p-6 shadow-2xl">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">📍</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-slate-900">Enable location for attendance</h3>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-slate-600">
|
||||||
|
To mark attendance, this app needs your current location to verify whether you are within your branch geofence.
|
||||||
|
Click Continue, then choose <strong>Allow</strong> in the browser location popup.
|
||||||
|
</p>
|
||||||
|
<p class="mt-3 text-xs leading-5 text-slate-500">
|
||||||
|
If you are outside the branch or location is denied/unavailable, your attendance can still be saved.
|
||||||
|
It will require approval only when branch geofence/IP verification is configured.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
|
<button type="button" id="employee-location-modal-cancel" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Cancel</button>
|
||||||
|
<button type="button" id="employee-location-modal-continue" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,6 +87,7 @@ window.EmployeePortalGeo = window.EmployeePortalGeo || (function () {
|
|||||||
'border-slate-200 bg-slate-50 text-slate-600'
|
'border-slate-200 bg-slate-50 text-slate-600'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHidden(form, lat, lon, acc) {
|
function setHidden(form, lat, lon, acc) {
|
||||||
var latInput = form.querySelector('input[name="latitude"]');
|
var latInput = form.querySelector('input[name="latitude"]');
|
||||||
var lonInput = form.querySelector('input[name="longitude"]');
|
var lonInput = form.querySelector('input[name="longitude"]');
|
||||||
@@ -59,35 +96,84 @@ window.EmployeePortalGeo = window.EmployeePortalGeo || (function () {
|
|||||||
if (lonInput) lonInput.value = lon || '';
|
if (lonInput) lonInput.value = lon || '';
|
||||||
if (accInput) accInput.value = acc || '';
|
if (accInput) accInput.value = acc || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLocationModal() {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
var modal = document.getElementById('employee-location-consent-modal');
|
||||||
|
var modalContinue = document.getElementById('employee-location-modal-continue');
|
||||||
|
var modalCancel = document.getElementById('employee-location-modal-cancel');
|
||||||
|
if (!modal || !modalContinue || !modalCancel) {
|
||||||
|
resolve(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup(result) {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
modalContinue.removeEventListener('click', onContinue);
|
||||||
|
modalCancel.removeEventListener('click', onCancel);
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
function onContinue() { cleanup(true); }
|
||||||
|
function onCancel() {
|
||||||
|
updateStatus('Attendance punch cancelled. Location permission was not requested.', 'warn');
|
||||||
|
cleanup(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
modalContinue.addEventListener('click', onContinue);
|
||||||
|
modalCancel.addEventListener('click', onCancel);
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
modalContinue.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureLocation(form) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setHidden(form, '', '', '');
|
||||||
|
updateStatus('Browser geolocation is not available. Attendance can be saved, but geofence verification cannot be performed.', 'warn');
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateStatus('Browser location popup opened. Please choose Allow to capture location...', 'info');
|
||||||
|
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||||
|
var c = pos.coords || {};
|
||||||
|
setHidden(form, c.latitude, c.longitude, c.accuracy);
|
||||||
|
updateStatus('Location captured. Accuracy: ' + Math.round(c.accuracy || 0) + ' meters.', 'ok');
|
||||||
|
resolve(true);
|
||||||
|
}, function (err) {
|
||||||
|
setHidden(form, '', '', '');
|
||||||
|
var reason = 'Location permission denied or unavailable.';
|
||||||
|
if (err && err.code === 1) reason = 'Location permission denied. Allow location for this site in browser settings.';
|
||||||
|
if (err && err.code === 2) reason = 'Location unavailable. Enable device location service and try again.';
|
||||||
|
if (err && err.code === 3) reason = 'Location capture timed out. Try again with stable GPS/location service.';
|
||||||
|
updateStatus(reason + ' Attendance can be saved, but geofence verification cannot be performed.', 'warn');
|
||||||
|
resolve(false);
|
||||||
|
}, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function bind() {
|
function bind() {
|
||||||
document.querySelectorAll('.employee-attendance-geo-form').forEach(function (form) {
|
document.querySelectorAll('.employee-attendance-geo-form').forEach(function (form) {
|
||||||
if (form.dataset.geoBound === '1') return;
|
if (form.dataset.geoBound === '1') return;
|
||||||
form.dataset.geoBound = '1';
|
form.dataset.geoBound = '1';
|
||||||
form.addEventListener('submit', function (event) {
|
form.addEventListener('submit', async function (event) {
|
||||||
if (form.dataset.geoSubmitted === '1') return;
|
if (form.dataset.geoSubmitted === '1') return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!navigator.geolocation) {
|
|
||||||
updateStatus('Browser geolocation is not available. Punch will continue without location.', 'warn');
|
var proceed = await openLocationModal();
|
||||||
form.dataset.geoSubmitted = '1';
|
if (!proceed) return;
|
||||||
form.submit();
|
|
||||||
return;
|
await captureLocation(form);
|
||||||
}
|
form.dataset.geoSubmitted = '1';
|
||||||
updateStatus('Requesting browser location. Please click Allow...', 'info');
|
form.submit();
|
||||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
|
||||||
var c = pos.coords || {};
|
|
||||||
setHidden(form, c.latitude, c.longitude, c.accuracy);
|
|
||||||
updateStatus('Location captured. Accuracy: ' + Math.round(c.accuracy || 0) + ' meters.', 'ok');
|
|
||||||
form.dataset.geoSubmitted = '1';
|
|
||||||
form.submit();
|
|
||||||
}, function () {
|
|
||||||
setHidden(form, '', '', '');
|
|
||||||
updateStatus('Location denied/unavailable. Punch will continue and may need approval if branch geofence is enabled.', 'warn');
|
|
||||||
form.dataset.geoSubmitted = '1';
|
|
||||||
form.submit();
|
|
||||||
}, {enableHighAccuracy: true, timeout: 12000, maximumAge: 0});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {bind: bind};
|
return {bind: bind};
|
||||||
})();
|
})();
|
||||||
window.EmployeePortalGeo.bind();
|
window.EmployeePortalGeo.bind();
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
Click Continue, then choose <span class="font-semibold text-slate-900">Allow</span> in the browser location popup.
|
Click Continue, then choose <span class="font-semibold text-slate-900">Allow</span> in the browser location popup.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xs text-slate-500">
|
<p class="mt-2 text-xs text-slate-500">
|
||||||
If you are outside the branch or location is denied/unavailable, your attendance can still be saved as pending approval for OD/client visit review.
|
If you are outside the branch or location is denied/unavailable, your attendance can still be saved. It will require approval only when branch geofence/IP verification is configured.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
geoAttempted = true;
|
geoAttempted = true;
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
setHidden('', '', '');
|
setHidden('', '', '');
|
||||||
updateStatus('Browser geolocation is not available. Punch will be saved as pending approval if branch geofence is enabled.', 'warn');
|
updateStatus('Browser geolocation is not available. Attendance can be saved, but geofence verification cannot be performed if location is denied/unavailable.', 'warn');
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
if (err && err.code === 1) reason = 'Location permission denied. Please allow location for this site in browser settings.';
|
if (err && err.code === 1) reason = 'Location permission denied. Please allow location for this site in browser settings.';
|
||||||
if (err && err.code === 2) reason = 'Location unavailable. Please enable device GPS/location service and try again.';
|
if (err && err.code === 2) reason = 'Location unavailable. Please enable device GPS/location service and try again.';
|
||||||
if (err && err.code === 3) reason = 'Location capture timed out. Please try again near a window or with GPS enabled.';
|
if (err && err.code === 3) reason = 'Location capture timed out. Please try again near a window or with GPS enabled.';
|
||||||
updateStatus(reason + ' Punch will be saved as pending approval if branch geofence is enabled.', 'warn');
|
updateStatus(reason + ' Attendance can be saved, but geofence verification cannot be performed if location is denied/unavailable.', 'warn');
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
|
}, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user