Skip to content

Commit

Permalink
Merge pull request #1824 from frappe/mergify/bp/version-15-hotfix/pr-…
Browse files Browse the repository at this point in the history
…1642

feat(Desk + PWA): Geolocation in Employee Checkin (backport #1642)
  • Loading branch information
ruchamahabal authored May 29, 2024
2 parents a00425e + 2c46823 commit 29ebc47
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 48 deletions.
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

0 comments on commit 29ebc47

Please sign in to comment.