Merge pull request #38 from shariquerik/sla
This commit is contained in:
commit
bd51426e12
@ -24,7 +24,17 @@
|
||||
"deal_owner",
|
||||
"section_break_eepu",
|
||||
"lead",
|
||||
"column_break_bqvs"
|
||||
"column_break_bqvs",
|
||||
"sla_tab",
|
||||
"sla",
|
||||
"sla_creation",
|
||||
"column_break_pfvq",
|
||||
"sla_status",
|
||||
"response_details_section",
|
||||
"response_by",
|
||||
"column_break_hpvj",
|
||||
"first_response_time",
|
||||
"first_responded_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -134,11 +144,62 @@
|
||||
"fieldname": "organization_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "SLA",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sla",
|
||||
"fieldtype": "Link",
|
||||
"label": "SLA",
|
||||
"options": "CRM Service Level Agreement"
|
||||
},
|
||||
{
|
||||
"fieldname": "response_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Response By"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pfvq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "SLA Status",
|
||||
"options": "\nFirst Response Due\nFailed\nFulfilled"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_creation",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "SLA Creation"
|
||||
},
|
||||
{
|
||||
"fieldname": "response_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Response Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hpvj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "first_responded_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "First Responded On"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 11:31:46.968519",
|
||||
"modified": "2023-12-11 12:37:51.198228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -7,10 +7,16 @@ from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMDeal(Document):
|
||||
def before_validate(self):
|
||||
self.set_sla()
|
||||
|
||||
def validate(self):
|
||||
self.set_primary_contact()
|
||||
self.set_primary_email_mobile_no()
|
||||
|
||||
def before_save(self):
|
||||
self.apply_sla()
|
||||
|
||||
def set_primary_contact(self, contact=None):
|
||||
if not self.contacts:
|
||||
return
|
||||
@ -45,6 +51,22 @@ class CRMDeal(Document):
|
||||
self.email = ""
|
||||
self.mobile_no = ""
|
||||
|
||||
def set_sla(self):
|
||||
"""
|
||||
Find an SLA to apply to the deal.
|
||||
"""
|
||||
if sla := get_sla("CRM Deal"):
|
||||
if not sla:
|
||||
return
|
||||
self.sla = sla.name
|
||||
|
||||
def apply_sla(self):
|
||||
"""
|
||||
Apply SLA if set.
|
||||
"""
|
||||
if sla := frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}):
|
||||
sla.apply(self)
|
||||
|
||||
@staticmethod
|
||||
def sort_options():
|
||||
return [
|
||||
@ -113,6 +135,9 @@ class CRMDeal(Document):
|
||||
"email",
|
||||
"mobile_no",
|
||||
"deal_owner",
|
||||
"sla_status",
|
||||
"first_response_time",
|
||||
"first_responded_on",
|
||||
"modified",
|
||||
]
|
||||
return {'columns': columns, 'rows': rows}
|
||||
@ -145,4 +170,10 @@ def set_primary_contact(deal, contact):
|
||||
deal = frappe.get_cached_doc("CRM Deal", deal)
|
||||
deal.set_primary_contact(contact)
|
||||
deal.save()
|
||||
return True
|
||||
return True
|
||||
|
||||
def get_sla(doctype):
|
||||
sla = frappe.db.exists("CRM Service Level Agreement", {"apply_on": doctype, "enabled": 1})
|
||||
if not sla:
|
||||
return None
|
||||
return frappe.get_cached_doc("CRM Service Level Agreement", sla)
|
||||
|
||||
@ -34,7 +34,17 @@
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"image",
|
||||
"converted"
|
||||
"converted",
|
||||
"sla_tab",
|
||||
"sla",
|
||||
"sla_creation",
|
||||
"column_break_ffnp",
|
||||
"sla_status",
|
||||
"response_details_section",
|
||||
"response_by",
|
||||
"column_break_pweh",
|
||||
"first_response_time",
|
||||
"first_responded_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -196,12 +206,63 @@
|
||||
"fieldname": "details",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "SLA",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sla",
|
||||
"fieldtype": "Link",
|
||||
"label": "SLA",
|
||||
"options": "CRM Service Level Agreement"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_creation",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "SLA Creation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ffnp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sla_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "SLA Status",
|
||||
"options": "\nFirst Response Due\nFailed\nFulfilled"
|
||||
},
|
||||
{
|
||||
"fieldname": "response_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Response Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "response_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Response By"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pweh",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "first_responded_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "First Responded On"
|
||||
}
|
||||
],
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 11:31:08.555096",
|
||||
"modified": "2023-12-10 13:54:53.630114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Lead",
|
||||
|
||||
@ -9,12 +9,18 @@ from frappe.utils import has_gravatar, validate_email_address
|
||||
|
||||
|
||||
class CRMLead(Document):
|
||||
def before_validate(self):
|
||||
self.set_sla()
|
||||
|
||||
def validate(self):
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
self.set_title()
|
||||
self.validate_email()
|
||||
|
||||
def before_save(self):
|
||||
self.apply_sla()
|
||||
|
||||
def set_full_name(self):
|
||||
if self.first_name:
|
||||
self.lead_name = " ".join(
|
||||
@ -121,6 +127,22 @@ class CRMLead(Document):
|
||||
deal.insert(ignore_permissions=True)
|
||||
return deal.name
|
||||
|
||||
def set_sla(self):
|
||||
"""
|
||||
Find an SLA to apply to the lead.
|
||||
"""
|
||||
if sla := get_sla("CRM Lead"):
|
||||
if not sla:
|
||||
return
|
||||
self.sla = sla.name
|
||||
|
||||
def apply_sla(self):
|
||||
"""
|
||||
Apply SLA if set.
|
||||
"""
|
||||
if sla := frappe.get_last_doc("CRM Service Level Agreement", {"name": self.sla}):
|
||||
sla.apply(self)
|
||||
|
||||
@staticmethod
|
||||
def sort_options():
|
||||
return [
|
||||
@ -193,6 +215,9 @@ class CRMLead(Document):
|
||||
"mobile_no",
|
||||
"lead_owner",
|
||||
"first_name",
|
||||
"sla_status",
|
||||
"first_response_time",
|
||||
"first_responded_on",
|
||||
"modified",
|
||||
"image",
|
||||
]
|
||||
@ -209,4 +234,10 @@ def convert_to_deal(lead):
|
||||
contact = lead.create_contact(False)
|
||||
deal = lead.create_deal(contact)
|
||||
lead.save()
|
||||
return deal
|
||||
return deal
|
||||
|
||||
def get_sla(doctype):
|
||||
sla = frappe.db.exists("CRM Service Level Agreement", {"apply_on": doctype, "enabled": 1})
|
||||
if not sla:
|
||||
return None
|
||||
return frappe.get_cached_doc("CRM Service Level Agreement", sla)
|
||||
|
||||
0
crm/fcrm/doctype/crm_service_day/__init__.py
Normal file
0
crm/fcrm/doctype/crm_service_day/__init__.py
Normal file
59
crm/fcrm/doctype/crm_service_day/crm_service_day.json
Normal file
59
crm/fcrm/doctype/crm_service_day/crm_service_day.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-12-04 16:07:20.400084",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"workday",
|
||||
"section_break_uegc",
|
||||
"start_time",
|
||||
"column_break_maie",
|
||||
"end_time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "workday",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Workday",
|
||||
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_uegc",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_maie",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "End Time",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-04 16:09:22.928308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Service Day",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
crm/fcrm/doctype/crm_service_day/crm_service_day.py
Normal file
9
crm/fcrm/doctype/crm_service_day/crm_service_day.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMServiceDay(Document):
|
||||
pass
|
||||
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("CRM Service Level Agreement", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
||||
frappe.ui.form.on("CRM Service Level Priority", {
|
||||
priorities_add: function (frm, cdt, cdn) {
|
||||
if (frm.doc.apply_on == "CRM Deal") {
|
||||
frappe.model.set_value(
|
||||
cdt,
|
||||
cdn,
|
||||
"reference_doctype",
|
||||
"CRM Deal Status"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,109 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:sla_name",
|
||||
"creation": "2023-12-04 13:07:18.426211",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sla_name",
|
||||
"apply_on",
|
||||
"enabled",
|
||||
"default",
|
||||
"column_break_uxua",
|
||||
"condition",
|
||||
"section_break_ufaf",
|
||||
"priorities",
|
||||
"section_break_rmgo",
|
||||
"working_hours"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "sla_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "SLA Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Check",
|
||||
"label": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uxua",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Simple Python Expression, Example: doc.status == 'Open' and doc.lead_source == 'Ads'",
|
||||
"fieldname": "condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Condition",
|
||||
"options": "Python"
|
||||
},
|
||||
{
|
||||
"fieldname": "apply_on",
|
||||
"fieldtype": "Link",
|
||||
"label": "Apply On",
|
||||
"link_filters": "[[{\"fieldname\":\"apply_on\",\"field_option\":\"DocType\"},\"name\",\"in\",[\"CRM Lead\",\"CRM Deal\"]]]",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ufaf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "priorities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Priorities",
|
||||
"options": "CRM Service Level Priority",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rmgo",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "working_hours",
|
||||
"fieldtype": "Table",
|
||||
"label": "Working Hours",
|
||||
"options": "CRM Service Day",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-04 16:13:24.638239",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Service Level Agreement",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from datetime import timedelta
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
get_datetime,
|
||||
get_weekdays,
|
||||
getdate,
|
||||
now_datetime,
|
||||
time_diff_in_seconds,
|
||||
)
|
||||
|
||||
|
||||
class CRMServiceLevelAgreement(Document):
|
||||
def apply(self, doc: Document):
|
||||
self.handle_new(doc)
|
||||
self.handle_status(doc)
|
||||
self.handle_targets(doc)
|
||||
self.handle_sla_status(doc)
|
||||
|
||||
def handle_new(self, doc: Document):
|
||||
if not doc.is_new():
|
||||
return
|
||||
creation = doc.sla_creation or now_datetime()
|
||||
doc.sla_creation = creation
|
||||
|
||||
def handle_status(self, doc: Document):
|
||||
if doc.is_new() or not doc.has_value_changed("status"):
|
||||
return
|
||||
self.set_first_responded_on(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):
|
||||
start_at = doc.sla_creation
|
||||
end_at = doc.first_responded_on
|
||||
if not start_at or not end_at:
|
||||
return
|
||||
doc.first_response_time = self.calc_elapsed_time(start_at, end_at)
|
||||
|
||||
def handle_targets(self, doc: Document):
|
||||
self.set_response_by(doc)
|
||||
|
||||
def set_response_by(self, doc: Document):
|
||||
start_time = doc.sla_creation
|
||||
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):
|
||||
is_failed = self.is_first_response_failed(doc)
|
||||
options = {
|
||||
"Fulfilled": True,
|
||||
"First Response Due": not doc.first_responded_on,
|
||||
"Failed": is_failed,
|
||||
}
|
||||
for status in options:
|
||||
if options[status]:
|
||||
doc.sla_status = status
|
||||
|
||||
def is_first_response_failed(self, doc: Document):
|
||||
if not doc.first_responded_on:
|
||||
return get_datetime(doc.response_by) < now_datetime()
|
||||
return get_datetime(doc.response_by) < get_datetime(doc.first_responded_on)
|
||||
|
||||
def calc_time(
|
||||
self,
|
||||
start_at: str,
|
||||
duration_seconds: int,
|
||||
):
|
||||
res = get_datetime(start_at)
|
||||
time_needed = duration_seconds
|
||||
holidays = []
|
||||
weekdays = get_weekdays()
|
||||
workdays = self.get_workdays()
|
||||
while time_needed:
|
||||
today = res
|
||||
today_day = getdate(today)
|
||||
today_weekday = weekdays[today.weekday()]
|
||||
is_workday = today_weekday in workdays
|
||||
is_holiday = today_day in holidays
|
||||
if is_holiday or not is_workday:
|
||||
res = add_to_date(res, days=1, as_datetime=True)
|
||||
continue
|
||||
today_workday = workdays[today_weekday]
|
||||
now_in_seconds = time_diff_in_seconds(today, today_day)
|
||||
start_time = max(today_workday.start_time.total_seconds(), now_in_seconds)
|
||||
till_start_time = max(start_time - now_in_seconds, 0)
|
||||
end_time = max(today_workday.end_time.total_seconds(), now_in_seconds)
|
||||
time_left = max(end_time - start_time, 0)
|
||||
if not time_left:
|
||||
res = getdate(add_to_date(res, days=1, as_datetime=True))
|
||||
continue
|
||||
time_taken = min(time_needed, time_left)
|
||||
time_needed -= time_taken
|
||||
time_required = till_start_time + time_taken
|
||||
res = add_to_date(res, seconds=time_required, as_datetime=True)
|
||||
return res
|
||||
|
||||
def calc_elapsed_time(self, start_time, end_time) -> float:
|
||||
"""
|
||||
Get took from start to end, excluding non-working hours
|
||||
|
||||
:param start_at: Date at which calculation starts
|
||||
:param end_at: Date at which calculation ends
|
||||
:return: Number of seconds
|
||||
"""
|
||||
start_time = get_datetime(start_time)
|
||||
end_time = get_datetime(end_time)
|
||||
holiday_list = []
|
||||
working_day_list = self.get_working_days()
|
||||
working_hours = self.get_working_hours()
|
||||
|
||||
total_seconds = 0
|
||||
current_time = start_time
|
||||
|
||||
while current_time < end_time:
|
||||
in_holiday_list = current_time.date() in holiday_list
|
||||
not_in_working_day_list = get_weekdays()[current_time.weekday()] not in working_day_list
|
||||
if in_holiday_list or not_in_working_day_list or not self.is_working_time(current_time, working_hours):
|
||||
current_time += timedelta(seconds=1)
|
||||
continue
|
||||
total_seconds += 1
|
||||
current_time += timedelta(seconds=1)
|
||||
|
||||
return total_seconds
|
||||
|
||||
def get_priorities(self):
|
||||
"""
|
||||
Return priorities related info as a dict. With `priority` as key
|
||||
"""
|
||||
res = {}
|
||||
for row in self.priorities:
|
||||
res[row.priority] = row
|
||||
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]:
|
||||
"""
|
||||
Return workdays related info as a dict. With `workday` as key
|
||||
"""
|
||||
res = {}
|
||||
for row in self.working_hours:
|
||||
res[row.workday] = row
|
||||
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
|
||||
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCRMServiceLevelAgreement(FrappeTestCase):
|
||||
pass
|
||||
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("CRM Service Level Priority", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-12-04 13:18:58.028384",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"default_priority",
|
||||
"reference_doctype",
|
||||
"priority",
|
||||
"first_response_time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "default_priority",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Default Priority"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Priority",
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"in_list_view": 1,
|
||||
"label": "First Response TIme",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "CRM Lead Status",
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "DocType",
|
||||
"options": "DocType"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-04 14:05:42.838493",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Service Level Priority",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMServiceLevelPriority(Document):
|
||||
pass
|
||||
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCRMServiceLevelPriority(FrappeTestCase):
|
||||
pass
|
||||
@ -1,19 +1,19 @@
|
||||
{
|
||||
"charts": [],
|
||||
"content": "[{\"id\":\"1nr6UkvDiL\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h5\\\"><b>PORTAL</b></span>\",\"col\":12}},{\"id\":\"1hyi8SysUY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Portal Page\",\"col\":3}},{\"id\":\"ktENiGaqXQ\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"VgeWLYOuAS\",\"type\":\"paragraph\",\"data\":{\"text\":\"<b>SHORTCUTS</b>\",\"col\":12}},{\"id\":\"A66FpG-K3T\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leads\",\"col\":3}},{\"id\":\"n9b6N5ebOj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deals\",\"col\":3}},{\"id\":\"sGHTXrludH\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Organizations\",\"col\":3}},{\"id\":\"uXZNCdqxy0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Contacts\",\"col\":3}},{\"id\":\"TZ7cULX3Tk\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"zpySv0nGVQ\",\"type\":\"paragraph\",\"data\":{\"text\":\"<b>META</b>\",\"col\":12}},{\"id\":\"fa-uKzobpp\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Statuses\",\"col\":3}},{\"id\":\"hxoZghUHP2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deal Statuses\",\"col\":3}},{\"id\":\"HbgghUpc8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Sources\",\"col\":3}},{\"id\":\"8cPs7Fohb4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Industries\",\"col\":3}}]",
|
||||
"content": "[{\"id\":\"1nr6UkvDiL\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h5\\\"><b>PORTAL</b></span>\",\"col\":12}},{\"id\":\"1hyi8SysUY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"CRM Portal Page\",\"col\":3}},{\"id\":\"ktENiGaqXQ\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"VgeWLYOuAS\",\"type\":\"paragraph\",\"data\":{\"text\":\"<b>SHORTCUTS</b>\",\"col\":12}},{\"id\":\"A66FpG-K3T\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leads\",\"col\":3}},{\"id\":\"n9b6N5ebOj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deals\",\"col\":3}},{\"id\":\"sGHTXrludH\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Organizations\",\"col\":3}},{\"id\":\"uXZNCdqxy0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Contacts\",\"col\":3}},{\"id\":\"v1kkMwlntf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"SLA\",\"col\":3}},{\"id\":\"TZ7cULX3Tk\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"zpySv0nGVQ\",\"type\":\"paragraph\",\"data\":{\"text\":\"<b>META</b>\",\"col\":12}},{\"id\":\"fa-uKzobpp\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Statuses\",\"col\":3}},{\"id\":\"hxoZghUHP2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Deal Statuses\",\"col\":3}},{\"id\":\"HbgghUpc8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead Sources\",\"col\":3}},{\"id\":\"8cPs7Fohb4\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Industries\",\"col\":3}}]",
|
||||
"creation": "2023-11-27 13:55:17.090361",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "crm",
|
||||
"icon": "filter",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
"label": "Frappe CRM",
|
||||
"links": [],
|
||||
"modified": "2023-11-29 13:37:56.731645",
|
||||
"modified": "2023-12-11 13:06:13.532693",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "Frappe CRM",
|
||||
@ -40,6 +40,14 @@
|
||||
"type": "URL",
|
||||
"url": "/crm"
|
||||
},
|
||||
{
|
||||
"color": "Grey",
|
||||
"doc_view": "List",
|
||||
"label": "SLA",
|
||||
"link_to": "CRM Service Level Agreement",
|
||||
"stats_filter": "[]",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"color": "Grey",
|
||||
"doc_view": "List",
|
||||
|
||||
@ -44,9 +44,31 @@
|
||||
<PhoneIcon class="h-4 w-4" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="['modified', 'creation'].includes(column.key)" class="truncate text-base">
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
].includes(column.key)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
{{ item.timeAgo }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'sla_status'"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Badge
|
||||
v-if="item.label"
|
||||
:variant="'subtle'"
|
||||
:theme="item.color"
|
||||
size="md"
|
||||
:label="item.label"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="column.type === 'Check'">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
@ -74,6 +96,7 @@ import {
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
FormControl,
|
||||
Badge,
|
||||
} from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -53,9 +53,31 @@
|
||||
<PhoneIcon class="h-4 w-4" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="['modified', 'creation'].includes(column.key)" class="truncate text-base">
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
'modified',
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
].includes(column.key)
|
||||
"
|
||||
class="truncate text-base"
|
||||
>
|
||||
{{ item.timeAgo }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'sla_status'"
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Badge
|
||||
v-if="item.label"
|
||||
:variant="'subtle'"
|
||||
:theme="item.color"
|
||||
size="md"
|
||||
:label="item.label"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="column.type === 'Check'">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
@ -83,6 +105,7 @@ import {
|
||||
ListSelectBanner,
|
||||
ListRowItem,
|
||||
FormControl,
|
||||
Badge,
|
||||
} from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@ -153,8 +153,8 @@ const allFields = [
|
||||
},
|
||||
{
|
||||
label: 'Deal Owner',
|
||||
name: 'lead_owner',
|
||||
type: 'link',
|
||||
name: 'deal_owner',
|
||||
type: 'user',
|
||||
placeholder: 'Deal Owner',
|
||||
},
|
||||
],
|
||||
|
||||
@ -94,6 +94,72 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="deal.data.sla_status" class="flex flex-col gap-2 border-b p-5">
|
||||
<div
|
||||
v-if="deal.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(deal.data.response_by, 'ddd, MMM D, YYYY h:mm A')"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{{ timeAgo(deal.data.response_by) }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-if="deal.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(
|
||||
deal.data.first_responded_on,
|
||||
'ddd, MMM D, YYYY h:mm A'
|
||||
)
|
||||
"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{{ formatTime(deal.data.first_response_time) }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
deal.data.sla_status == 'Failed' && deal.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(
|
||||
deal.data.first_responded_on,
|
||||
'ddd, MMM D, YYYY h:mm A'
|
||||
)
|
||||
"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
{{ formatTime(deal.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="deal.data.sla_status"
|
||||
variant="outline"
|
||||
:theme="
|
||||
deal.data.sla_status === 'Failed'
|
||||
? 'red'
|
||||
: deal.data.sla_status === 'Fulfilled'
|
||||
? 'green'
|
||||
: 'gray'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div
|
||||
@ -275,7 +341,14 @@ import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Section from '@/components/Section.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 { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
@ -313,6 +386,15 @@ const deal = createResource({
|
||||
params: { name: props.dealId },
|
||||
cache: ['deal', props.dealId],
|
||||
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)
|
||||
|
||||
@ -71,6 +71,7 @@ import {
|
||||
dateTooltipFormat,
|
||||
timeAgo,
|
||||
formatNumberIntoCurrency,
|
||||
formatTime,
|
||||
} from '@/utils'
|
||||
import {
|
||||
FeatherIcon,
|
||||
@ -154,6 +155,16 @@ const rows = computed(() => {
|
||||
label: deal.status,
|
||||
color: getDealStatus(deal.status)?.iconColorClass,
|
||||
}
|
||||
} else if (row == 'sla_status') {
|
||||
_rows[row] = {
|
||||
label: deal.sla_status,
|
||||
color:
|
||||
deal.sla_status === 'Failed'
|
||||
? 'red'
|
||||
: deal.sla_status === 'Fulfilled'
|
||||
? 'green'
|
||||
: 'gray',
|
||||
}
|
||||
} else if (row == 'deal_owner') {
|
||||
_rows[row] = {
|
||||
label: deal.deal_owner && getUser(deal.deal_owner).full_name,
|
||||
@ -164,6 +175,17 @@ const rows = computed(() => {
|
||||
label: dateFormat(deal[row], dateTooltipFormat),
|
||||
timeAgo: timeAgo(deal[row]),
|
||||
}
|
||||
} else if (['first_response_time', 'first_responded_on'].includes(row)) {
|
||||
_rows[row] = {
|
||||
label: deal.first_responded_on
|
||||
? dateFormat(deal.first_responded_on, dateTooltipFormat)
|
||||
: '',
|
||||
timeAgo: deal[row]
|
||||
? row == 'first_responded_on'
|
||||
? timeAgo(deal[row])
|
||||
: formatTime(deal[row])
|
||||
: '',
|
||||
}
|
||||
}
|
||||
})
|
||||
return _rows
|
||||
|
||||
@ -134,6 +134,72 @@
|
||||
</div>
|
||||
</template>
|
||||
</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-col overflow-y-auto">
|
||||
<div
|
||||
@ -181,7 +247,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import Section from '@/components/Section.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 { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
@ -197,6 +270,7 @@ import {
|
||||
Avatar,
|
||||
Tabs,
|
||||
Breadcrumbs,
|
||||
Badge,
|
||||
call,
|
||||
} from 'frappe-ui'
|
||||
import { ref, computed } from 'vue'
|
||||
@ -220,6 +294,15 @@ const lead = createResource({
|
||||
params: { name: props.leadId },
|
||||
cache: ['lead', props.leadId],
|
||||
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)
|
||||
|
||||
@ -65,7 +65,7 @@ import { statusesStore } from '@/stores/statuses'
|
||||
import { useOrderBy } from '@/composables/orderby'
|
||||
import { useFilter } from '@/composables/filter'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
||||
import { dateFormat, dateTooltipFormat, timeAgo, formatTime } from '@/utils'
|
||||
import {
|
||||
FeatherIcon,
|
||||
Dialog,
|
||||
@ -154,6 +154,16 @@ const rows = computed(() => {
|
||||
label: lead.status,
|
||||
color: getLeadStatus(lead.status)?.iconColorClass,
|
||||
}
|
||||
} else if (row == 'sla_status') {
|
||||
_rows[row] = {
|
||||
label: lead.sla_status,
|
||||
color:
|
||||
lead.sla_status === 'Failed'
|
||||
? 'red'
|
||||
: lead.sla_status === 'Fulfilled'
|
||||
? 'green'
|
||||
: 'gray',
|
||||
}
|
||||
} else if (row == 'lead_owner') {
|
||||
_rows[row] = {
|
||||
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
|
||||
@ -164,6 +174,17 @@ const rows = computed(() => {
|
||||
label: dateFormat(lead[row], dateTooltipFormat),
|
||||
timeAgo: timeAgo(lead[row]),
|
||||
}
|
||||
} else if (['first_response_time', 'first_responded_on'].includes(row)) {
|
||||
_rows[row] = {
|
||||
label: lead.first_responded_on
|
||||
? dateFormat(lead.first_responded_on, dateTooltipFormat)
|
||||
: '',
|
||||
timeAgo: lead[row]
|
||||
? row == 'first_responded_on'
|
||||
? timeAgo(lead[row])
|
||||
: formatTime(lead[row])
|
||||
: '',
|
||||
}
|
||||
}
|
||||
})
|
||||
return _rows
|
||||
|
||||
@ -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) {
|
||||
const _format = format || 'DD-MM-YYYY HH:mm:ss'
|
||||
return useDateFormat(date, _format).value
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user