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(PWA): Attendance Dashboard #2170

Merged
merged 36 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d8455f6
feat: add Attendance Dashboard
krantheman Aug 9, 2024
b353fa1
feat: add attendance summary
krantheman Aug 9, 2024
d1a8eaa
feat: fetch event of selected month
krantheman Aug 9, 2024
eb5a21e
feat: add calendar days
krantheman Aug 20, 2024
70e6563
chore: remove redundant classes
krantheman Aug 20, 2024
032cd71
fix: colors and icons
krantheman Aug 21, 2024
a8d2615
feat: add Upcoming Shifts list
krantheman Aug 22, 2024
acf1974
feat: add color coding to summary
krantheman Aug 22, 2024
b635ff2
feat: add ionic modal for shifts
krantheman Aug 22, 2024
6bdab50
feat: add Shift Request list
krantheman Aug 26, 2024
55020d5
feat: add Shift Request list view
krantheman Aug 26, 2024
c98de3f
feat: add Shift Request form
krantheman Aug 26, 2024
8742cd7
feat: set shift request approvers
krantheman Aug 27, 2024
e43f891
feat: add recent attendance requests
krantheman Aug 27, 2024
bf961f0
feat: add Attendance Request list and form
krantheman Aug 27, 2024
8b9c88b
feat: add Attendance Request summary
krantheman Aug 27, 2024
9375f97
Merge branch 'develop' into feat-attendance-dashboard
krantheman Sep 9, 2024
54d8df8
fix: submit for Attendance Request
krantheman Sep 9, 2024
1ad4d5f
refactor: upcoming shifts
krantheman Sep 9, 2024
de5cb41
feat: add Shift Assignment list
krantheman Sep 9, 2024
0e43c1d
feat: add Shift Assignment form
krantheman Sep 10, 2024
5de57c4
fix: render 'New' button only if user has perms for doctype
krantheman Sep 10, 2024
91629f2
feat: add Employee Checkin list
krantheman Sep 10, 2024
19390c3
feat: add Employee Checkin form
krantheman Sep 10, 2024
834cc2b
feat: add DatetimePicker and switch from Input to DatePicker
krantheman Sep 11, 2024
336201d
feat: add Employee Checkin summary
krantheman Sep 12, 2024
924be06
feat: add Request a Shift button
krantheman Sep 17, 2024
0280160
feat: show employee name for team requests
krantheman Sep 17, 2024
d182ff2
refactor: remove tabs where unnecessary
krantheman Sep 17, 2024
b6090b2
fix: Employee Checkin list filters
krantheman Sep 17, 2024
589d660
feat: add attendance and shift requests to home page
krantheman Sep 17, 2024
0e9285b
feat: show team shift requests
krantheman Sep 18, 2024
2cb0146
feat: update icons for attendance and shifts
krantheman Sep 18, 2024
d180ae3
feat: add notifications for shift request
krantheman Sep 18, 2024
083c9bc
refactor: use ListItem component for all items
krantheman Sep 18, 2024
e0edd24
fix: get_shift_requests api fields
krantheman Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions frontend/src/components/AttendanceCalendar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<div class="flex flex-col w-full gap-5" v-if="calendarEvents.data">
<div class="text-lg text-gray-800 font-bold">Attendance Calendar</div>

<div class="flex flex-col gap-6 bg-white py-6 px-3.5 rounded-lg border-none">
<!-- Month Change -->
<div class="flex flex-row justify-between items-center px-4">
<Button
icon="chevron-left"
variant="ghost"
@click="firstOfMonth = firstOfMonth.subtract(1, 'M')"
/>
<span class="text-lg text-gray-800 font-bold">
{{ firstOfMonth.format("MMMM") }} {{ firstOfMonth.format("YYYY") }}
</span>
<Button
icon="chevron-right"
variant="ghost"
@click="firstOfMonth = firstOfMonth.add(1, 'M')"
/>
</div>

<!-- Calendar -->
<div class="grid grid-cols-7 gap-y-3">
<div
v-for="day in DAYS"
class="flex justify-center text-gray-600 text-sm font-medium leading-6"
>
{{ day }}
</div>
<div v-for="_ in firstOfMonth.get('d')" />
<div v-for="index in firstOfMonth.endOf('M').get('D')">
<div
class="h-8 w-8 flex rounded-full mx-auto"
:class="getEventOnDate(index) && `bg-${colorMap[getEventOnDate(index)]}`"
>
<span class="text-gray-800 text-sm font-medium m-auto">
{{ index }}
</span>
</div>
</div>
</div>

<hr />

<!-- Summary -->
<div class="grid grid-cols-4 mx-2">
<div v-for="status in summaryStatuses" class="flex flex-col gap-1">
<div class="flex flex-row gap-1 items-center">
<span class="rounded full h-3 w-3" :class="`bg-${colorMap[status]}`" />
<span class="text-gray-600 text-sm font-medium leading-5"> {{ status }} </span>
</div>
<span class="text-gray-800 text-base font-semibold leading-6 mx-auto">
{{ summary[status] || 0 }}
</span>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { computed, inject, ref, watch } from "vue"
import { createResource } from "frappe-ui"

const dayjs = inject("$dayjs")
const employee = inject("$employee")
const firstOfMonth = ref(dayjs().date(1).startOf("D"))

const colorMap = {
Present: "green-200",
"Work From Home": "green-200",
"Half Day": "yellow-100",
Absent: "red-100",
"On Leave": "blue-100",
Holiday: "gray-100",
}

const summaryStatuses = ["Present", "Half Day", "Absent", "On Leave"]

const summary = computed(() => {
const summary = {}

for (const status of Object.values(calendarEvents.data)) {
let updatedStatus = status === "Work From Home" ? "Present" : status
if (updatedStatus in summary) {
summary[updatedStatus] += 1
} else {
summary[updatedStatus] = 1
}
}

return summary
})

watch(
() => firstOfMonth.value,
() => {
calendarEvents.fetch()
}
)

const getEventOnDate = (date) => {
return calendarEvents.data[firstOfMonth.value.date(date).format("YYYY-MM-DD")]
}

const DAYS = ["S", "M", "T", "W", "T", "F", "S"]

//resources
const calendarEvents = createResource({
url: "hrms.api.get_attendance_calendar_events",
auto: true,
cache: "hrms:attendance_calendar_events",
makeParams() {
return {
employee: employee.data.name,
from_date: firstOfMonth.value.format("YYYY-MM-DD"),
to_date: firstOfMonth.value.endOf("M").format("YYYY-MM-DD"),
}
},
})
</script>
54 changes: 54 additions & 0 deletions frontend/src/components/AttendanceRequestItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<ListItem>
<template #left>
<AttendanceIcon class="h-5 w-5 text-gray-500" />
<div class="flex flex-col items-start gap-1.5">
<div class="text-base font-normal text-gray-800">
{{ props.doc.reason }}
</div>
<div class="text-xs font-normal text-gray-500">
<span>{{ props.doc.attendance_dates || getDates(props.doc) }}</span>
<span v-if="props.doc.to_date">
<span class="whitespace-pre"> &middot; </span>
<span class="whitespace-nowrap">{{
`${props.doc.total_attendance_days || getTotalDays(props.doc)}d`
}}</span>
</span>
</div>
</div>
</template>
<template #right>
<Badge variant="outline" :theme="colorMap[status]" :label="status" size="md" />
<FeatherIcon name="chevron-right" class="h-5 w-5 text-gray-500" />
</template>
</ListItem>
</template>

<script setup>
import { computed } from "vue"
import { Badge, FeatherIcon } from "frappe-ui"

import ListItem from "@/components/ListItem.vue"
import AttendanceIcon from "@/components/icons/AttendanceIcon.vue"
import { getDates, getTotalDays } from "@/data/attendance"

const props = defineProps({
doc: {
type: Object,
},
workflowStateField: {
type: String,
required: false,
},
})

const status = computed(() => {
if (props.workflowStateField) return props.doc[props.workflowStateField]
return props.doc.docstatus ? "Submitted" : "Draft"
})

const colorMap = {
Draft: "gray",
Submitted: "blue",
}
</script>
6 changes: 6 additions & 0 deletions frontend/src/components/BottomTabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import HomeIcon from "@/components/icons/HomeIcon.vue"
import LeaveIcon from "@/components/icons/LeaveIcon.vue"
import ExpenseIcon from "@/components/icons/ExpenseIcon.vue"
import SalaryIcon from "@/components/icons/SalaryIcon.vue"
import AttendanceIcon from "@/components/icons/AttendanceIcon.vue"

const route = useRoute()

Expand All @@ -39,6 +40,11 @@ const tabItems = [
title: "Home",
route: "/home",
},
{
icon: AttendanceIcon,
title: "Attendance",
route: "/dashboard/attendance",
},
{
icon: LeaveIcon,
title: "Leaves",
Expand Down
20 changes: 7 additions & 13 deletions frontend/src/components/CheckInPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

<template v-if="settings.data?.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 }}
<span>Last {{ lastLogType }} was at {{ formatTimestamp(lastLog.time) }}</span>
<span class="whitespace-pre"> &middot; </span>
<router-link :to="{ name: 'EmployeeCheckinListView' }" v-slot="{ navigate }">
<span @click="navigate" class="underline">View List</span>
</router-link>
</div>
<Button
class="mt-4 mb-1 drop-shadow-sm py-5 text-base"
Expand Down Expand Up @@ -75,6 +79,8 @@ import { createResource, createListResource, toast, FeatherIcon } from "frappe-u
import { computed, inject, ref, onMounted, onBeforeUnmount } from "vue"
import { IonModal, modalController } from "@ionic/vue"

import { formatTimestamp } from "@/utils/formatters"

const DOCTYPE = "Employee Checkin"

const socket = inject("$socket")
Expand Down Expand Up @@ -115,18 +121,6 @@ const nextAction = computed(() => {
: { action: "IN", label: "Check In" }
})

const lastLogTime = computed(() => {
const timestamp = lastLog?.value?.time
const formattedTime = dayjs(timestamp).format("hh:mm a")

if (dayjs(timestamp).isToday()) return formattedTime
else if (dayjs(timestamp).isYesterday()) return `${formattedTime} yesterday`
else if (dayjs(timestamp).isSame(dayjs(), "year"))
return `${formattedTime} on ${dayjs(timestamp).format("D MMM")}`

return `${formattedTime} on ${dayjs(timestamp).format("D MMM, YYYY")}`
})

function handleLocationSuccess(position) {
latitude.value = position.coords.latitude
longitude.value = position.coords.longitude
Expand Down
83 changes: 33 additions & 50 deletions frontend/src/components/EmployeeAdvanceItem.vue
Original file line number Diff line number Diff line change
@@ -1,52 +1,37 @@
<template>
<div class="flex flex-col w-full justify-center gap-2.5">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-start gap-3 grow">
<EmployeeAdvanceIcon class="h-5 w-5 mt-[3px] text-gray-500" />
<div class="flex flex-col items-start gap-1">
<div
v-if="props.doc.balance_amount"
class="text-lg font-bold text-gray-800 leading-6"
>
{{ `${currency} ${props.doc.balance_amount} /` }}
<span class="text-gray-600">
{{ `${currency} ${props.doc.paid_amount}` }}
</span>
</div>
<div v-else class="text-lg font-bold text-gray-800 leading-6">
{{ `${currency} ${props.doc.advance_amount}` }}
</div>
<div class="text-xs font-normal text-gray-500">
<span>
{{ props.doc.purpose }}
</span>
<span class="whitespace-pre"> &middot; </span>
<span class="whitespace-nowrap">
{{ postingDate }}
</span>
</div>
<ListItem
:isTeamRequest="props.isTeamRequest"
:employee="props.doc.employee"
:employeeName="props.doc.employee_name"
>
<template #left>
<EmployeeAdvanceIcon class="h-5 w-5 mt-[3px] text-gray-500" />
<div class="flex flex-col items-start gap-1">
<div v-if="props.doc.balance_amount" class="text-lg font-bold text-gray-800 leading-6">
{{ `${currency} ${props.doc.balance_amount} /` }}
<span class="text-gray-600">
{{ `${currency} ${props.doc.paid_amount}` }}
</span>
</div>
<div v-else class="text-lg font-bold text-gray-800 leading-6">
{{ `${currency} ${props.doc.advance_amount}` }}
</div>
<div class="text-xs font-normal text-gray-500">
<span>
{{ props.doc.purpose }}
</span>
<span class="whitespace-pre"> &middot; </span>
<span class="whitespace-nowrap">
{{ postingDate }}
</span>
</div>
</div>
<div class="flex flex-row justify-end items-center gap-2">
<Badge
variant="outline"
:theme="colorMap[status]"
:label="status"
size="md"
/>
<FeatherIcon name="chevron-right" class="h-5 w-5 text-gray-500" />
</div>
</div>
<div
v-if="props.isTeamRequest"
class="flex flex-row items-center gap-2 pl-8"
>
<EmployeeAvatar :employeeID="props.doc.employee" />
<div class="text-sm text-gray-600 grow">
{{ props.doc.employee_name }}
</div>
</div>
</div>
</template>
<template #right>
<Badge variant="outline" :theme="colorMap[status]" :label="status" size="md" />
<FeatherIcon name="chevron-right" class="h-5 w-5 text-gray-500" />
</template>
</ListItem>
</template>

<script setup>
Expand All @@ -55,7 +40,7 @@ import { computed, inject } from "vue"

import { getCurrencySymbol } from "@/data/currencies"

import EmployeeAvatar from "@/components/EmployeeAvatar.vue"
import ListItem from "@/components/ListItem.vue"
import EmployeeAdvanceIcon from "@/components/icons/EmployeeAdvanceIcon.vue"

const dayjs = inject("$dayjs")
Expand Down Expand Up @@ -88,8 +73,6 @@ const postingDate = computed(() => {
})

const status = computed(() => {
return props.workflowStateField
? props.doc[props.workflowStateField]
: props.doc.status
return props.workflowStateField ? props.doc[props.workflowStateField] : props.doc.status
})
</script>
29 changes: 29 additions & 0 deletions frontend/src/components/EmployeeCheckinItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<ListItem>
<template #left>
<FeatherIcon name="clock" class="h-5 w-5 text-gray-500" />
<div class="flex flex-col items-start gap-1.5">
<div class="text-base font-normal text-gray-800">Log Type: {{ props.doc.log_type }}</div>
<div class="text-xs font-normal text-gray-500">
<span>{{ formatTimestamp(props.doc.time) }}</span>
</div>
</div>
</template>
<template #right>
<FeatherIcon name="chevron-right" class="h-5 w-5 text-gray-500" />
</template>
</ListItem>
</template>

<script setup>
import { FeatherIcon } from "frappe-ui"

import ListItem from "@/components/ListItem.vue"
import { formatTimestamp } from "@/utils/formatters"

const props = defineProps({
doc: {
type: Object,
},
})
</script>
Loading
Loading