Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Desk + PWA): Geolocation in Employee Checkin (backport #1642) #1824

Merged
merged 11 commits into from
May 29, 2024
111 changes: 83 additions & 28 deletions frontend/src/components/CheckInPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<div class="flex flex-col bg-white rounded w-full py-6 px-4 border-none">
<h2 class="text-lg font-bold text-gray-900">Hey, {{ employee?.data?.first_name }} 👋</h2>

<template v-if="allowCheckinFromMobile.data">
<template v-if="HRSettings.doc?.allow_employee_checkin_from_mobile_app">
<div class="font-medium text-sm text-gray-500 mt-1.5" v-if="lastLog">
Last {{ lastLogType }} was at {{ lastLogTime }}
</div>
<Button
class="mt-4 mb-1 drop-shadow-sm py-5 text-base"
id="open-checkin-modal"
@click="checkinTimestamp = dayjs().format('YYYY-MM-DD HH:mm:ss')"
@click="handleEmployeeCheckin"
>
<template #prefix>
<FeatherIcon
Expand All @@ -19,51 +19,72 @@
</template>
{{ nextAction.label }}
</Button>

<ion-modal
ref="modal"
trigger="open-checkin-modal"
:initial-breakpoint="1"
:breakpoints="[0, 1]"
>
<div class="h-40 w-full flex flex-col items-center justify-center gap-5 p-4 mb-5">
<div class="flex flex-col gap-1.5 items-center justify-center">
<div class="font-bold text-xl">
{{ dayjs(checkinTimestamp).format("hh:mm:ss a") }}
</div>
<div class="font-medium text-gray-500 text-sm">
{{ dayjs().format("D MMM, YYYY") }}
</div>
</div>
<Button
variant="solid"
class="w-full py-5 text-sm"
@click="submitLog(nextAction.action)"
>
Confirm {{ nextAction.label }}
</Button>
</div>
</ion-modal>
</template>

<div v-else class="font-medium text-sm text-gray-500 mt-1.5">
{{ dayjs().format("ddd, D MMMM, YYYY") }}
</div>
</div>

<ion-modal
v-if="HRSettings.doc?.allow_employee_checkin_from_mobile_app"
ref="modal"
trigger="open-checkin-modal"
:initial-breakpoint="1"
:breakpoints="[0, 1]"
>
<div class="h-120 w-full flex flex-col items-center justify-center gap-5 p-4 mb-5">
<div class="flex flex-col gap-1.5 mt-2 items-center justify-center">
<div class="font-bold text-xl">
{{ dayjs(checkinTimestamp).format("hh:mm:ss a") }}
</div>
<div class="font-medium text-gray-500 text-sm">
{{ dayjs().format("D MMM, YYYY") }}
</div>
</div>

<template v-if="HRSettings.doc?.allow_geolocation_tracking">
<span v-if="locationStatus" class="font-medium text-gray-500 text-sm">
{{ locationStatus }}
</span>

<div class="rounded border-4 translate-z-0 block overflow-hidden w-full h-170">
<iframe
width="100%"
height="170"
frameborder="0"
scrolling="no"
marginheight="0"
marginwidth="0"
style="border: 0"
:src="`https://maps.google.com/maps?q=${latitude},${longitude}&hl=en&z=15&amp;output=embed`"
>
</iframe>
</div>
</template>

<Button variant="solid" class="w-full py-5 text-sm" @click="submitLog(nextAction.action)">
Confirm {{ nextAction.label }}
</Button>
</div>
</ion-modal>
</template>

<script setup>
import { createListResource, toast, FeatherIcon } from "frappe-ui"
import { computed, inject, ref, onMounted, onBeforeUnmount } from "vue"
import { IonModal, modalController } from "@ionic/vue"
import { allowCheckinFromMobile } from "@/data/settings"
import { HRSettings } from "@/data/HRSettings"

const DOCTYPE = "Employee Checkin"

const socket = inject("$socket")
const employee = inject("$employee")
const dayjs = inject("$dayjs")
const checkinTimestamp = ref(null)
const latitude = ref(0)
const longitude = ref(0)
const locationStatus = ref("")

const checkins = createListResource({
doctype: DOCTYPE,
Expand Down Expand Up @@ -102,6 +123,38 @@ const lastLogTime = computed(() => {
return `${formattedTime} on ${dayjs(timestamp).format("D MMM, YYYY")}`
})

function handleLocationSuccess(position) {
latitude.value = position.coords.latitude
longitude.value = position.coords.longitude

locationStatus.value = `
Latitude: ${Number(latitude.value).toFixed(5)}°,
Longitude: ${Number(longitude.value).toFixed(5)}°
`
}

function handleLocationError(error) {
locationStatus.value = "Unable to retrieve your location"
if (error) locationStatus.value += `: ERROR(${error.code}): ${error.message}`
}

const fetchLocation = () => {
if (!navigator.geolocation) {
locationStatus.value = "Geolocation is not supported by your current browser"
} else {
locationStatus.value = "Locating..."
navigator.geolocation.getCurrentPosition(handleLocationSuccess, handleLocationError)
}
}

const handleEmployeeCheckin = () => {
checkinTimestamp.value = dayjs().format("YYYY-MM-DD HH:mm:ss")

if (HRSettings.doc?.allow_geolocation_tracking) {
fetchLocation()
}
}

const submitLog = (logType) => {
const action = logType === "IN" ? "Check-in" : "Check-out"

Expand All @@ -110,6 +163,8 @@ const submitLog = (logType) => {
employee: employee.data.name,
log_type: logType,
time: checkinTimestamp.value,
latitude: latitude.value,
longitude: longitude.value,
},
{
onSuccess() {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/data/HRSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createDocumentResource } from "frappe-ui"

export const HRSettings = createDocumentResource({
doctype: "HR Settings",
name: "HR Settings",
auto: true,
})
6 changes: 0 additions & 6 deletions frontend/src/data/settings.js

This file was deleted.

6 changes: 0 additions & 6 deletions hrms/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from frappe.model.workflow import get_workflow_name
from frappe.query_builder import Order
from frappe.utils import getdate
from frappe.utils.data import cint

SUPPORTED_FIELD_TYPES = [
"Link",
Expand Down Expand Up @@ -627,8 +626,3 @@ def get_workflow_state_field(doctype: str) -> str | None:
def get_allowed_states_for_workflow(workflow: dict, user_id: str) -> list[str]:
user_roles = frappe.get_roles(user_id)
return [transition.state for transition in workflow.transitions if transition.allowed in user_roles]


@frappe.whitelist()
def is_employee_checkin_allowed():
return cint(frappe.db.get_single_value("HR Settings", "allow_employee_checkin_from_mobile_app"))
50 changes: 48 additions & 2 deletions hrms/hr/doctype/employee_checkin/employee_checkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@
// For license information, please see license.txt

frappe.ui.form.on("Employee Checkin", {
// setup: (frm) => {
// }
refresh: async (_frm) => {
const allow_geolocation_tracking = await frappe.db.get_single_value(
"HR Settings",
"allow_geolocation_tracking",
);

if (!allow_geolocation_tracking) {
hide_field(["fetch_geolocation", "latitude", "longitude", "geolocation"]);
return;
}
},

fetch_geolocation: async (frm) => {
if (!navigator.geolocation) {
frappe.msgprint({
message: __("Geolocation is not supported by your current browser"),
title: __("Geolocation Error"),
indicator: "red",
});
hide_field(["geolocation"]);
return;
}

frappe.dom.freeze(__("Fetching your geolocation") + "...");

navigator.geolocation.getCurrentPosition(
async (position) => {
frm.set_value("latitude", position.coords.latitude);
frm.set_value("longitude", position.coords.longitude);

await frm.call("set_geolocation_from_coordinates");
frappe.dom.unfreeze();
},
(error) => {
frappe.dom.unfreeze();

let msg = __("Unable to retrieve your location") + "<br><br>";
if (error) {
msg += __("ERROR({0}): {1}", [error.code, error.message]);
}
frappe.msgprint({
message: msg,
title: __("Geolocation Error"),
indicator: "red",
});
},
);
},
});
59 changes: 58 additions & 1 deletion hrms/hr/doctype/employee_checkin/employee_checkin.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@
"device_id",
"skip_auto_attendance",
"attendance",
"location_section",
"latitude",
"column_break_yqpi",
"longitude",
"section_break_ksbo",
"fetch_geolocation",
"geolocation",
"shift_timings_section",
"shift_start",
"shift_end",
"column_break_vyyt",
"shift_actual_start",
"shift_actual_end"
],
Expand Down Expand Up @@ -107,10 +116,58 @@
"fieldtype": "Datetime",
"hidden": 1,
"label": "Shift Actual End"
},
{
"fieldname": "location_section",
"fieldtype": "Section Break",
"label": "Location"
},
{
"fieldname": "geolocation",
"fieldtype": "Geolocation",
"label": "Geolocation",
"read_only": 1
},
{
"fieldname": "shift_timings_section",
"fieldtype": "Section Break",
"label": "Shift Timings"
},
{
"fieldname": "column_break_vyyt",
"fieldtype": "Column Break"
},
{
"fieldname": "latitude",
"fieldtype": "Float",
"label": "Latitude",
"precision": "7",
"read_only": 1
},
{
"fieldname": "longitude",
"fieldtype": "Float",
"label": "Longitude",
"precision": "7",
"read_only": 1
},
{
"fieldname": "column_break_yqpi",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ksbo",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "fetch_geolocation",
"fieldtype": "Button",
"label": "Fetch Geolocation"
}
],
"links": [],
"modified": "2024-04-02 01:50:23.150627",
"modified": "2024-05-29 21:19:11.550766",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",
Expand Down
23 changes: 23 additions & 0 deletions hrms/hr/doctype/employee_checkin/employee_checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_log()
self.fetch_shift()
self.set_geolocation_from_coordinates()

def validate_duplicate_log(self):
doc = frappe.db.exists(
Expand Down Expand Up @@ -60,6 +61,28 @@ def fetch_shift(self):
else:
self.shift = None

@frappe.whitelist()
def set_geolocation_from_coordinates(self):
if not frappe.db.get_single_value("HR Settings", "allow_geolocation_tracking"):
return

if not (self.latitude and self.longitude):
return

self.geolocation = frappe.json.dumps(
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
# geojson needs coordinates in reverse order: long, lat instead of lat, long
"geometry": {"type": "Point", "coordinates": [self.longitude, self.latitude]},
}
],
}
)


@frappe.whitelist()
def add_log_based_on_employee_field(
Expand Down
Loading
Loading