fix: show sla details on lead page
fixed some time calculation logic and some more fixes
This commit is contained in:
parent
da53a9eed5
commit
08c766c4cd
@ -1,8 +1,8 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
|
from datetime import timedelta
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_to_date,
|
add_to_date,
|
||||||
@ -30,8 +30,15 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
def handle_status(self, doc: Document):
|
def handle_status(self, doc: Document):
|
||||||
if doc.is_new() or not doc.has_value_changed("status"):
|
if doc.is_new() or not doc.has_value_changed("status"):
|
||||||
return
|
return
|
||||||
|
self.set_first_responded_on(doc)
|
||||||
self.set_first_response_time(doc)
|
self.set_first_response_time(doc)
|
||||||
|
|
||||||
|
def set_first_responded_on(self, doc: Document):
|
||||||
|
if doc.status != self.get_default_priority():
|
||||||
|
doc.first_responded_on = (
|
||||||
|
doc.first_responded_on or now_datetime()
|
||||||
|
)
|
||||||
|
|
||||||
def set_first_response_time(self, doc: Document):
|
def set_first_response_time(self, doc: Document):
|
||||||
start_at = doc.sla_creation
|
start_at = doc.sla_creation
|
||||||
end_at = doc.first_responded_on
|
end_at = doc.first_responded_on
|
||||||
@ -43,8 +50,18 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
self.set_response_by(doc)
|
self.set_response_by(doc)
|
||||||
|
|
||||||
def set_response_by(self, doc: Document):
|
def set_response_by(self, doc: Document):
|
||||||
start = doc.sla_creation
|
start_time = doc.sla_creation
|
||||||
doc.response_by = self.calc_time(start, doc.status, "first_response_time")
|
status = doc.status
|
||||||
|
|
||||||
|
priorities = self.get_priorities()
|
||||||
|
priority = priorities.get(status)
|
||||||
|
if not priority or doc.response_by:
|
||||||
|
return
|
||||||
|
|
||||||
|
first_response_time = priority.get("first_response_time", 0)
|
||||||
|
end_time = self.calc_time(start_time, first_response_time)
|
||||||
|
if end_time:
|
||||||
|
doc.response_by = end_time
|
||||||
|
|
||||||
def handle_sla_status(self, doc: Document):
|
def handle_sla_status(self, doc: Document):
|
||||||
is_failed = self.is_first_response_failed(doc)
|
is_failed = self.is_first_response_failed(doc)
|
||||||
@ -65,12 +82,10 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
def calc_time(
|
def calc_time(
|
||||||
self,
|
self,
|
||||||
start_at: str,
|
start_at: str,
|
||||||
priority: str,
|
duration_seconds: int,
|
||||||
target: Literal["first_response_time"],
|
|
||||||
):
|
):
|
||||||
res = get_datetime(start_at)
|
res = get_datetime(start_at)
|
||||||
priority = self.get_priorities()[priority]
|
time_needed = duration_seconds
|
||||||
time_needed = priority.get(target, 0)
|
|
||||||
holidays = []
|
holidays = []
|
||||||
weekdays = get_weekdays()
|
weekdays = get_weekdays()
|
||||||
workdays = self.get_workdays()
|
workdays = self.get_workdays()
|
||||||
@ -98,8 +113,7 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
res = add_to_date(res, seconds=time_required, as_datetime=True)
|
res = add_to_date(res, seconds=time_required, as_datetime=True)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def calc_elapsed_time(self, start_time, end_time) -> float:
|
||||||
def calc_elapsed_time(self, start_at, end_at) -> float:
|
|
||||||
"""
|
"""
|
||||||
Get took from start to end, excluding non-working hours
|
Get took from start to end, excluding non-working hours
|
||||||
|
|
||||||
@ -107,38 +121,25 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
:param end_at: Date at which calculation ends
|
:param end_at: Date at which calculation ends
|
||||||
:return: Number of seconds
|
:return: Number of seconds
|
||||||
"""
|
"""
|
||||||
start_at = getdate(start_at)
|
start_time = get_datetime(start_time)
|
||||||
end_at = getdate(end_at)
|
end_time = get_datetime(end_time)
|
||||||
time_took = 0
|
holiday_list = []
|
||||||
holidays = []
|
working_day_list = self.get_working_days()
|
||||||
weekdays = get_weekdays()
|
working_hours = self.get_working_hours()
|
||||||
workdays = self.get_workdays()
|
|
||||||
while getdate(start_at) <= getdate(end_at):
|
total_seconds = 0
|
||||||
today = start_at
|
current_time = start_time
|
||||||
today_day = getdate(today)
|
|
||||||
today_weekday = weekdays[today.weekday()]
|
while current_time < end_time:
|
||||||
is_workday = today_weekday in workdays
|
in_holiday_list = current_time.date() in holiday_list
|
||||||
is_holiday = today_day in holidays
|
not_in_working_day_list = get_weekdays()[current_time.weekday()] not in working_day_list
|
||||||
if is_holiday or not is_workday:
|
if in_holiday_list or not_in_working_day_list or not self.is_working_time(current_time, working_hours):
|
||||||
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
current_time += timedelta(seconds=1)
|
||||||
continue
|
continue
|
||||||
today_workday = workdays[today_weekday]
|
total_seconds += 1
|
||||||
is_today = getdate(start_at) == getdate(end_at)
|
current_time += timedelta(seconds=1)
|
||||||
if not is_today:
|
|
||||||
working_start = today_workday.start_time
|
return total_seconds
|
||||||
working_end = today_workday.end_time
|
|
||||||
working_time = time_diff_in_seconds(working_start, working_end)
|
|
||||||
time_took += working_time
|
|
||||||
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
|
||||||
continue
|
|
||||||
now_in_seconds = time_diff_in_seconds(today, today_day)
|
|
||||||
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
|
|
||||||
end_at_seconds = time_diff_in_seconds(getdate(end_at), end_at)
|
|
||||||
end_time = max(today_workday.end_time.total_seconds(), end_at_seconds)
|
|
||||||
time_taken = end_time - start_time
|
|
||||||
time_took += time_taken
|
|
||||||
start_at = getdate(add_to_date(start_at, days=1, as_datetime=True))
|
|
||||||
return time_took
|
|
||||||
|
|
||||||
def get_priorities(self):
|
def get_priorities(self):
|
||||||
"""
|
"""
|
||||||
@ -149,6 +150,16 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
res[row.priority] = row
|
res[row.priority] = row
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_default_priority(self):
|
||||||
|
"""
|
||||||
|
Return default priority
|
||||||
|
"""
|
||||||
|
for row in self.priorities:
|
||||||
|
if row.default_priority:
|
||||||
|
return row.priority
|
||||||
|
|
||||||
|
return self.priorities[0].priority
|
||||||
|
|
||||||
def get_workdays(self) -> dict[str, dict]:
|
def get_workdays(self) -> dict[str, dict]:
|
||||||
"""
|
"""
|
||||||
Return workdays related info as a dict. With `workday` as key
|
Return workdays related info as a dict. With `workday` as key
|
||||||
@ -157,3 +168,21 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
for row in self.working_hours:
|
for row in self.working_hours:
|
||||||
res[row.workday] = row
|
res[row.workday] = row
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def get_working_days(self) -> dict[str, dict]:
|
||||||
|
workdays = []
|
||||||
|
for row in self.working_hours:
|
||||||
|
workdays.append(row.workday)
|
||||||
|
return workdays
|
||||||
|
|
||||||
|
def get_working_hours(self) -> dict[str, dict]:
|
||||||
|
res = {}
|
||||||
|
for row in self.working_hours:
|
||||||
|
res[row.workday] = (row.start_time, row.end_time)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def is_working_time(self, date_time, working_hours):
|
||||||
|
day_of_week = get_weekdays()[date_time.weekday()]
|
||||||
|
start_time, end_time = working_hours.get(day_of_week, (0, 0))
|
||||||
|
date_time = timedelta(hours=date_time.hour, minutes=date_time.minute, seconds=date_time.second)
|
||||||
|
return start_time <= date_time < end_time
|
||||||
|
|||||||
@ -134,6 +134,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
|
<div v-if="lead.data.sla_status" class="flex flex-col gap-2 border-b p-5">
|
||||||
|
<div
|
||||||
|
v-if="lead.data.sla_status == 'First Response Due'"
|
||||||
|
class="flex items-center gap-4 text-base leading-5"
|
||||||
|
>
|
||||||
|
<div class="w-[106px] text-gray-600">Response By</div>
|
||||||
|
<Tooltip
|
||||||
|
:text="dateFormat(lead.data.response_by, 'ddd, MMM D, YYYY h:mm A')"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ timeAgo(lead.data.response_by) }}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="lead.data.sla_status == 'Fulfilled'"
|
||||||
|
class="flex items-center gap-4 text-base leading-5"
|
||||||
|
>
|
||||||
|
<div class="w-[106px] text-gray-600">Fulfilled In</div>
|
||||||
|
<Tooltip
|
||||||
|
:text="
|
||||||
|
dateFormat(
|
||||||
|
lead.data.first_responded_on,
|
||||||
|
'ddd, MMM D, YYYY h:mm A'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ formatTime(lead.data.first_response_time) }}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
lead.data.sla_status == 'Failed' && lead.data.first_responded_on
|
||||||
|
"
|
||||||
|
class="flex items-center gap-4 text-base leading-5"
|
||||||
|
>
|
||||||
|
<div class="w-[106px] text-gray-600">Fulfilled In</div>
|
||||||
|
<Tooltip
|
||||||
|
:text="
|
||||||
|
dateFormat(
|
||||||
|
lead.data.first_responded_on,
|
||||||
|
'ddd, MMM D, YYYY h:mm A'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ formatTime(lead.data.first_response_time) }}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 text-base leading-5">
|
||||||
|
<div class="w-[106px] text-gray-600">Status</div>
|
||||||
|
<div class="">
|
||||||
|
<Badge
|
||||||
|
:label="lead.data.sla_status"
|
||||||
|
variant="outline"
|
||||||
|
:theme="
|
||||||
|
lead.data.sla_status === 'Failed'
|
||||||
|
? 'red'
|
||||||
|
: lead.data.sla_status === 'Fulfilled'
|
||||||
|
? 'green'
|
||||||
|
: 'gray'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
@ -181,7 +247,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
|||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import { openWebsite, createToast, activeAgents } from '@/utils'
|
import {
|
||||||
|
openWebsite,
|
||||||
|
createToast,
|
||||||
|
activeAgents,
|
||||||
|
dateFormat,
|
||||||
|
timeAgo,
|
||||||
|
formatTime,
|
||||||
|
} from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
@ -197,6 +270,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Tabs,
|
Tabs,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
Badge,
|
||||||
call,
|
call,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@ -220,6 +294,15 @@ const lead = createResource({
|
|||||||
params: { name: props.leadId },
|
params: { name: props.leadId },
|
||||||
cache: ['lead', props.leadId],
|
cache: ['lead', props.leadId],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (
|
||||||
|
data.response_by &&
|
||||||
|
data.sla_status == 'First Response Due' &&
|
||||||
|
new Date(data.response_by) < new Date()
|
||||||
|
) {
|
||||||
|
updateField('sla_status', 'Failed')
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const reload = ref(false)
|
const reload = ref(false)
|
||||||
|
|||||||
@ -12,6 +12,31 @@ export function createToast(options) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTime(seconds) {
|
||||||
|
const days = Math.floor(seconds / (3600 * 24))
|
||||||
|
const hours = Math.floor((seconds % (3600 * 24)) / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
|
||||||
|
let formattedTime = ''
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
formattedTime += `${days}d `
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0 || days > 0) {
|
||||||
|
formattedTime += `${hours}h `
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0 || hours > 0 || days > 0) {
|
||||||
|
formattedTime += `${minutes}m `
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedTime += `${remainingSeconds}s`
|
||||||
|
|
||||||
|
return formattedTime.trim()
|
||||||
|
}
|
||||||
|
|
||||||
export function dateFormat(date, format) {
|
export function dateFormat(date, format) {
|
||||||
const _format = format || 'DD-MM-YYYY HH:mm:ss'
|
const _format = format || 'DD-MM-YYYY HH:mm:ss'
|
||||||
return useDateFormat(date, _format).value
|
return useDateFormat(date, _format).value
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user