Merge pull request #39 from shariquerik/sla-1
This commit is contained in:
commit
0c08ba2300
@ -125,7 +125,6 @@ def get_list_data(doctype: str, filters: dict, order_by: str):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctype_fields(doctype):
|
||||
not_allowed_fieldtypes = [
|
||||
"Section Break",
|
||||
|
||||
@ -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 Communication Status", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:status",
|
||||
"creation": "2023-12-13 13:25:07.213100",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-13 13:28:38.746199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Communication Status",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"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 CRMCommunicationStatus(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 TestCRMCommunicationStatus(FrappeTestCase):
|
||||
pass
|
||||
@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_doctype_fields
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_deal(name):
|
||||
@ -25,4 +26,5 @@ def get_deal(name):
|
||||
fields=["contact", "is_primary"],
|
||||
)
|
||||
|
||||
deal["doctype_fields"] = get_doctype_fields("CRM Deal")
|
||||
return deal
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
"sla_creation",
|
||||
"column_break_pfvq",
|
||||
"sla_status",
|
||||
"communication_status",
|
||||
"response_details_section",
|
||||
"response_by",
|
||||
"column_break_hpvj",
|
||||
@ -195,11 +196,18 @@
|
||||
"fieldname": "first_responded_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "First Responded On"
|
||||
},
|
||||
{
|
||||
"default": "Open",
|
||||
"fieldname": "communication_status",
|
||||
"fieldtype": "Link",
|
||||
"label": "Communication Status",
|
||||
"options": "CRM Communication Status"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-11 12:37:51.198228",
|
||||
"modified": "2023-12-13 13:50:55.235109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -5,6 +5,8 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||
|
||||
|
||||
class CRMDeal(Document):
|
||||
def before_validate(self):
|
||||
@ -55,7 +57,7 @@ class CRMDeal(Document):
|
||||
"""
|
||||
Find an SLA to apply to the deal.
|
||||
"""
|
||||
sla = get_sla("CRM Deal")
|
||||
sla = get_sla(self)
|
||||
if not sla:
|
||||
return
|
||||
self.sla = sla.name
|
||||
@ -139,6 +141,7 @@ class CRMDeal(Document):
|
||||
"mobile_no",
|
||||
"deal_owner",
|
||||
"sla_status",
|
||||
"response_by",
|
||||
"first_response_time",
|
||||
"first_responded_on",
|
||||
"modified",
|
||||
@ -175,8 +178,3 @@ def set_primary_contact(deal, contact):
|
||||
deal.save()
|
||||
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)
|
||||
|
||||
0
crm/fcrm/doctype/crm_holiday/__init__.py
Normal file
0
crm/fcrm/doctype/crm_holiday/__init__.py
Normal file
57
crm/fcrm/doctype/crm_holiday/crm_holiday.json
Normal file
57
crm/fcrm/doctype/crm_holiday/crm_holiday.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-12-14 11:16:15.476366",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"date",
|
||||
"column_break_xzyo",
|
||||
"weekly_off",
|
||||
"section_break_zenz",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xzyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "weekly_off",
|
||||
"fieldtype": "Check",
|
||||
"label": "Weekly Off"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_zenz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-14 11:17:41.745419",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Holiday",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
crm/fcrm/doctype/crm_holiday/crm_holiday.py
Normal file
9
crm/fcrm/doctype/crm_holiday/crm_holiday.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 CRMHoliday(Document):
|
||||
pass
|
||||
0
crm/fcrm/doctype/crm_holiday_list/__init__.py
Normal file
0
crm/fcrm/doctype/crm_holiday_list/__init__.py
Normal file
8
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.js
Normal file
8
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.js
Normal file
@ -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 Holiday List", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
111
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json
Normal file
111
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:holiday_list_name",
|
||||
"creation": "2023-12-14 11:09:12.876640",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"holiday_list_name",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"column_break_qwqc",
|
||||
"total_holidays",
|
||||
"add_weekly_holidays_section",
|
||||
"weekly_off",
|
||||
"add_to_holidays",
|
||||
"holidays_section",
|
||||
"holidays",
|
||||
"clear_table"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "holiday_list_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Holiday List Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "From Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "To Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qwqc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_holidays",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Holidays"
|
||||
},
|
||||
{
|
||||
"fieldname": "add_weekly_holidays_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Add Weekly Holidays"
|
||||
},
|
||||
{
|
||||
"fieldname": "weekly_off",
|
||||
"fieldtype": "Select",
|
||||
"label": "Weekly Off",
|
||||
"options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday"
|
||||
},
|
||||
{
|
||||
"fieldname": "add_to_holidays",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add to Holidays"
|
||||
},
|
||||
{
|
||||
"fieldname": "holidays_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Holidays"
|
||||
},
|
||||
{
|
||||
"fieldname": "clear_table",
|
||||
"fieldtype": "Button",
|
||||
"label": "Clear Table"
|
||||
},
|
||||
{
|
||||
"fieldname": "holidays",
|
||||
"fieldtype": "Table",
|
||||
"label": "Holidays",
|
||||
"options": "CRM Holiday"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-14 11:18:27.236817",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Holiday List",
|
||||
"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": []
|
||||
}
|
||||
9
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.py
Normal file
9
crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.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 CRMHolidayList(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 TestCRMHolidayList(FrappeTestCase):
|
||||
pass
|
||||
@ -1,6 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from crm.api.doc import get_doctype_fields
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead(name):
|
||||
@ -13,4 +14,5 @@ def get_lead(name):
|
||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||
lead = lead.pop()
|
||||
|
||||
lead["doctype_fields"] = get_doctype_fields("CRM Lead")
|
||||
return lead
|
||||
@ -40,6 +40,7 @@
|
||||
"sla_creation",
|
||||
"column_break_ffnp",
|
||||
"sla_status",
|
||||
"communication_status",
|
||||
"response_details_section",
|
||||
"response_by",
|
||||
"column_break_pweh",
|
||||
@ -257,12 +258,19 @@
|
||||
"fieldname": "first_responded_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "First Responded On"
|
||||
},
|
||||
{
|
||||
"default": "Open",
|
||||
"fieldname": "communication_status",
|
||||
"fieldtype": "Link",
|
||||
"label": "Communication Status",
|
||||
"options": "CRM Communication Status"
|
||||
}
|
||||
],
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-10 13:54:53.630114",
|
||||
"modified": "2023-12-13 13:50:40.055487",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Lead",
|
||||
|
||||
@ -6,6 +6,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from frappe.utils import has_gravatar, validate_email_address
|
||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||
|
||||
|
||||
class CRMLead(Document):
|
||||
@ -124,6 +125,19 @@ class CRMLead(Document):
|
||||
"contacts": [{"contact": contact}],
|
||||
}
|
||||
)
|
||||
|
||||
if self.first_responded_on:
|
||||
deal.update(
|
||||
{
|
||||
"sla_creation": self.sla_creation,
|
||||
"response_by": self.response_by,
|
||||
"sla_status": self.sla_status,
|
||||
"communication_status": self.communication_status,
|
||||
"first_response_time": self.first_response_time,
|
||||
"first_responded_on": self.first_responded_on
|
||||
}
|
||||
)
|
||||
|
||||
deal.insert(ignore_permissions=True)
|
||||
return deal.name
|
||||
|
||||
@ -131,7 +145,7 @@ class CRMLead(Document):
|
||||
"""
|
||||
Find an SLA to apply to the lead.
|
||||
"""
|
||||
sla = get_sla("CRM Lead")
|
||||
sla = get_sla(self)
|
||||
if not sla:
|
||||
return
|
||||
self.sla = sla.name
|
||||
@ -219,6 +233,7 @@ class CRMLead(Document):
|
||||
"lead_owner",
|
||||
"first_name",
|
||||
"sla_status",
|
||||
"response_by",
|
||||
"first_response_time",
|
||||
"first_responded_on",
|
||||
"modified",
|
||||
@ -239,8 +254,3 @@ def convert_to_deal(lead):
|
||||
lead.save()
|
||||
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)
|
||||
|
||||
@ -6,16 +6,3 @@
|
||||
|
||||
// },
|
||||
// });
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -6,15 +6,20 @@
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sla_name",
|
||||
"apply_on",
|
||||
"column_break_uxua",
|
||||
"sla_name",
|
||||
"enabled",
|
||||
"default",
|
||||
"column_break_uxua",
|
||||
"section_break_nevd",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"column_break_pzjg",
|
||||
"condition",
|
||||
"section_break_ufaf",
|
||||
"priorities",
|
||||
"section_break_rmgo",
|
||||
"holiday_list",
|
||||
"working_hours"
|
||||
],
|
||||
"fields": [
|
||||
@ -60,7 +65,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ufaf",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Response and Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "priorities",
|
||||
@ -71,7 +77,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rmgo",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Working Hours"
|
||||
},
|
||||
{
|
||||
"fieldname": "working_hours",
|
||||
@ -79,11 +86,36 @@
|
||||
"label": "Working Hours",
|
||||
"options": "CRM Service Day",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_nevd",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validity"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pzjg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "End Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "holiday_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Holiday List",
|
||||
"options": "CRM Holiday List"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-04 16:13:24.638239",
|
||||
"modified": "2023-12-15 11:50:29.956775",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Service Level Agreement",
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
@ -12,12 +13,27 @@ from frappe.utils import (
|
||||
now_datetime,
|
||||
time_diff_in_seconds,
|
||||
)
|
||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_context
|
||||
|
||||
|
||||
class CRMServiceLevelAgreement(Document):
|
||||
def validate(self):
|
||||
self.validate_condition()
|
||||
|
||||
def validate_condition(self):
|
||||
if not self.condition:
|
||||
return
|
||||
try:
|
||||
temp_doc = frappe.new_doc(self.apply_on)
|
||||
frappe.safe_eval(self.condition, None, get_context(temp_doc))
|
||||
except Exception as e:
|
||||
frappe.throw(
|
||||
_("The Condition '{0}' is invalid: {1}").format(self.condition, str(e))
|
||||
)
|
||||
|
||||
def apply(self, doc: Document):
|
||||
self.handle_new(doc)
|
||||
self.handle_status(doc)
|
||||
self.handle_communication_status(doc)
|
||||
self.handle_targets(doc)
|
||||
self.handle_sla_status(doc)
|
||||
|
||||
@ -27,14 +43,14 @@ class CRMServiceLevelAgreement(Document):
|
||||
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"):
|
||||
def handle_communication_status(self, doc: Document):
|
||||
if doc.is_new() or not doc.has_value_changed("communication_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():
|
||||
if doc.communication_status != self.get_default_priority():
|
||||
doc.first_responded_on = (
|
||||
doc.first_responded_on or now_datetime()
|
||||
)
|
||||
@ -51,10 +67,10 @@ class CRMServiceLevelAgreement(Document):
|
||||
|
||||
def set_response_by(self, doc: Document):
|
||||
start_time = doc.sla_creation
|
||||
status = doc.status
|
||||
communication_status = doc.communication_status
|
||||
|
||||
priorities = self.get_priorities()
|
||||
priority = priorities.get(status)
|
||||
priority = priorities.get(communication_status)
|
||||
if not priority or doc.response_by:
|
||||
return
|
||||
|
||||
|
||||
62
crm/fcrm/doctype/crm_service_level_agreement/utils.py
Normal file
62
crm/fcrm/doctype/crm_service_level_agreement/utils.py
Normal file
@ -0,0 +1,62 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import JoinType
|
||||
from frappe.utils.safe_exec import get_safe_globals
|
||||
|
||||
DOCTYPE = "CRM Service Level Agreement"
|
||||
|
||||
def get_sla(doc: Document) -> Document:
|
||||
"""
|
||||
Get Service Level Agreement for `doc`
|
||||
|
||||
:param doc: Lead/Deal to use
|
||||
:return: Applicable SLA
|
||||
"""
|
||||
check_permissions(DOCTYPE, None)
|
||||
SLA = frappe.qb.DocType(DOCTYPE)
|
||||
Priority = frappe.qb.DocType("CRM Service Level Priority")
|
||||
priority = doc.communication_status
|
||||
q = (
|
||||
frappe.qb.from_(SLA)
|
||||
.select(SLA.name, SLA.condition)
|
||||
.where(SLA.apply_on == doc.doctype)
|
||||
.where(SLA.enabled == True)
|
||||
)
|
||||
if priority:
|
||||
q = (
|
||||
q.join(Priority, JoinType.inner)
|
||||
.on(Priority.parent == SLA.name)
|
||||
.where(Priority.priority == priority)
|
||||
)
|
||||
sla_list = q.run(as_dict=True)
|
||||
res = None
|
||||
for sla in sla_list:
|
||||
cond = sla.get("condition")
|
||||
if not cond or frappe.safe_eval(cond, None, get_context(doc)):
|
||||
res = sla
|
||||
break
|
||||
return res
|
||||
|
||||
def check_permissions(doctype, parent):
|
||||
user = frappe.session.user
|
||||
permissions = ("select", "read")
|
||||
has_select_permission, has_read_permission = [
|
||||
frappe.has_permission(doctype, perm, user=user, parent_doctype=parent)
|
||||
for perm in permissions
|
||||
]
|
||||
|
||||
if not has_select_permission and not has_read_permission:
|
||||
frappe.throw(f"Insufficient Permission for {doctype}", frappe.PermissionError)
|
||||
|
||||
def get_context(d: Document) -> dict:
|
||||
"""
|
||||
Get safe context for `safe_eval`
|
||||
|
||||
:param doc: `Document` to add in context
|
||||
:return: Context with `doc` and safe variables
|
||||
"""
|
||||
utils = get_safe_globals().get("frappe").get("utils")
|
||||
return {
|
||||
"doc": d.as_dict(),
|
||||
"frappe": frappe._dict(utils=utils),
|
||||
}
|
||||
@ -7,9 +7,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"default_priority",
|
||||
"reference_doctype",
|
||||
"column_break_grod",
|
||||
"priority",
|
||||
"first_response_time"
|
||||
"section_break_anyl",
|
||||
"first_response_time",
|
||||
"column_break_bwgs"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -21,10 +23,10 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Priority",
|
||||
"options": "reference_doctype",
|
||||
"options": "CRM Communication Status",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -35,17 +37,22 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "CRM Lead Status",
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "DocType",
|
||||
"options": "DocType"
|
||||
"fieldname": "column_break_grod",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_anyl",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bwgs",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-04 14:05:42.838493",
|
||||
"modified": "2023-12-15 11:49:54.424029",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Service Level Priority",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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\":\"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}}]",
|
||||
"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}},{\"id\":\"ApHOcISpiJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Communication Statuses\",\"col\":3}}]",
|
||||
"creation": "2023-11-27 13:55:17.090361",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@ -13,7 +13,7 @@
|
||||
"is_hidden": 0,
|
||||
"label": "Frappe CRM",
|
||||
"links": [],
|
||||
"modified": "2023-12-11 13:06:13.532693",
|
||||
"modified": "2023-12-13 19:59:33.129412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "Frappe CRM",
|
||||
@ -25,6 +25,14 @@
|
||||
"roles": [],
|
||||
"sequence_id": 1.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"color": "Grey",
|
||||
"doc_view": "List",
|
||||
"label": "Communication Statuses",
|
||||
"link_to": "CRM Communication Status",
|
||||
"stats_filter": "[]",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"color": "Grey",
|
||||
"doc_view": "List",
|
||||
|
||||
@ -10,11 +10,12 @@ def before_install():
|
||||
def after_install():
|
||||
add_default_lead_statuses()
|
||||
add_default_deal_statuses()
|
||||
add_default_communication_statuses()
|
||||
frappe.db.commit()
|
||||
|
||||
def add_default_lead_statuses():
|
||||
statuses = {
|
||||
"Open": {
|
||||
"New": {
|
||||
"color": "gray",
|
||||
"position": 1,
|
||||
},
|
||||
@ -90,4 +91,15 @@ def add_default_deal_statuses():
|
||||
doc.deal_status = status
|
||||
doc.color = statuses[status]["color"]
|
||||
doc.position = statuses[status]["position"]
|
||||
doc.insert()
|
||||
doc.insert()
|
||||
|
||||
def add_default_communication_statuses():
|
||||
statuses = ["Open", "Replied"]
|
||||
|
||||
for status in statuses:
|
||||
if frappe.db.exists("CRM Communication Status", status):
|
||||
continue
|
||||
|
||||
doc = frappe.new_doc("CRM Communication Status")
|
||||
doc.status = status
|
||||
doc.insert()
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(column.key)
|
||||
"
|
||||
class="truncate text-base"
|
||||
@ -62,11 +63,11 @@
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Badge
|
||||
v-if="item.label"
|
||||
v-if="item.value"
|
||||
:variant="'subtle'"
|
||||
:theme="item.color"
|
||||
size="md"
|
||||
:label="item.label"
|
||||
:label="item.value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="column.type === 'Check'">
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
'creation',
|
||||
'first_response_time',
|
||||
'first_responded_on',
|
||||
'response_by',
|
||||
].includes(column.key)
|
||||
"
|
||||
class="truncate text-base"
|
||||
@ -71,11 +72,11 @@
|
||||
class="truncate text-base"
|
||||
>
|
||||
<Badge
|
||||
v-if="item.label"
|
||||
v-if="item.value"
|
||||
:variant="'subtle'"
|
||||
:theme="item.color"
|
||||
size="md"
|
||||
:label="item.label"
|
||||
:label="item.value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="column.type === 'Check'">
|
||||
|
||||
@ -338,7 +338,7 @@ const sections = computed(() => {
|
||||
fields: [
|
||||
{
|
||||
label: 'Email',
|
||||
type: 'dropdown',
|
||||
type: props.contact.name ? 'dropdown' : 'data',
|
||||
name: 'email_id',
|
||||
options: props.contact?.email_ids?.map((email) => {
|
||||
return {
|
||||
@ -374,7 +374,7 @@ const sections = computed(() => {
|
||||
fields: [
|
||||
{
|
||||
label: 'Mobile No.',
|
||||
type: 'dropdown',
|
||||
type: props.contact.name ? 'dropdown' : 'data',
|
||||
name: 'mobile_no',
|
||||
options: props.contact?.phone_nos?.map((phone) => {
|
||||
return {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{
|
||||
label: editMode ? 'Update' : 'Create',
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) => updateNote(close),
|
||||
onClick: () => updateNote(),
|
||||
},
|
||||
],
|
||||
}"
|
||||
@ -67,7 +67,7 @@ const title = ref(null)
|
||||
const editMode = ref(false)
|
||||
let _note = ref({})
|
||||
|
||||
async function updateNote(close) {
|
||||
async function updateNote() {
|
||||
if (
|
||||
props.note.title === _note.value.title &&
|
||||
props.note.content === _note.value.content
|
||||
@ -97,7 +97,7 @@ async function updateNote(close) {
|
||||
notes.value.reload()
|
||||
}
|
||||
}
|
||||
close()
|
||||
show.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
{
|
||||
label: editMode ? 'Update' : 'Create',
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) => updateTask(close),
|
||||
onClick: () => updateTask(),
|
||||
},
|
||||
],
|
||||
}"
|
||||
@ -142,7 +142,7 @@ function updateTaskPriority(priority) {
|
||||
_task.value.priority = priority
|
||||
}
|
||||
|
||||
async function updateTask(close) {
|
||||
async function updateTask() {
|
||||
if (!_task.value.assigned_to) {
|
||||
_task.value.assigned_to = getUser().email
|
||||
}
|
||||
@ -168,7 +168,7 @@ async function updateTask(close) {
|
||||
tasks.value.reload()
|
||||
}
|
||||
}
|
||||
close()
|
||||
show.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@ -10,10 +10,8 @@
|
||||
:options="field.options"
|
||||
v-model="newDeal[field.name]"
|
||||
>
|
||||
<template v-if="field.name == 'status'" #prefix>
|
||||
<IndicatorIcon
|
||||
:class="getDealStatus(newDeal[field.name]).iconColorClass"
|
||||
/>
|
||||
<template v-if="field.prefix" #prefix>
|
||||
<IndicatorIcon :class="field.prefix" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
@ -73,7 +71,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { FormControl, Tooltip } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { getDealStatus, statusOptions } = statusesStore()
|
||||
@ -88,82 +86,83 @@ const props = defineProps({
|
||||
const showOrganizationModal = ref(false)
|
||||
const _organization = ref({})
|
||||
|
||||
const allFields = [
|
||||
{
|
||||
section: 'Deal Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
name: 'salutation',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: 'Mr',
|
||||
value: 'Mr',
|
||||
},
|
||||
{
|
||||
label: 'Ms',
|
||||
value: 'Ms',
|
||||
},
|
||||
{
|
||||
label: 'Mrs',
|
||||
value: 'Mrs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Mobile No',
|
||||
name: 'mobile_no',
|
||||
type: 'data',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Other Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
name: 'organization',
|
||||
type: 'link',
|
||||
placeholder: 'Organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (data) => (props.newDeal.organization = data),
|
||||
create: (value, close) => {
|
||||
_organization.value.organization_name = value
|
||||
showOrganizationModal.value = true
|
||||
close()
|
||||
const allFields = computed(() => {
|
||||
return [
|
||||
{
|
||||
section: 'Deal Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
name: 'salutation',
|
||||
type: 'link',
|
||||
doctype: 'Salutation',
|
||||
placeholder: 'Salutation',
|
||||
change: (data) => (props.newDeal.salutation = data),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions('deal'),
|
||||
},
|
||||
{
|
||||
label: 'Deal Owner',
|
||||
name: 'deal_owner',
|
||||
type: 'user',
|
||||
placeholder: 'Deal Owner',
|
||||
doctype: 'User',
|
||||
change: (data) => (props.newDeal.deal_owner = data),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
label: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Mobile No',
|
||||
name: 'mobile_no',
|
||||
type: 'data',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Other Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
name: 'organization',
|
||||
type: 'link',
|
||||
placeholder: 'Organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (data) => (props.newDeal.organization = data),
|
||||
create: (value, close) => {
|
||||
_organization.value.organization_name = value
|
||||
showOrganizationModal.value = true
|
||||
close()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions(
|
||||
'deal',
|
||||
(field, value) => (props.newDeal[field] = value)
|
||||
),
|
||||
prefix: getDealStatus(props.newDeal.status).iconColorClass,
|
||||
},
|
||||
{
|
||||
label: 'Deal Owner',
|
||||
name: 'deal_owner',
|
||||
type: 'user',
|
||||
placeholder: 'Deal Owner',
|
||||
doctype: 'User',
|
||||
change: (data) => (props.newDeal.deal_owner = data),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.newDeal.status) {
|
||||
props.newDeal.status = getDealStatus(props.newDeal.status).name
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -10,10 +10,8 @@
|
||||
:options="field.options"
|
||||
v-model="newLead[field.name]"
|
||||
>
|
||||
<template v-if="field.name == 'status'" #prefix>
|
||||
<IndicatorIcon
|
||||
:class="getLeadStatus(newLead[field.name]).iconColorClass"
|
||||
/>
|
||||
<template v-if="field.prefix" #prefix>
|
||||
<IndicatorIcon :class="field.prefix" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
@ -73,7 +71,7 @@ import Link from '@/components/Controls/Link.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { FormControl, Tooltip } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const { getUser } = usersStore()
|
||||
const { getLeadStatus, statusOptions } = statusesStore()
|
||||
@ -88,82 +86,83 @@ const props = defineProps({
|
||||
const showOrganizationModal = ref(false)
|
||||
const _organization = ref({})
|
||||
|
||||
const allFields = [
|
||||
{
|
||||
section: 'Lead Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
name: 'salutation',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: 'Mr',
|
||||
value: 'Mr',
|
||||
},
|
||||
{
|
||||
label: 'Ms',
|
||||
value: 'Ms',
|
||||
},
|
||||
{
|
||||
label: 'Mrs',
|
||||
value: 'Mrs',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Mobile No',
|
||||
name: 'mobile_no',
|
||||
type: 'data',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Other Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
name: 'organization',
|
||||
type: 'link',
|
||||
placeholder: 'Organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (data) => (props.newLead.organization = data),
|
||||
create: (value, close) => {
|
||||
_organization.value.organization_name = value
|
||||
showOrganizationModal.value = true
|
||||
close()
|
||||
const allFields = computed(() => {
|
||||
return [
|
||||
{
|
||||
section: 'Lead Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
name: 'salutation',
|
||||
type: 'link',
|
||||
placeholder: 'Salutation',
|
||||
doctype: 'Salutation',
|
||||
change: (data) => (props.newLead.salutation = data),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions('lead'),
|
||||
},
|
||||
{
|
||||
label: 'Lead Owner',
|
||||
name: 'lead_owner',
|
||||
type: 'user',
|
||||
placeholder: 'Lead Owner',
|
||||
doctype: 'User',
|
||||
change: (data) => (props.newLead.lead_owner = data),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
label: 'First Name',
|
||||
name: 'first_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
name: 'last_name',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
name: 'email',
|
||||
type: 'data',
|
||||
},
|
||||
{
|
||||
label: 'Mobile No',
|
||||
name: 'mobile_no',
|
||||
type: 'data',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Other Details',
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
name: 'organization',
|
||||
type: 'link',
|
||||
placeholder: 'Organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (data) => (props.newLead.organization = data),
|
||||
create: (value, close) => {
|
||||
_organization.value.organization_name = value
|
||||
showOrganizationModal.value = true
|
||||
close()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions(
|
||||
'lead',
|
||||
(field, value) => (props.newLead[field] = value)
|
||||
),
|
||||
prefix: getLeadStatus(props.newLead.status).iconColorClass,
|
||||
},
|
||||
{
|
||||
label: 'Lead Owner',
|
||||
name: 'lead_owner',
|
||||
type: 'user',
|
||||
placeholder: 'Lead Owner',
|
||||
doctype: 'User',
|
||||
change: (data) => (props.newLead.lead_owner = data),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.newLead.status) {
|
||||
props.newLead.status = getLeadStatus(props.newLead.status).name
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
114
frontend/src/components/SLASection.vue
Normal file
114
frontend/src/components/SLASection.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1.5 border-b px-6 py-3">
|
||||
<div
|
||||
v-for="s in slaSection"
|
||||
:key="s.label"
|
||||
class="flex items-center gap-2 text-base leading-5"
|
||||
>
|
||||
<div class="w-[106px] text-sm text-gray-600">{{ s.label }}</div>
|
||||
<div class="grid min-h-[28px] items-center">
|
||||
<Tooltip
|
||||
v-if="s.tooltipText"
|
||||
:text="s.tooltipText"
|
||||
class="ml-2 cursor-pointer"
|
||||
>
|
||||
<Badge
|
||||
v-if="s.type == 'Badge'"
|
||||
class="-ml-1"
|
||||
:label="s.value"
|
||||
variant="subtle"
|
||||
:theme="s.color"
|
||||
/>
|
||||
<div v-else>{{ s.value }}</div>
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
class="form-control"
|
||||
v-if="s.type == 'Select'"
|
||||
:options="s.options"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<Button :label="s.value">
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dropdown, Badge, Tooltip, FeatherIcon } from 'frappe-ui'
|
||||
import { timeAgo, dateFormat, formatTime, dateTooltipFormat } from '@/utils'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { computed, defineModel } from 'vue'
|
||||
|
||||
const data = defineModel()
|
||||
const emit = defineEmits(['updateField'])
|
||||
|
||||
const { communicationStatuses } = statusesStore()
|
||||
|
||||
let slaSection = computed(() => {
|
||||
let sections = []
|
||||
if (data.value.first_responded_on) {
|
||||
sections.push({
|
||||
label: 'Fulfilled In',
|
||||
type: 'Duration',
|
||||
value: formatTime(data.value.first_response_time),
|
||||
tooltipText: dateFormat(data.value.first_responded_on, dateTooltipFormat),
|
||||
})
|
||||
}
|
||||
|
||||
let status = data.value.sla_status
|
||||
let tooltipText = status
|
||||
let color =
|
||||
data.value.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: data.value.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
|
||||
if (status == 'First Response Due') {
|
||||
status = timeAgo(data.value.response_by)
|
||||
tooltipText = dateFormat(data.value.response_by, dateTooltipFormat)
|
||||
if (new Date(data.value.response_by) < new Date()) {
|
||||
color = 'red'
|
||||
}
|
||||
}
|
||||
|
||||
sections.push(
|
||||
...[
|
||||
{
|
||||
label: 'SLA',
|
||||
type: 'Badge',
|
||||
value: status,
|
||||
tooltipText: tooltipText,
|
||||
color: color,
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
value: data.value.communication_status,
|
||||
type: 'Select',
|
||||
options: communicationStatuses.data?.map((status) => ({
|
||||
label: status.name,
|
||||
value: status.name,
|
||||
onClick: () =>
|
||||
emit('updateField', 'communication_status', status.name),
|
||||
})),
|
||||
},
|
||||
]
|
||||
)
|
||||
return sections
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.form-control button) {
|
||||
border-color: transparent;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
@ -3,12 +3,12 @@
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.label"
|
||||
class="flex items-center gap-2 px-3 text-base leading-5 first:mt-3"
|
||||
class="flex items-center gap-2 px-3 leading-5 first:mt-3"
|
||||
>
|
||||
<div class="w-[106px] shrink-0 text-gray-600">
|
||||
<div class="w-[106px] shrink-0 text-sm text-gray-600">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
<div class="grid min-h-[28px] flex-1 items-center overflow-hidden">
|
||||
<div class="grid min-h-[28px] flex-1 text-base items-center overflow-hidden">
|
||||
<FormControl
|
||||
v-if="
|
||||
[
|
||||
|
||||
@ -175,16 +175,9 @@
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ tab }">
|
||||
<LeadsListView
|
||||
class="mt-4"
|
||||
v-if="tab.label === 'Leads' && rows.length"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:options="{ selectable: false }"
|
||||
/>
|
||||
<DealsListView
|
||||
class="mt-4"
|
||||
v-else-if="tab.label === 'Deals' && rows.length"
|
||||
v-if="tab.label === 'Deals' && rows.length"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:options="{ selectable: false }"
|
||||
@ -227,9 +220,7 @@ import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import {
|
||||
@ -248,7 +239,7 @@ import { useRouter } from 'vue-router'
|
||||
const { getContactByName, contacts } = contactsStore()
|
||||
const { getUser } = usersStore()
|
||||
const { getOrganization } = organizationsStore()
|
||||
const { getLeadStatus, getDealStatus } = statusesStore()
|
||||
const { getDealStatus } = statusesStore()
|
||||
|
||||
const props = defineProps({
|
||||
contactId: {
|
||||
@ -314,11 +305,6 @@ async function deleteContact() {
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Leads',
|
||||
icon: h(LeadsIcon, { class: 'h-4 w-4' }),
|
||||
count: computed(() => leads.data?.length),
|
||||
},
|
||||
{
|
||||
label: 'Deals',
|
||||
icon: h(DealsIcon, { class: 'h-4 w-4' }),
|
||||
@ -326,31 +312,6 @@ const tabs = [
|
||||
},
|
||||
]
|
||||
|
||||
const leads = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'CRM Lead',
|
||||
cache: ['leads', props.contactId],
|
||||
fields: [
|
||||
'name',
|
||||
'first_name',
|
||||
'lead_name',
|
||||
'image',
|
||||
'organization',
|
||||
'status',
|
||||
'email',
|
||||
'mobile_no',
|
||||
'lead_owner',
|
||||
'modified',
|
||||
],
|
||||
filters: {
|
||||
email: contact.value?.email_id,
|
||||
converted: 0,
|
||||
},
|
||||
orderBy: 'modified desc',
|
||||
pageLength: 20,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const deals = createResource({
|
||||
url: 'crm.api.contact.get_linked_deals',
|
||||
cache: ['deals', props.contactId],
|
||||
@ -361,48 +322,12 @@ const deals = createResource({
|
||||
})
|
||||
|
||||
const rows = computed(() => {
|
||||
let list = []
|
||||
list = tabIndex.value ? deals : leads
|
||||
if (!deals.data || deals.data == []) return []
|
||||
|
||||
if (!list.data) return []
|
||||
|
||||
return list.data.map((row) => {
|
||||
return tabIndex.value ? getDealRowObject(row) : getLeadRowObject(row)
|
||||
})
|
||||
return deals.data.map((row) => getDealRowObject(row))
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
return tabIndex.value ? dealColumns : leadColumns
|
||||
})
|
||||
|
||||
function getLeadRowObject(lead) {
|
||||
return {
|
||||
name: lead.name,
|
||||
lead_name: {
|
||||
label: lead.lead_name,
|
||||
image: lead.image,
|
||||
image_label: lead.first_name,
|
||||
},
|
||||
organization: {
|
||||
label: lead.organization,
|
||||
logo: getOrganization(lead.organization)?.organization_logo,
|
||||
},
|
||||
status: {
|
||||
label: lead.status,
|
||||
color: getLeadStatus(lead.status)?.iconColorClass,
|
||||
},
|
||||
email: lead.email,
|
||||
mobile_no: lead.mobile_no,
|
||||
lead_owner: {
|
||||
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
|
||||
...(lead.lead_owner && getUser(lead.lead_owner)),
|
||||
},
|
||||
modified: {
|
||||
label: dateFormat(lead.modified, dateTooltipFormat),
|
||||
timeAgo: timeAgo(lead.modified),
|
||||
},
|
||||
}
|
||||
}
|
||||
const columns = computed(() => dealColumns)
|
||||
|
||||
function getDealRowObject(deal) {
|
||||
return {
|
||||
@ -429,44 +354,6 @@ function getDealRowObject(deal) {
|
||||
}
|
||||
}
|
||||
|
||||
const leadColumns = [
|
||||
{
|
||||
label: 'Name',
|
||||
key: 'lead_name',
|
||||
width: '12rem',
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organization',
|
||||
width: '10rem',
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
key: 'status',
|
||||
width: '8rem',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
key: 'email',
|
||||
width: '12rem',
|
||||
},
|
||||
{
|
||||
label: 'Mobile no',
|
||||
key: 'mobile_no',
|
||||
width: '11rem',
|
||||
},
|
||||
{
|
||||
label: 'Lead owner',
|
||||
key: 'lead_owner',
|
||||
width: '10rem',
|
||||
},
|
||||
{
|
||||
label: 'Last modified',
|
||||
key: 'modified',
|
||||
width: '8rem',
|
||||
},
|
||||
]
|
||||
|
||||
const dealColumns = [
|
||||
{
|
||||
label: 'Organization',
|
||||
|
||||
@ -4,6 +4,33 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Dropdown
|
||||
:options="[
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Delete',
|
||||
onClick: () =>
|
||||
$dialog({
|
||||
title: 'Delete Deal',
|
||||
message: 'Are you sure you want to delete this deal?',
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(close) {
|
||||
deleteDeal(deal.data.name)
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
]"
|
||||
@click.stop
|
||||
>
|
||||
<Button icon="more-horizontal" />
|
||||
</Dropdown>
|
||||
<Link
|
||||
class="form-control"
|
||||
:value="getUser(deal.data.deal_owner).full_name"
|
||||
@ -99,79 +126,21 @@
|
||||
</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">
|
||||
<SLASection
|
||||
v-if="deal.data.sla_status"
|
||||
v-model="deal.data"
|
||||
@updateField="updateField"
|
||||
/>
|
||||
<div
|
||||
v-if="detailSections.length"
|
||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div
|
||||
v-for="(section, i) in detailSections.data"
|
||||
v-for="(section, i) in detailSections"
|
||||
:key="section.label"
|
||||
class="flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== detailSections.data.length - 1 }"
|
||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<template #actions>
|
||||
@ -346,13 +315,8 @@ 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,
|
||||
dateFormat,
|
||||
timeAgo,
|
||||
formatTime,
|
||||
} from '@/utils'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import { openWebsite, createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
@ -389,15 +353,6 @@ 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)
|
||||
@ -475,17 +430,13 @@ const tabs = [
|
||||
},
|
||||
]
|
||||
|
||||
const detailSections = createResource({
|
||||
url: 'crm.api.doc.get_doctype_fields',
|
||||
params: { doctype: 'CRM Deal' },
|
||||
cache: 'dealFields',
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
return getParsedFields(data)
|
||||
},
|
||||
const detailSections = computed(() => {
|
||||
let data = deal.data
|
||||
if (!data) return []
|
||||
return getParsedFields(data.doctype_fields, data.contacts)
|
||||
})
|
||||
|
||||
function getParsedFields(sections) {
|
||||
function getParsedFields(sections, contacts) {
|
||||
sections.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
if (['website', 'annual_revenue'].includes(field.name)) {
|
||||
@ -510,15 +461,14 @@ function getParsedFields(sections) {
|
||||
let contactSection = {
|
||||
label: 'Contacts',
|
||||
opened: true,
|
||||
contacts: computed(() =>
|
||||
deal.data?.contacts.map((contact) => {
|
||||
contacts:
|
||||
contacts?.map((contact) => {
|
||||
return {
|
||||
name: contact.contact,
|
||||
is_primary: contact.is_primary,
|
||||
opened: false,
|
||||
}
|
||||
})
|
||||
),
|
||||
}) || [],
|
||||
}
|
||||
|
||||
return [...sections, contactSection]
|
||||
@ -599,4 +549,12 @@ function updateField(name, value, callback) {
|
||||
callback?.()
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteDeal(name) {
|
||||
await call('frappe.client.delete', {
|
||||
doctype: 'CRM Deal',
|
||||
name,
|
||||
})
|
||||
router.push({ name: 'Deals' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -156,14 +156,25 @@ const rows = computed(() => {
|
||||
color: getDealStatus(deal.status)?.iconColorClass,
|
||||
}
|
||||
} else if (row == 'sla_status') {
|
||||
let value = deal.sla_status
|
||||
let tooltipText = value
|
||||
let color =
|
||||
deal.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: deal.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
if (value == 'First Response Due') {
|
||||
value = timeAgo(deal.response_by)
|
||||
tooltipText = dateFormat(deal.response_by, dateTooltipFormat)
|
||||
if (new Date(deal.response_by) < new Date()) {
|
||||
color = 'red'
|
||||
}
|
||||
}
|
||||
_rows[row] = {
|
||||
label: deal.sla_status,
|
||||
color:
|
||||
deal.sla_status === 'Failed'
|
||||
? 'red'
|
||||
: deal.sla_status === 'Fulfilled'
|
||||
? 'green'
|
||||
: 'gray',
|
||||
label: tooltipText,
|
||||
value: value,
|
||||
color: color,
|
||||
}
|
||||
} else if (row == 'deal_owner') {
|
||||
_rows[row] = {
|
||||
@ -175,15 +186,18 @@ const rows = computed(() => {
|
||||
label: dateFormat(deal[row], dateTooltipFormat),
|
||||
timeAgo: timeAgo(deal[row]),
|
||||
}
|
||||
} else if (['first_response_time', 'first_responded_on'].includes(row)) {
|
||||
} else if (
|
||||
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
||||
row
|
||||
)
|
||||
) {
|
||||
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
||||
_rows[row] = {
|
||||
label: deal.first_responded_on
|
||||
? dateFormat(deal.first_responded_on, dateTooltipFormat)
|
||||
: '',
|
||||
label: deal[field] ? dateFormat(deal[field], dateTooltipFormat) : '',
|
||||
timeAgo: deal[row]
|
||||
? row == 'first_responded_on'
|
||||
? timeAgo(deal[row])
|
||||
: formatTime(deal[row])
|
||||
? row == 'first_response_time'
|
||||
? formatTime(deal[row])
|
||||
: timeAgo(deal[row])
|
||||
: '',
|
||||
}
|
||||
}
|
||||
@ -239,7 +253,7 @@ const showNewDialog = ref(false)
|
||||
|
||||
let newDeal = reactive({
|
||||
organization: '',
|
||||
status: 'Qualification',
|
||||
status: '',
|
||||
email: '',
|
||||
mobile_no: '',
|
||||
deal_owner: '',
|
||||
|
||||
@ -4,6 +4,33 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
<template #right-header>
|
||||
<Dropdown
|
||||
:options="[
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Delete',
|
||||
onClick: () =>
|
||||
$dialog({
|
||||
title: 'Delete Lead',
|
||||
message: 'Are you sure you want to delete this lead?',
|
||||
actions: [
|
||||
{
|
||||
label: 'Delete',
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(close) {
|
||||
deleteLead(lead.data.name)
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
]"
|
||||
@click.stop
|
||||
>
|
||||
<Button icon="more-horizontal" />
|
||||
</Dropdown>
|
||||
<Link
|
||||
class="form-control"
|
||||
:value="getUser(lead.data.lead_owner).full_name"
|
||||
@ -139,79 +166,21 @@
|
||||
</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">
|
||||
<SLASection
|
||||
v-if="lead.data.sla_status"
|
||||
v-model="lead.data"
|
||||
@updateField="updateField"
|
||||
/>
|
||||
<div
|
||||
v-if="detailSections.length"
|
||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div
|
||||
v-for="(section, i) in detailSections.data"
|
||||
v-for="(section, i) in detailSections"
|
||||
:key="section.label"
|
||||
class="flex flex-col p-3"
|
||||
:class="{ 'border-b': i !== detailSections.data.length - 1 }"
|
||||
:class="{ 'border-b': i !== detailSections.length - 1 }"
|
||||
>
|
||||
<Section :is-opened="section.opened" :label="section.label">
|
||||
<SectionFields
|
||||
@ -252,14 +221,9 @@ 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 SLASection from '@/components/SLASection.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import {
|
||||
openWebsite,
|
||||
createToast,
|
||||
dateFormat,
|
||||
timeAgo,
|
||||
formatTime,
|
||||
} from '@/utils'
|
||||
import { openWebsite, createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
@ -274,7 +238,6 @@ import {
|
||||
Avatar,
|
||||
Tabs,
|
||||
Breadcrumbs,
|
||||
Badge,
|
||||
call,
|
||||
} from 'frappe-ui'
|
||||
import { ref, computed } from 'vue'
|
||||
@ -298,15 +261,6 @@ 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)
|
||||
@ -390,14 +344,10 @@ function validateFile(file) {
|
||||
}
|
||||
}
|
||||
|
||||
const detailSections = createResource({
|
||||
url: 'crm.api.doc.get_doctype_fields',
|
||||
params: { doctype: 'CRM Lead' },
|
||||
cache: 'leadFields',
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
return getParsedFields(data)
|
||||
},
|
||||
const detailSections = computed(() => {
|
||||
let data = lead.data
|
||||
if (!data) return []
|
||||
return getParsedFields(data.doctype_fields, data.contacts)
|
||||
})
|
||||
|
||||
function getParsedFields(sections) {
|
||||
@ -426,6 +376,7 @@ function getParsedFields(sections) {
|
||||
}
|
||||
|
||||
async function convertToDeal() {
|
||||
updateField('communication_status', 'Replied')
|
||||
let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||
lead: lead.data.name,
|
||||
})
|
||||
@ -441,4 +392,12 @@ function updateField(name, value, callback) {
|
||||
callback?.()
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteLead(name) {
|
||||
await call('frappe.client.delete', {
|
||||
doctype: 'CRM Lead',
|
||||
name,
|
||||
})
|
||||
router.push({ name: 'Leads' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -155,14 +155,25 @@ const rows = computed(() => {
|
||||
color: getLeadStatus(lead.status)?.iconColorClass,
|
||||
}
|
||||
} else if (row == 'sla_status') {
|
||||
let value = lead.sla_status
|
||||
let tooltipText = value
|
||||
let color =
|
||||
lead.sla_status == 'Failed'
|
||||
? 'red'
|
||||
: lead.sla_status == 'Fulfilled'
|
||||
? 'green'
|
||||
: 'orange'
|
||||
if (value == 'First Response Due') {
|
||||
value = timeAgo(lead.response_by)
|
||||
tooltipText = dateFormat(lead.response_by, dateTooltipFormat)
|
||||
if (new Date(lead.response_by) < new Date()) {
|
||||
color = 'red'
|
||||
}
|
||||
}
|
||||
_rows[row] = {
|
||||
label: lead.sla_status,
|
||||
color:
|
||||
lead.sla_status === 'Failed'
|
||||
? 'red'
|
||||
: lead.sla_status === 'Fulfilled'
|
||||
? 'green'
|
||||
: 'gray',
|
||||
label: tooltipText,
|
||||
value: value,
|
||||
color: color,
|
||||
}
|
||||
} else if (row == 'lead_owner') {
|
||||
_rows[row] = {
|
||||
@ -174,15 +185,18 @@ const rows = computed(() => {
|
||||
label: dateFormat(lead[row], dateTooltipFormat),
|
||||
timeAgo: timeAgo(lead[row]),
|
||||
}
|
||||
} else if (['first_response_time', 'first_responded_on'].includes(row)) {
|
||||
} else if (
|
||||
['first_response_time', 'first_responded_on', 'response_by'].includes(
|
||||
row
|
||||
)
|
||||
) {
|
||||
let field = row == 'response_by' ? 'response_by' : 'first_responded_on'
|
||||
_rows[row] = {
|
||||
label: lead.first_responded_on
|
||||
? dateFormat(lead.first_responded_on, dateTooltipFormat)
|
||||
: '',
|
||||
label: lead[field] ? dateFormat(lead[field], dateTooltipFormat) : '',
|
||||
timeAgo: lead[row]
|
||||
? row == 'first_responded_on'
|
||||
? timeAgo(lead[row])
|
||||
: formatTime(lead[row])
|
||||
? row == 'first_response_time'
|
||||
? formatTime(lead[row])
|
||||
: timeAgo(lead[row])
|
||||
: '',
|
||||
}
|
||||
}
|
||||
@ -242,7 +256,7 @@ let newLead = reactive({
|
||||
last_name: '',
|
||||
lead_name: '',
|
||||
organization: '',
|
||||
status: 'Open',
|
||||
status: '',
|
||||
email: '',
|
||||
mobile_no: '',
|
||||
lead_owner: '',
|
||||
|
||||
@ -6,6 +6,7 @@ import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
export const statusesStore = defineStore('crm-statuses', () => {
|
||||
let leadStatusesByName = reactive({})
|
||||
let dealStatusesByName = reactive({})
|
||||
let communicationStatusesByName = reactive({})
|
||||
|
||||
const leadStatuses = createListResource({
|
||||
doctype: 'CRM Lead Status',
|
||||
@ -41,6 +42,20 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const communicationStatuses = createListResource({
|
||||
doctype: 'CRM Communication Status',
|
||||
fields: ['name'],
|
||||
cache: 'communication-statuses',
|
||||
initialData: [],
|
||||
auto: true,
|
||||
transform(statuses) {
|
||||
for (let status of statuses) {
|
||||
communicationStatusesByName[status.name] = status
|
||||
}
|
||||
return statuses
|
||||
},
|
||||
})
|
||||
|
||||
function colorClasses(color, onlyIcon = false) {
|
||||
let textColor = `!text-${color}-600`
|
||||
if (color == 'black') {
|
||||
@ -55,13 +70,26 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
}
|
||||
|
||||
function getLeadStatus(name) {
|
||||
if (!name) {
|
||||
name = leadStatuses.data[0].name
|
||||
}
|
||||
return leadStatusesByName[name]
|
||||
}
|
||||
|
||||
function getDealStatus(name) {
|
||||
if (!name) {
|
||||
name = dealStatuses.data[0].name
|
||||
}
|
||||
return dealStatusesByName[name]
|
||||
}
|
||||
|
||||
function getCommunicationStatus(name) {
|
||||
if (!name) {
|
||||
name = communicationStatuses.data[0].name
|
||||
}
|
||||
return communicationStatuses[name]
|
||||
}
|
||||
|
||||
function statusOptions(doctype, action) {
|
||||
let statusesByName =
|
||||
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
||||
@ -84,8 +112,10 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
return {
|
||||
leadStatuses,
|
||||
dealStatuses,
|
||||
communicationStatuses,
|
||||
getLeadStatus,
|
||||
getDealStatus,
|
||||
getCommunicationStatus,
|
||||
statusOptions,
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user