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):
|
def get_doctype_fields(doctype):
|
||||||
not_allowed_fieldtypes = [
|
not_allowed_fieldtypes = [
|
||||||
"Section Break",
|
"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
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
|
from crm.api.doc import get_doctype_fields
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_deal(name):
|
def get_deal(name):
|
||||||
@ -25,4 +26,5 @@ def get_deal(name):
|
|||||||
fields=["contact", "is_primary"],
|
fields=["contact", "is_primary"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
deal["doctype_fields"] = get_doctype_fields("CRM Deal")
|
||||||
return deal
|
return deal
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"sla_creation",
|
"sla_creation",
|
||||||
"column_break_pfvq",
|
"column_break_pfvq",
|
||||||
"sla_status",
|
"sla_status",
|
||||||
|
"communication_status",
|
||||||
"response_details_section",
|
"response_details_section",
|
||||||
"response_by",
|
"response_by",
|
||||||
"column_break_hpvj",
|
"column_break_hpvj",
|
||||||
@ -195,11 +196,18 @@
|
|||||||
"fieldname": "first_responded_on",
|
"fieldname": "first_responded_on",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "First Responded On"
|
"label": "First Responded On"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Open",
|
||||||
|
"fieldname": "communication_status",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Communication Status",
|
||||||
|
"options": "CRM Communication Status"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-11 12:37:51.198228",
|
"modified": "2023-12-13 13:50:55.235109",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
|
|
||||||
|
|
||||||
class CRMDeal(Document):
|
class CRMDeal(Document):
|
||||||
def before_validate(self):
|
def before_validate(self):
|
||||||
@ -55,7 +57,7 @@ class CRMDeal(Document):
|
|||||||
"""
|
"""
|
||||||
Find an SLA to apply to the deal.
|
Find an SLA to apply to the deal.
|
||||||
"""
|
"""
|
||||||
sla = get_sla("CRM Deal")
|
sla = get_sla(self)
|
||||||
if not sla:
|
if not sla:
|
||||||
return
|
return
|
||||||
self.sla = sla.name
|
self.sla = sla.name
|
||||||
@ -139,6 +141,7 @@ class CRMDeal(Document):
|
|||||||
"mobile_no",
|
"mobile_no",
|
||||||
"deal_owner",
|
"deal_owner",
|
||||||
"sla_status",
|
"sla_status",
|
||||||
|
"response_by",
|
||||||
"first_response_time",
|
"first_response_time",
|
||||||
"first_responded_on",
|
"first_responded_on",
|
||||||
"modified",
|
"modified",
|
||||||
@ -175,8 +178,3 @@ def set_primary_contact(deal, contact):
|
|||||||
deal.save()
|
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)
|
|
||||||
|
|||||||
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
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
|
from crm.api.doc import get_doctype_fields
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_lead(name):
|
def get_lead(name):
|
||||||
@ -13,4 +14,5 @@ def get_lead(name):
|
|||||||
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
frappe.throw(_("Lead not found"), frappe.DoesNotExistError)
|
||||||
lead = lead.pop()
|
lead = lead.pop()
|
||||||
|
|
||||||
|
lead["doctype_fields"] = get_doctype_fields("CRM Lead")
|
||||||
return lead
|
return lead
|
||||||
@ -40,6 +40,7 @@
|
|||||||
"sla_creation",
|
"sla_creation",
|
||||||
"column_break_ffnp",
|
"column_break_ffnp",
|
||||||
"sla_status",
|
"sla_status",
|
||||||
|
"communication_status",
|
||||||
"response_details_section",
|
"response_details_section",
|
||||||
"response_by",
|
"response_by",
|
||||||
"column_break_pweh",
|
"column_break_pweh",
|
||||||
@ -257,12 +258,19 @@
|
|||||||
"fieldname": "first_responded_on",
|
"fieldname": "first_responded_on",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "First Responded On"
|
"label": "First Responded On"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Open",
|
||||||
|
"fieldname": "communication_status",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Communication Status",
|
||||||
|
"options": "CRM Communication Status"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-10 13:54:53.630114",
|
"modified": "2023-12-13 13:50:40.055487",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Lead",
|
"name": "CRM Lead",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from frappe.utils import has_gravatar, validate_email_address
|
from frappe.utils import has_gravatar, validate_email_address
|
||||||
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
|
|
||||||
|
|
||||||
class CRMLead(Document):
|
class CRMLead(Document):
|
||||||
@ -124,6 +125,19 @@ class CRMLead(Document):
|
|||||||
"contacts": [{"contact": contact}],
|
"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)
|
deal.insert(ignore_permissions=True)
|
||||||
return deal.name
|
return deal.name
|
||||||
|
|
||||||
@ -131,7 +145,7 @@ class CRMLead(Document):
|
|||||||
"""
|
"""
|
||||||
Find an SLA to apply to the lead.
|
Find an SLA to apply to the lead.
|
||||||
"""
|
"""
|
||||||
sla = get_sla("CRM Lead")
|
sla = get_sla(self)
|
||||||
if not sla:
|
if not sla:
|
||||||
return
|
return
|
||||||
self.sla = sla.name
|
self.sla = sla.name
|
||||||
@ -219,6 +233,7 @@ class CRMLead(Document):
|
|||||||
"lead_owner",
|
"lead_owner",
|
||||||
"first_name",
|
"first_name",
|
||||||
"sla_status",
|
"sla_status",
|
||||||
|
"response_by",
|
||||||
"first_response_time",
|
"first_response_time",
|
||||||
"first_responded_on",
|
"first_responded_on",
|
||||||
"modified",
|
"modified",
|
||||||
@ -239,8 +254,3 @@ def convert_to_deal(lead):
|
|||||||
lead.save()
|
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)
|
|
||||||
|
|||||||
@ -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",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"sla_name",
|
|
||||||
"apply_on",
|
"apply_on",
|
||||||
|
"column_break_uxua",
|
||||||
|
"sla_name",
|
||||||
"enabled",
|
"enabled",
|
||||||
"default",
|
"default",
|
||||||
"column_break_uxua",
|
"section_break_nevd",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"column_break_pzjg",
|
||||||
"condition",
|
"condition",
|
||||||
"section_break_ufaf",
|
"section_break_ufaf",
|
||||||
"priorities",
|
"priorities",
|
||||||
"section_break_rmgo",
|
"section_break_rmgo",
|
||||||
|
"holiday_list",
|
||||||
"working_hours"
|
"working_hours"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -60,7 +65,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_ufaf",
|
"fieldname": "section_break_ufaf",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Response and Follow Up"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "priorities",
|
"fieldname": "priorities",
|
||||||
@ -71,7 +77,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_rmgo",
|
"fieldname": "section_break_rmgo",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Working Hours"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "working_hours",
|
"fieldname": "working_hours",
|
||||||
@ -79,11 +86,36 @@
|
|||||||
"label": "Working Hours",
|
"label": "Working Hours",
|
||||||
"options": "CRM Service Day",
|
"options": "CRM Service Day",
|
||||||
"reqd": 1
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-04 16:13:24.638239",
|
"modified": "2023-12-15 11:50:29.956775",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Service Level Agreement",
|
"name": "CRM Service Level Agreement",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
@ -12,12 +13,27 @@ from frappe.utils import (
|
|||||||
now_datetime,
|
now_datetime,
|
||||||
time_diff_in_seconds,
|
time_diff_in_seconds,
|
||||||
)
|
)
|
||||||
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_context
|
||||||
|
|
||||||
|
|
||||||
class CRMServiceLevelAgreement(Document):
|
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):
|
def apply(self, doc: Document):
|
||||||
self.handle_new(doc)
|
self.handle_new(doc)
|
||||||
self.handle_status(doc)
|
self.handle_communication_status(doc)
|
||||||
self.handle_targets(doc)
|
self.handle_targets(doc)
|
||||||
self.handle_sla_status(doc)
|
self.handle_sla_status(doc)
|
||||||
|
|
||||||
@ -27,14 +43,14 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
creation = doc.sla_creation or now_datetime()
|
creation = doc.sla_creation or now_datetime()
|
||||||
doc.sla_creation = creation
|
doc.sla_creation = creation
|
||||||
|
|
||||||
def handle_status(self, doc: Document):
|
def handle_communication_status(self, doc: Document):
|
||||||
if doc.is_new() or not doc.has_value_changed("status"):
|
if doc.is_new() or not doc.has_value_changed("communication_status"):
|
||||||
return
|
return
|
||||||
self.set_first_responded_on(doc)
|
self.set_first_responded_on(doc)
|
||||||
self.set_first_response_time(doc)
|
self.set_first_response_time(doc)
|
||||||
|
|
||||||
def set_first_responded_on(self, doc: Document):
|
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 = (
|
||||||
doc.first_responded_on or now_datetime()
|
doc.first_responded_on or now_datetime()
|
||||||
)
|
)
|
||||||
@ -51,10 +67,10 @@ class CRMServiceLevelAgreement(Document):
|
|||||||
|
|
||||||
def set_response_by(self, doc: Document):
|
def set_response_by(self, doc: Document):
|
||||||
start_time = doc.sla_creation
|
start_time = doc.sla_creation
|
||||||
status = doc.status
|
communication_status = doc.communication_status
|
||||||
|
|
||||||
priorities = self.get_priorities()
|
priorities = self.get_priorities()
|
||||||
priority = priorities.get(status)
|
priority = priorities.get(communication_status)
|
||||||
if not priority or doc.response_by:
|
if not priority or doc.response_by:
|
||||||
return
|
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",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"default_priority",
|
"default_priority",
|
||||||
"reference_doctype",
|
"column_break_grod",
|
||||||
"priority",
|
"priority",
|
||||||
"first_response_time"
|
"section_break_anyl",
|
||||||
|
"first_response_time",
|
||||||
|
"column_break_bwgs"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -21,10 +23,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "priority",
|
"fieldname": "priority",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Priority",
|
"label": "Priority",
|
||||||
"options": "reference_doctype",
|
"options": "CRM Communication Status",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,17 +37,22 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "CRM Lead Status",
|
"fieldname": "column_break_grod",
|
||||||
"fieldname": "reference_doctype",
|
"fieldtype": "Column Break"
|
||||||
"fieldtype": "Link",
|
},
|
||||||
"label": "DocType",
|
{
|
||||||
"options": "DocType"
|
"fieldname": "section_break_anyl",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bwgs",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-04 14:05:42.838493",
|
"modified": "2023-12-15 11:49:54.424029",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Service Level Priority",
|
"name": "CRM Service Level Priority",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"charts": [],
|
"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",
|
"creation": "2023-11-27 13:55:17.090361",
|
||||||
"custom_blocks": [],
|
"custom_blocks": [],
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"is_hidden": 0,
|
"is_hidden": 0,
|
||||||
"label": "Frappe CRM",
|
"label": "Frappe CRM",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-11 13:06:13.532693",
|
"modified": "2023-12-13 19:59:33.129412",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "Frappe CRM",
|
"name": "Frappe CRM",
|
||||||
@ -25,6 +25,14 @@
|
|||||||
"roles": [],
|
"roles": [],
|
||||||
"sequence_id": 1.0,
|
"sequence_id": 1.0,
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"color": "Grey",
|
||||||
|
"doc_view": "List",
|
||||||
|
"label": "Communication Statuses",
|
||||||
|
"link_to": "CRM Communication Status",
|
||||||
|
"stats_filter": "[]",
|
||||||
|
"type": "DocType"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"color": "Grey",
|
"color": "Grey",
|
||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
|
|||||||
@ -10,11 +10,12 @@ def before_install():
|
|||||||
def after_install():
|
def after_install():
|
||||||
add_default_lead_statuses()
|
add_default_lead_statuses()
|
||||||
add_default_deal_statuses()
|
add_default_deal_statuses()
|
||||||
|
add_default_communication_statuses()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
def add_default_lead_statuses():
|
def add_default_lead_statuses():
|
||||||
statuses = {
|
statuses = {
|
||||||
"Open": {
|
"New": {
|
||||||
"color": "gray",
|
"color": "gray",
|
||||||
"position": 1,
|
"position": 1,
|
||||||
},
|
},
|
||||||
@ -91,3 +92,14 @@ def add_default_deal_statuses():
|
|||||||
doc.color = statuses[status]["color"]
|
doc.color = statuses[status]["color"]
|
||||||
doc.position = statuses[status]["position"]
|
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',
|
'creation',
|
||||||
'first_response_time',
|
'first_response_time',
|
||||||
'first_responded_on',
|
'first_responded_on',
|
||||||
|
'response_by',
|
||||||
].includes(column.key)
|
].includes(column.key)
|
||||||
"
|
"
|
||||||
class="truncate text-base"
|
class="truncate text-base"
|
||||||
@ -62,11 +63,11 @@
|
|||||||
class="truncate text-base"
|
class="truncate text-base"
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="item.label"
|
v-if="item.value"
|
||||||
:variant="'subtle'"
|
:variant="'subtle'"
|
||||||
:theme="item.color"
|
:theme="item.color"
|
||||||
size="md"
|
size="md"
|
||||||
:label="item.label"
|
:label="item.value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="column.type === 'Check'">
|
<div v-else-if="column.type === 'Check'">
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
'creation',
|
'creation',
|
||||||
'first_response_time',
|
'first_response_time',
|
||||||
'first_responded_on',
|
'first_responded_on',
|
||||||
|
'response_by',
|
||||||
].includes(column.key)
|
].includes(column.key)
|
||||||
"
|
"
|
||||||
class="truncate text-base"
|
class="truncate text-base"
|
||||||
@ -71,11 +72,11 @@
|
|||||||
class="truncate text-base"
|
class="truncate text-base"
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="item.label"
|
v-if="item.value"
|
||||||
:variant="'subtle'"
|
:variant="'subtle'"
|
||||||
:theme="item.color"
|
:theme="item.color"
|
||||||
size="md"
|
size="md"
|
||||||
:label="item.label"
|
:label="item.value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="column.type === 'Check'">
|
<div v-else-if="column.type === 'Check'">
|
||||||
|
|||||||
@ -338,7 +338,7 @@ const sections = computed(() => {
|
|||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
type: 'dropdown',
|
type: props.contact.name ? 'dropdown' : 'data',
|
||||||
name: 'email_id',
|
name: 'email_id',
|
||||||
options: props.contact?.email_ids?.map((email) => {
|
options: props.contact?.email_ids?.map((email) => {
|
||||||
return {
|
return {
|
||||||
@ -374,7 +374,7 @@ const sections = computed(() => {
|
|||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Mobile No.',
|
label: 'Mobile No.',
|
||||||
type: 'dropdown',
|
type: props.contact.name ? 'dropdown' : 'data',
|
||||||
name: 'mobile_no',
|
name: 'mobile_no',
|
||||||
options: props.contact?.phone_nos?.map((phone) => {
|
options: props.contact?.phone_nos?.map((phone) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{
|
{
|
||||||
label: editMode ? 'Update' : 'Create',
|
label: editMode ? 'Update' : 'Create',
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
onClick: ({ close }) => updateNote(close),
|
onClick: () => updateNote(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
@ -67,7 +67,7 @@ const title = ref(null)
|
|||||||
const editMode = ref(false)
|
const editMode = ref(false)
|
||||||
let _note = ref({})
|
let _note = ref({})
|
||||||
|
|
||||||
async function updateNote(close) {
|
async function updateNote() {
|
||||||
if (
|
if (
|
||||||
props.note.title === _note.value.title &&
|
props.note.title === _note.value.title &&
|
||||||
props.note.content === _note.value.content
|
props.note.content === _note.value.content
|
||||||
@ -97,7 +97,7 @@ async function updateNote(close) {
|
|||||||
notes.value.reload()
|
notes.value.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close()
|
show.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
{
|
{
|
||||||
label: editMode ? 'Update' : 'Create',
|
label: editMode ? 'Update' : 'Create',
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
onClick: ({ close }) => updateTask(close),
|
onClick: () => updateTask(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
@ -142,7 +142,7 @@ function updateTaskPriority(priority) {
|
|||||||
_task.value.priority = priority
|
_task.value.priority = priority
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTask(close) {
|
async function updateTask() {
|
||||||
if (!_task.value.assigned_to) {
|
if (!_task.value.assigned_to) {
|
||||||
_task.value.assigned_to = getUser().email
|
_task.value.assigned_to = getUser().email
|
||||||
}
|
}
|
||||||
@ -168,7 +168,7 @@ async function updateTask(close) {
|
|||||||
tasks.value.reload()
|
tasks.value.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close()
|
show.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -10,10 +10,8 @@
|
|||||||
:options="field.options"
|
:options="field.options"
|
||||||
v-model="newDeal[field.name]"
|
v-model="newDeal[field.name]"
|
||||||
>
|
>
|
||||||
<template v-if="field.name == 'status'" #prefix>
|
<template v-if="field.prefix" #prefix>
|
||||||
<IndicatorIcon
|
<IndicatorIcon :class="field.prefix" />
|
||||||
:class="getDealStatus(newDeal[field.name]).iconColorClass"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl
|
<FormControl
|
||||||
@ -73,7 +71,7 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
|||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { FormControl, Tooltip } from 'frappe-ui'
|
import { FormControl, Tooltip } from 'frappe-ui'
|
||||||
import { ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getDealStatus, statusOptions } = statusesStore()
|
const { getDealStatus, statusOptions } = statusesStore()
|
||||||
@ -88,82 +86,83 @@ const props = defineProps({
|
|||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
const _organization = ref({})
|
const _organization = ref({})
|
||||||
|
|
||||||
const allFields = [
|
const allFields = computed(() => {
|
||||||
{
|
return [
|
||||||
section: 'Deal Details',
|
{
|
||||||
fields: [
|
section: 'Deal Details',
|
||||||
{
|
fields: [
|
||||||
label: 'Salutation',
|
{
|
||||||
name: 'salutation',
|
label: 'Salutation',
|
||||||
type: 'select',
|
name: 'salutation',
|
||||||
options: [
|
type: 'link',
|
||||||
{
|
doctype: 'Salutation',
|
||||||
label: 'Mr',
|
placeholder: 'Salutation',
|
||||||
value: 'Mr',
|
change: (data) => (props.newDeal.salutation = data),
|
||||||
},
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'First Name',
|
||||||
label: 'Status',
|
name: 'first_name',
|
||||||
name: 'status',
|
type: 'data',
|
||||||
type: 'select',
|
},
|
||||||
options: statusOptions('deal'),
|
{
|
||||||
},
|
label: 'Last Name',
|
||||||
{
|
name: 'last_name',
|
||||||
label: 'Deal Owner',
|
type: 'data',
|
||||||
name: 'deal_owner',
|
},
|
||||||
type: 'user',
|
{
|
||||||
placeholder: 'Deal Owner',
|
label: 'Email',
|
||||||
doctype: 'User',
|
name: 'email',
|
||||||
change: (data) => (props.newDeal.deal_owner = data),
|
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>
|
</script>
|
||||||
|
|||||||
@ -10,10 +10,8 @@
|
|||||||
:options="field.options"
|
:options="field.options"
|
||||||
v-model="newLead[field.name]"
|
v-model="newLead[field.name]"
|
||||||
>
|
>
|
||||||
<template v-if="field.name == 'status'" #prefix>
|
<template v-if="field.prefix" #prefix>
|
||||||
<IndicatorIcon
|
<IndicatorIcon :class="field.prefix" />
|
||||||
:class="getLeadStatus(newLead[field.name]).iconColorClass"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl
|
<FormControl
|
||||||
@ -73,7 +71,7 @@ import Link from '@/components/Controls/Link.vue'
|
|||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { FormControl, Tooltip } from 'frappe-ui'
|
import { FormControl, Tooltip } from 'frappe-ui'
|
||||||
import { ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getLeadStatus, statusOptions } = statusesStore()
|
const { getLeadStatus, statusOptions } = statusesStore()
|
||||||
@ -88,82 +86,83 @@ const props = defineProps({
|
|||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
const _organization = ref({})
|
const _organization = ref({})
|
||||||
|
|
||||||
const allFields = [
|
const allFields = computed(() => {
|
||||||
{
|
return [
|
||||||
section: 'Lead Details',
|
{
|
||||||
fields: [
|
section: 'Lead Details',
|
||||||
{
|
fields: [
|
||||||
label: 'Salutation',
|
{
|
||||||
name: 'salutation',
|
label: 'Salutation',
|
||||||
type: 'select',
|
name: 'salutation',
|
||||||
options: [
|
type: 'link',
|
||||||
{
|
placeholder: 'Salutation',
|
||||||
label: 'Mr',
|
doctype: 'Salutation',
|
||||||
value: 'Mr',
|
change: (data) => (props.newLead.salutation = data),
|
||||||
},
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: 'First Name',
|
||||||
label: 'Status',
|
name: 'first_name',
|
||||||
name: 'status',
|
type: 'data',
|
||||||
type: 'select',
|
},
|
||||||
options: statusOptions('lead'),
|
{
|
||||||
},
|
label: 'Last Name',
|
||||||
{
|
name: 'last_name',
|
||||||
label: 'Lead Owner',
|
type: 'data',
|
||||||
name: 'lead_owner',
|
},
|
||||||
type: 'user',
|
{
|
||||||
placeholder: 'Lead Owner',
|
label: 'Email',
|
||||||
doctype: 'User',
|
name: 'email',
|
||||||
change: (data) => (props.newLead.lead_owner = data),
|
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>
|
</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
|
<div
|
||||||
v-for="field in fields"
|
v-for="field in fields"
|
||||||
:key="field.label"
|
: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 }}
|
{{ field.label }}
|
||||||
</div>
|
</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
|
<FormControl
|
||||||
v-if="
|
v-if="
|
||||||
[
|
[
|
||||||
|
|||||||
@ -175,16 +175,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ tab }">
|
<template #default="{ tab }">
|
||||||
<LeadsListView
|
|
||||||
class="mt-4"
|
|
||||||
v-if="tab.label === 'Leads' && rows.length"
|
|
||||||
:rows="rows"
|
|
||||||
:columns="columns"
|
|
||||||
:options="{ selectable: false }"
|
|
||||||
/>
|
|
||||||
<DealsListView
|
<DealsListView
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
v-else-if="tab.label === 'Deals' && rows.length"
|
v-if="tab.label === 'Deals' && rows.length"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:options="{ selectable: false }"
|
:options="{ selectable: false }"
|
||||||
@ -227,9 +220,7 @@ import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
|||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
|
||||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
|
||||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import {
|
import {
|
||||||
@ -248,7 +239,7 @@ import { useRouter } from 'vue-router'
|
|||||||
const { getContactByName, contacts } = contactsStore()
|
const { getContactByName, contacts } = contactsStore()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
const { getLeadStatus, getDealStatus } = statusesStore()
|
const { getDealStatus } = statusesStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
contactId: {
|
contactId: {
|
||||||
@ -314,11 +305,6 @@ async function deleteContact() {
|
|||||||
|
|
||||||
const tabIndex = ref(0)
|
const tabIndex = ref(0)
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
|
||||||
label: 'Leads',
|
|
||||||
icon: h(LeadsIcon, { class: 'h-4 w-4' }),
|
|
||||||
count: computed(() => leads.data?.length),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Deals',
|
label: 'Deals',
|
||||||
icon: h(DealsIcon, { class: 'h-4 w-4' }),
|
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({
|
const deals = createResource({
|
||||||
url: 'crm.api.contact.get_linked_deals',
|
url: 'crm.api.contact.get_linked_deals',
|
||||||
cache: ['deals', props.contactId],
|
cache: ['deals', props.contactId],
|
||||||
@ -361,48 +322,12 @@ const deals = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
let list = []
|
if (!deals.data || deals.data == []) return []
|
||||||
list = tabIndex.value ? deals : leads
|
|
||||||
|
|
||||||
if (!list.data) return []
|
return deals.data.map((row) => getDealRowObject(row))
|
||||||
|
|
||||||
return list.data.map((row) => {
|
|
||||||
return tabIndex.value ? getDealRowObject(row) : getLeadRowObject(row)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = computed(() => {
|
const columns = computed(() => dealColumns)
|
||||||
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),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDealRowObject(deal) {
|
function getDealRowObject(deal) {
|
||||||
return {
|
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 = [
|
const dealColumns = [
|
||||||
{
|
{
|
||||||
label: 'Organization',
|
label: 'Organization',
|
||||||
|
|||||||
@ -4,6 +4,33 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<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
|
<Link
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:value="getUser(deal.data.deal_owner).full_name"
|
:value="getUser(deal.data.deal_owner).full_name"
|
||||||
@ -99,79 +126,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="deal.data.sla_status" class="flex flex-col gap-2 border-b p-5">
|
<SLASection
|
||||||
<div
|
v-if="deal.data.sla_status"
|
||||||
v-if="deal.data.sla_status == 'First Response Due'"
|
v-model="deal.data"
|
||||||
class="flex items-center gap-4 text-base leading-5"
|
@updateField="updateField"
|
||||||
>
|
/>
|
||||||
<div class="w-[106px] text-gray-600">Response By</div>
|
<div
|
||||||
<Tooltip
|
v-if="detailSections.length"
|
||||||
:text="dateFormat(deal.data.response_by, 'ddd, MMM D, YYYY h:mm A')"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
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 class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections.data"
|
v-for="(section, i) in detailSections"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col p-3"
|
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">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -346,13 +315,8 @@ import ContactModal from '@/components/Modals/ContactModal.vue'
|
|||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import {
|
import SLASection from '@/components/SLASection.vue'
|
||||||
openWebsite,
|
import { openWebsite, createToast } from '@/utils'
|
||||||
createToast,
|
|
||||||
dateFormat,
|
|
||||||
timeAgo,
|
|
||||||
formatTime,
|
|
||||||
} from '@/utils'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
@ -389,15 +353,6 @@ const deal = createResource({
|
|||||||
params: { name: props.dealId },
|
params: { name: props.dealId },
|
||||||
cache: ['deal', props.dealId],
|
cache: ['deal', props.dealId],
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess: (data) => {
|
|
||||||
if (
|
|
||||||
data.response_by &&
|
|
||||||
data.sla_status == 'First Response Due' &&
|
|
||||||
new Date(data.response_by) < new Date()
|
|
||||||
) {
|
|
||||||
updateField('sla_status', 'Failed')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const reload = ref(false)
|
const reload = ref(false)
|
||||||
@ -475,17 +430,13 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const detailSections = createResource({
|
const detailSections = computed(() => {
|
||||||
url: 'crm.api.doc.get_doctype_fields',
|
let data = deal.data
|
||||||
params: { doctype: 'CRM Deal' },
|
if (!data) return []
|
||||||
cache: 'dealFields',
|
return getParsedFields(data.doctype_fields, data.contacts)
|
||||||
auto: true,
|
|
||||||
transform: (data) => {
|
|
||||||
return getParsedFields(data)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function getParsedFields(sections) {
|
function getParsedFields(sections, contacts) {
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
section.fields.forEach((field) => {
|
section.fields.forEach((field) => {
|
||||||
if (['website', 'annual_revenue'].includes(field.name)) {
|
if (['website', 'annual_revenue'].includes(field.name)) {
|
||||||
@ -510,15 +461,14 @@ function getParsedFields(sections) {
|
|||||||
let contactSection = {
|
let contactSection = {
|
||||||
label: 'Contacts',
|
label: 'Contacts',
|
||||||
opened: true,
|
opened: true,
|
||||||
contacts: computed(() =>
|
contacts:
|
||||||
deal.data?.contacts.map((contact) => {
|
contacts?.map((contact) => {
|
||||||
return {
|
return {
|
||||||
name: contact.contact,
|
name: contact.contact,
|
||||||
is_primary: contact.is_primary,
|
is_primary: contact.is_primary,
|
||||||
opened: false,
|
opened: false,
|
||||||
}
|
}
|
||||||
})
|
}) || [],
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...sections, contactSection]
|
return [...sections, contactSection]
|
||||||
@ -599,4 +549,12 @@ function updateField(name, value, callback) {
|
|||||||
callback?.()
|
callback?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteDeal(name) {
|
||||||
|
await call('frappe.client.delete', {
|
||||||
|
doctype: 'CRM Deal',
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
router.push({ name: 'Deals' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -156,14 +156,25 @@ const rows = computed(() => {
|
|||||||
color: getDealStatus(deal.status)?.iconColorClass,
|
color: getDealStatus(deal.status)?.iconColorClass,
|
||||||
}
|
}
|
||||||
} else if (row == 'sla_status') {
|
} 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] = {
|
_rows[row] = {
|
||||||
label: deal.sla_status,
|
label: tooltipText,
|
||||||
color:
|
value: value,
|
||||||
deal.sla_status === 'Failed'
|
color: color,
|
||||||
? 'red'
|
|
||||||
: deal.sla_status === 'Fulfilled'
|
|
||||||
? 'green'
|
|
||||||
: 'gray',
|
|
||||||
}
|
}
|
||||||
} else if (row == 'deal_owner') {
|
} else if (row == 'deal_owner') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
@ -175,15 +186,18 @@ const rows = computed(() => {
|
|||||||
label: dateFormat(deal[row], dateTooltipFormat),
|
label: dateFormat(deal[row], dateTooltipFormat),
|
||||||
timeAgo: timeAgo(deal[row]),
|
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] = {
|
_rows[row] = {
|
||||||
label: deal.first_responded_on
|
label: deal[field] ? dateFormat(deal[field], dateTooltipFormat) : '',
|
||||||
? dateFormat(deal.first_responded_on, dateTooltipFormat)
|
|
||||||
: '',
|
|
||||||
timeAgo: deal[row]
|
timeAgo: deal[row]
|
||||||
? row == 'first_responded_on'
|
? row == 'first_response_time'
|
||||||
? timeAgo(deal[row])
|
? formatTime(deal[row])
|
||||||
: formatTime(deal[row])
|
: timeAgo(deal[row])
|
||||||
: '',
|
: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -239,7 +253,7 @@ const showNewDialog = ref(false)
|
|||||||
|
|
||||||
let newDeal = reactive({
|
let newDeal = reactive({
|
||||||
organization: '',
|
organization: '',
|
||||||
status: 'Qualification',
|
status: '',
|
||||||
email: '',
|
email: '',
|
||||||
mobile_no: '',
|
mobile_no: '',
|
||||||
deal_owner: '',
|
deal_owner: '',
|
||||||
|
|||||||
@ -4,6 +4,33 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<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
|
<Link
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:value="getUser(lead.data.lead_owner).full_name"
|
:value="getUser(lead.data.lead_owner).full_name"
|
||||||
@ -139,79 +166,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-if="lead.data.sla_status" class="flex flex-col gap-2 border-b p-5">
|
<SLASection
|
||||||
<div
|
v-if="lead.data.sla_status"
|
||||||
v-if="lead.data.sla_status == 'First Response Due'"
|
v-model="lead.data"
|
||||||
class="flex items-center gap-4 text-base leading-5"
|
@updateField="updateField"
|
||||||
>
|
/>
|
||||||
<div class="w-[106px] text-gray-600">Response By</div>
|
<div
|
||||||
<Tooltip
|
v-if="detailSections.length"
|
||||||
:text="dateFormat(lead.data.response_by, 'ddd, MMM D, YYYY h:mm A')"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
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 class="flex flex-col overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(section, i) in detailSections.data"
|
v-for="(section, i) in detailSections"
|
||||||
:key="section.label"
|
:key="section.label"
|
||||||
class="flex flex-col p-3"
|
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">
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
<SectionFields
|
<SectionFields
|
||||||
@ -252,14 +221,9 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
|||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import {
|
import { openWebsite, createToast } from '@/utils'
|
||||||
openWebsite,
|
|
||||||
createToast,
|
|
||||||
dateFormat,
|
|
||||||
timeAgo,
|
|
||||||
formatTime,
|
|
||||||
} from '@/utils'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
@ -274,7 +238,6 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Tabs,
|
Tabs,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Badge,
|
|
||||||
call,
|
call,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@ -298,15 +261,6 @@ const lead = createResource({
|
|||||||
params: { name: props.leadId },
|
params: { name: props.leadId },
|
||||||
cache: ['lead', props.leadId],
|
cache: ['lead', props.leadId],
|
||||||
auto: true,
|
auto: true,
|
||||||
onSuccess: (data) => {
|
|
||||||
if (
|
|
||||||
data.response_by &&
|
|
||||||
data.sla_status == 'First Response Due' &&
|
|
||||||
new Date(data.response_by) < new Date()
|
|
||||||
) {
|
|
||||||
updateField('sla_status', 'Failed')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const reload = ref(false)
|
const reload = ref(false)
|
||||||
@ -390,14 +344,10 @@ function validateFile(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailSections = createResource({
|
const detailSections = computed(() => {
|
||||||
url: 'crm.api.doc.get_doctype_fields',
|
let data = lead.data
|
||||||
params: { doctype: 'CRM Lead' },
|
if (!data) return []
|
||||||
cache: 'leadFields',
|
return getParsedFields(data.doctype_fields, data.contacts)
|
||||||
auto: true,
|
|
||||||
transform: (data) => {
|
|
||||||
return getParsedFields(data)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function getParsedFields(sections) {
|
function getParsedFields(sections) {
|
||||||
@ -426,6 +376,7 @@ function getParsedFields(sections) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function convertToDeal() {
|
async function convertToDeal() {
|
||||||
|
updateField('communication_status', 'Replied')
|
||||||
let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||||
lead: lead.data.name,
|
lead: lead.data.name,
|
||||||
})
|
})
|
||||||
@ -441,4 +392,12 @@ function updateField(name, value, callback) {
|
|||||||
callback?.()
|
callback?.()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteLead(name) {
|
||||||
|
await call('frappe.client.delete', {
|
||||||
|
doctype: 'CRM Lead',
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
router.push({ name: 'Leads' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -155,14 +155,25 @@ const rows = computed(() => {
|
|||||||
color: getLeadStatus(lead.status)?.iconColorClass,
|
color: getLeadStatus(lead.status)?.iconColorClass,
|
||||||
}
|
}
|
||||||
} else if (row == 'sla_status') {
|
} 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] = {
|
_rows[row] = {
|
||||||
label: lead.sla_status,
|
label: tooltipText,
|
||||||
color:
|
value: value,
|
||||||
lead.sla_status === 'Failed'
|
color: color,
|
||||||
? 'red'
|
|
||||||
: lead.sla_status === 'Fulfilled'
|
|
||||||
? 'green'
|
|
||||||
: 'gray',
|
|
||||||
}
|
}
|
||||||
} else if (row == 'lead_owner') {
|
} else if (row == 'lead_owner') {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
@ -174,15 +185,18 @@ const rows = computed(() => {
|
|||||||
label: dateFormat(lead[row], dateTooltipFormat),
|
label: dateFormat(lead[row], dateTooltipFormat),
|
||||||
timeAgo: timeAgo(lead[row]),
|
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] = {
|
_rows[row] = {
|
||||||
label: lead.first_responded_on
|
label: lead[field] ? dateFormat(lead[field], dateTooltipFormat) : '',
|
||||||
? dateFormat(lead.first_responded_on, dateTooltipFormat)
|
|
||||||
: '',
|
|
||||||
timeAgo: lead[row]
|
timeAgo: lead[row]
|
||||||
? row == 'first_responded_on'
|
? row == 'first_response_time'
|
||||||
? timeAgo(lead[row])
|
? formatTime(lead[row])
|
||||||
: formatTime(lead[row])
|
: timeAgo(lead[row])
|
||||||
: '',
|
: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +256,7 @@ let newLead = reactive({
|
|||||||
last_name: '',
|
last_name: '',
|
||||||
lead_name: '',
|
lead_name: '',
|
||||||
organization: '',
|
organization: '',
|
||||||
status: 'Open',
|
status: '',
|
||||||
email: '',
|
email: '',
|
||||||
mobile_no: '',
|
mobile_no: '',
|
||||||
lead_owner: '',
|
lead_owner: '',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
|||||||
export const statusesStore = defineStore('crm-statuses', () => {
|
export const statusesStore = defineStore('crm-statuses', () => {
|
||||||
let leadStatusesByName = reactive({})
|
let leadStatusesByName = reactive({})
|
||||||
let dealStatusesByName = reactive({})
|
let dealStatusesByName = reactive({})
|
||||||
|
let communicationStatusesByName = reactive({})
|
||||||
|
|
||||||
const leadStatuses = createListResource({
|
const leadStatuses = createListResource({
|
||||||
doctype: 'CRM Lead Status',
|
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) {
|
function colorClasses(color, onlyIcon = false) {
|
||||||
let textColor = `!text-${color}-600`
|
let textColor = `!text-${color}-600`
|
||||||
if (color == 'black') {
|
if (color == 'black') {
|
||||||
@ -55,13 +70,26 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLeadStatus(name) {
|
function getLeadStatus(name) {
|
||||||
|
if (!name) {
|
||||||
|
name = leadStatuses.data[0].name
|
||||||
|
}
|
||||||
return leadStatusesByName[name]
|
return leadStatusesByName[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDealStatus(name) {
|
function getDealStatus(name) {
|
||||||
|
if (!name) {
|
||||||
|
name = dealStatuses.data[0].name
|
||||||
|
}
|
||||||
return dealStatusesByName[name]
|
return dealStatusesByName[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCommunicationStatus(name) {
|
||||||
|
if (!name) {
|
||||||
|
name = communicationStatuses.data[0].name
|
||||||
|
}
|
||||||
|
return communicationStatuses[name]
|
||||||
|
}
|
||||||
|
|
||||||
function statusOptions(doctype, action) {
|
function statusOptions(doctype, action) {
|
||||||
let statusesByName =
|
let statusesByName =
|
||||||
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
doctype == 'deal' ? dealStatusesByName : leadStatusesByName
|
||||||
@ -84,8 +112,10 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
|||||||
return {
|
return {
|
||||||
leadStatuses,
|
leadStatuses,
|
||||||
dealStatuses,
|
dealStatuses,
|
||||||
|
communicationStatuses,
|
||||||
getLeadStatus,
|
getLeadStatus,
|
||||||
getDealStatus,
|
getDealStatus,
|
||||||
|
getCommunicationStatus,
|
||||||
statusOptions,
|
statusOptions,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user