@@ -249,4 +257,5 @@
});
})();
+{% endif %}
{% endblock %}
diff --git a/app/modules/employees/ui.py b/app/modules/employees/ui.py
index 431f29c..a00ce70 100644
--- a/app/modules/employees/ui.py
+++ b/app/modules/employees/ui.py
@@ -1,4 +1,4 @@
-from __future__ import annotations
+from __future__ import annotations
from pathlib import Path
from io import BytesIO
@@ -195,6 +195,80 @@ def _client_ip(request: Request) -> str | None:
return request.client.host if request.client else None
+_ATTENDANCE_MOBILE_ONLY_MESSAGE = (
+ "Attendance punch-in and punch-out are allowed only from a mobile browser with location/GPS enabled. "
+ "Please open the ERP on your mobile phone to mark attendance. Desktop access is view-only for attendance history."
+)
+
+
+def _is_mobile_attendance_device(request: Request) -> bool:
+ """Server-side guard for staff self-attendance punch actions.
+
+ This intentionally does not replace GPS/geofence validation. It only blocks
+ desktop/laptop browsers from submitting punch-in or punch-out requests.
+ The existing attendance service still performs employee status, geo/IP,
+ timing, duplicate punch and approval checks.
+ """
+ user_agent = (request.headers.get("user-agent") or "").strip().lower()
+ if not user_agent:
+ return False
+
+ mobile_indicators = (
+ "mobi",
+ "android",
+ "iphone",
+ "ipod",
+ "ipad",
+ "windows phone",
+ "blackberry",
+ "bb10",
+ "opera mini",
+ "opera mobi",
+ "mobile safari",
+ )
+ desktop_indicators = (
+ "windows nt",
+ "macintosh",
+ "x11",
+ "cros",
+ "linux x86_64",
+ )
+
+ has_mobile_signal = any(token in user_agent for token in mobile_indicators)
+ has_desktop_signal = any(token in user_agent for token in desktop_indicators)
+
+ if has_mobile_signal:
+ return True
+ if has_desktop_signal:
+ return False
+ return False
+
+
+def _attendance_mobile_context(request: Request) -> dict:
+ is_mobile = _is_mobile_attendance_device(request)
+ return {
+ "is_mobile_attendance_device": is_mobile,
+ "attendance_punch_blocked_reason": None if is_mobile else _ATTENDANCE_MOBILE_ONLY_MESSAGE,
+ }
+
+
+def _render_self_attendance_page(request: Request, db, current_user, *, errors: list[str] | None = None):
+ employee = get_employee_for_user(db, current_user)
+ rows = list_own_attendance(db, current_user)
+ return _render(
+ request,
+ "modules/employees/templates/employees/self_attendance.html",
+ db,
+ current_user,
+ title="My Attendance",
+ employee=employee,
+ rows=rows,
+ today_attendance=get_today_attendance_for_user(db, current_user),
+ attendance_rows=rows[:7],
+ errors=errors or [],
+ **_attendance_mobile_context(request),
+ )
+
def _active_financial_year(request: Request) -> str | None:
@@ -2527,6 +2601,7 @@ def _employee_portal_context(request: Request, db, current_user, *, active_tab:
"payslips": list_own_payslips(db, current_user)[:5],
"own_documents": list_own_employee_documents(db, current_user)[:8],
"work_payload": work_payload,
+ **_attendance_mobile_context(request),
}
@@ -2586,20 +2661,7 @@ def employee_self_attendance(request: Request):
require_permission(db, current_user, "employees.attendance.view_self")
except Exception:
return _redirect_denied()
- employee = get_employee_for_user(db, current_user)
- rows = list_own_attendance(db, current_user)
- return _render(
- request,
- "modules/employees/templates/employees/self_attendance.html",
- db,
- current_user,
- title="My Attendance",
- employee=employee,
- rows=rows,
- today_attendance=get_today_attendance_for_user(db, current_user),
- attendance_rows=list_own_attendance(db, current_user)[:7],
- errors=[],
- )
+ return _render_self_attendance_page(request, db, current_user, errors=[])
finally:
db.close()
@@ -2626,6 +2688,8 @@ def employee_punch_in_submit(
require_permission(db, current_user, "employees.attendance.punch")
except Exception:
return _redirect_denied()
+ if not _is_mobile_attendance_device(request):
+ return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
punch_in_attendance(
db,
current_user,
@@ -2662,6 +2726,8 @@ def employee_punch_out_submit(
require_permission(db, current_user, "employees.attendance.punch")
except Exception:
return _redirect_denied()
+ if not _is_mobile_attendance_device(request):
+ return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
punch_out_attendance(
db,
current_user,