Enhance employee portal with staff attendance overview
This commit is contained in:
@@ -109,9 +109,66 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-900">Today’s Attendance</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">{% if today_attendance %}Punch In: {{ today_attendance.punch_in_utc.strftime('%H:%M') if today_attendance.punch_in_utc else '-' }} · Punch Out: {{ today_attendance.punch_out_utc.strftime('%H:%M') if today_attendance.punch_out_utc else '-' }}{% else %}Attendance not marked for today.{% endif %}</p>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
{% if today_attendance %}
|
||||
Punch In: {{ today_attendance.punch_in_local_at.strftime('%H:%M') if today_attendance.punch_in_local_at else (today_attendance.punch_in_utc.strftime('%H:%M UTC') if today_attendance.punch_in_utc else '-') }} ·
|
||||
Punch Out: {{ today_attendance.punch_out_local_at.strftime('%H:%M') if today_attendance.punch_out_local_at else (today_attendance.punch_out_utc.strftime('%H:%M UTC') if today_attendance.punch_out_utc else '-') }}
|
||||
{% else %}
|
||||
Attendance not marked for today.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<a href="/employee/attendance" class="af-btn af-btn-secondary">Details</a>
|
||||
</div>
|
||||
|
||||
<div id="employee-dashboard-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 you to allow location before submitting attendance.
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3">
|
||||
<form method="post" action="/employee/attendance/punch-in" class="employee-dashboard-attendance-geo-form rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="latitude">
|
||||
<input type="hidden" name="longitude">
|
||||
<input type="hidden" name="accuracy_meters">
|
||||
<label class="block text-xs font-semibold uppercase text-emerald-700">Punch In Remarks</label>
|
||||
<input name="remarks" class="mt-2 w-full rounded-xl border border-emerald-200 bg-white px-3 py-2 text-sm" placeholder="Optional; client visit / OD reason">
|
||||
<button class="mt-3 w-full rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-50" {% if today_attendance and today_attendance.punch_in_utc %}disabled{% endif %}>Punch In</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/employee/attendance/punch-out" class="employee-dashboard-attendance-geo-form rounded-2xl border border-brand-200 bg-brand-50 p-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="latitude">
|
||||
<input type="hidden" name="longitude">
|
||||
<input type="hidden" name="accuracy_meters">
|
||||
<label class="block text-xs font-semibold uppercase text-brand-700">Punch Out Remarks</label>
|
||||
<input name="remarks" class="mt-2 w-full rounded-xl border border-brand-200 bg-white px-3 py-2 text-sm" placeholder="Optional remarks">
|
||||
<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:cursor-not-allowed 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>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-slate-100 pt-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h4 class="text-sm font-semibold text-slate-900">Recent Attendance</h4>
|
||||
<a href="/employee/attendance" class="text-xs font-semibold text-brand-700 hover:underline">View all</a>
|
||||
</div>
|
||||
<div class="mt-3 space-y-2">
|
||||
{% for row in attendance_rows or [] %}
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-semibold text-slate-900">{{ row.attendance_date }}</span>
|
||||
<span>{{ (row.status or '-').replace('_',' ').title() }}</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
In: {{ row.punch_in_local_at.strftime('%H:%M') if row.punch_in_local_at else (row.punch_in_utc.strftime('%H:%M UTC') if row.punch_in_utc else '-') }} ·
|
||||
Out: {{ row.punch_out_local_at.strftime('%H:%M') if row.punch_out_local_at else (row.punch_out_utc.strftime('%H:%M UTC') if row.punch_out_utc else '-') }} ·
|
||||
Duration: {{ (row.work_duration_minutes ~ ' min') if row.work_duration_minutes else '-' }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-xl border border-dashed border-slate-300 px-3 py-3 text-xs text-slate-500">No recent attendance records found.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href="/employee/attendance" class="af-btn af-btn-secondary">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,4 +202,131 @@
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-6 shadow-soft"><h3 class="font-semibold text-amber-900">Your login is not linked to an employee master</h3><p class="mt-1 text-sm text-amber-900">Submit an employee link request or ask Firm Admin/Partner/Branch Manager to link your user account from Employee Master.</p><a href="/employee/register" class="mt-4 inline-flex rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-800">Request Employee Link</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="employee-dashboard-location-consent-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-900/60 p-4" aria-hidden="true">
|
||||
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-10 w-10 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 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 <span class="font-semibold text-slate-900">Allow</span> in the browser location popup.
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
If location is denied/unavailable or you are outside branch range, the punch can still be saved for approval as per existing attendance rules.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<button type="button" id="employee-dashboard-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-dashboard-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>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const statusBox = document.getElementById('employee-dashboard-geo-status');
|
||||
const forms = document.querySelectorAll('.employee-dashboard-attendance-geo-form');
|
||||
const modal = document.getElementById('employee-dashboard-location-consent-modal');
|
||||
const modalContinue = document.getElementById('employee-dashboard-location-modal-continue');
|
||||
const modalCancel = document.getElementById('employee-dashboard-location-modal-cancel');
|
||||
|
||||
function updateStatus(message, tone) {
|
||||
if (!statusBox) return;
|
||||
statusBox.textContent = message;
|
||||
statusBox.className = 'mt-4 rounded-xl border px-3 py-2 text-xs ' + (
|
||||
tone === 'ok'
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
||||
: tone === 'warn'
|
||||
? 'border-amber-200 bg-amber-50 text-amber-700'
|
||||
: 'border-slate-200 bg-slate-50 text-slate-600'
|
||||
);
|
||||
}
|
||||
|
||||
function setHidden(form, lat, lon, acc) {
|
||||
const latInput = form.querySelector('input[name="latitude"]');
|
||||
const lonInput = form.querySelector('input[name="longitude"]');
|
||||
const accInput = form.querySelector('input[name="accuracy_meters"]');
|
||||
if (latInput) latInput.value = lat || '';
|
||||
if (lonInput) lonInput.value = lon || '';
|
||||
if (accInput) accInput.value = acc || '';
|
||||
}
|
||||
|
||||
function openLocationModal() {
|
||||
return new Promise(function (resolve) {
|
||||
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. Punch will be saved as pending approval if branch geofence is enabled.', 'warn');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus('Browser location popup opened. Please choose Allow to capture location...', 'info');
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
const 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, '', '', '');
|
||||
let reason = 'Location permission denied or unavailable.';
|
||||
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 === 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');
|
||||
resolve(false);
|
||||
}, { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener('submit', async function (event) {
|
||||
const latInput = form.querySelector('input[name="latitude"]');
|
||||
const lonInput = form.querySelector('input[name="longitude"]');
|
||||
if (!latInput || !lonInput) return;
|
||||
if (!latInput.value || !lonInput.value) {
|
||||
event.preventDefault();
|
||||
const proceed = await openLocationModal();
|
||||
if (!proceed) return;
|
||||
await captureLocation(form);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user