Compare commits
No commits in common. "main" and "v1.51.0" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,5 +7,6 @@ dev-dist
|
|||||||
tags
|
tags
|
||||||
node_modules
|
node_modules
|
||||||
crm/public/frontend
|
crm/public/frontend
|
||||||
|
frontend/yarn.lock
|
||||||
crm/www/crm.html
|
crm/www/crm.html
|
||||||
build
|
build
|
||||||
|
|||||||
@ -84,14 +84,6 @@ The motivation behind building Frappe CRM stems from the need for a simple, cust
|
|||||||
- [Frappe Framework](https://github.com/frappe/frappe): A full-stack web application framework.
|
- [Frappe Framework](https://github.com/frappe/frappe): A full-stack web application framework.
|
||||||
- [Frappe UI](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
|
- [Frappe UI](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
This app is compatible with the following versions of Frappe and ERPNext:
|
|
||||||
|
|
||||||
| CRM branch | Stability | Frappe branch | ERPNext branch |
|
|
||||||
| :-------------------- | :-------- | :------------------- | :------------------- |
|
|
||||||
| main - v1.x | stable | v15.x | v15.x |
|
|
||||||
| develop - future/v2.x | unstable | develop - future/v16 | develop - future/v16 |
|
|
||||||
|
|
||||||
## Getting Started (Production)
|
## Getting Started (Production)
|
||||||
|
|
||||||
### Managed Hosting
|
### Managed Hosting
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
__version__ = "1.53.1"
|
__version__ = "1.51.0"
|
||||||
__title__ = "Frappe CRM"
|
__title__ = "Frappe CRM"
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_assignment_rules_list():
|
|
||||||
assignment_rules = []
|
|
||||||
for docname in frappe.get_all(
|
|
||||||
"Assignment Rule", filters={"document_type": ["in", ["CRM Lead", "CRM Deal"]]}
|
|
||||||
):
|
|
||||||
doc = frappe.get_value(
|
|
||||||
"Assignment Rule",
|
|
||||||
docname,
|
|
||||||
fieldname=[
|
|
||||||
"name",
|
|
||||||
"description",
|
|
||||||
"disabled",
|
|
||||||
"priority",
|
|
||||||
],
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
users_exists = bool(frappe.db.exists("Assignment Rule User", {"parent": docname.name}))
|
|
||||||
assignment_rules.append({**doc, "users_exists": users_exists})
|
|
||||||
return assignment_rules
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def duplicate_assignment_rule(docname, new_name):
|
|
||||||
doc = frappe.get_doc("Assignment Rule", docname)
|
|
||||||
doc.name = new_name
|
|
||||||
doc.assignment_rule_name = new_name
|
|
||||||
doc.insert()
|
|
||||||
return doc
|
|
||||||
139
crm/api/doc.py
139
crm/api/doc.py
@ -662,7 +662,7 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False, only_re
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def remove_assignments(doctype, name, assignees, ignore_permissions=False):
|
def remove_assignments(doctype, name, assignees, ignore_permissions=False):
|
||||||
assignees = frappe.parse_json(assignees)
|
assignees = json.loads(assignees)
|
||||||
|
|
||||||
if not assignees:
|
if not assignees:
|
||||||
return
|
return
|
||||||
@ -750,11 +750,7 @@ def getCounts(d, doctype):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_linked_docs_of_document(doctype, docname):
|
def get_linked_docs_of_document(doctype, docname):
|
||||||
try:
|
doc = frappe.get_doc(doctype, docname)
|
||||||
doc = frappe.get_doc(doctype, docname)
|
|
||||||
except frappe.DoesNotExistError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
linked_docs = get_linked_docs(doc)
|
linked_docs = get_linked_docs(doc)
|
||||||
dynamic_linked_docs = get_dynamic_linked_docs(doc)
|
dynamic_linked_docs = get_dynamic_linked_docs(doc)
|
||||||
|
|
||||||
@ -763,14 +759,7 @@ def get_linked_docs_of_document(doctype, docname):
|
|||||||
|
|
||||||
docs_data = []
|
docs_data = []
|
||||||
for doc in linked_docs:
|
for doc in linked_docs:
|
||||||
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
|
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
|
|
||||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
title = data.get("title")
|
title = data.get("title")
|
||||||
if data.doctype == "CRM Call Log":
|
if data.doctype == "CRM Call Log":
|
||||||
title = f"Call from {data.get('from')} to {data.get('to')}"
|
title = f"Call from {data.get('from')} to {data.get('to')}"
|
||||||
@ -778,9 +767,6 @@ def get_linked_docs_of_document(doctype, docname):
|
|||||||
if data.doctype == "CRM Deal":
|
if data.doctype == "CRM Deal":
|
||||||
title = data.get("organization")
|
title = data.get("organization")
|
||||||
|
|
||||||
if data.doctype == "CRM Notification":
|
|
||||||
title = data.get("message")
|
|
||||||
|
|
||||||
docs_data.append(
|
docs_data.append(
|
||||||
{
|
{
|
||||||
"doc": data.doctype,
|
"doc": data.doctype,
|
||||||
@ -793,51 +779,25 @@ def get_linked_docs_of_document(doctype, docname):
|
|||||||
|
|
||||||
|
|
||||||
def remove_doc_link(doctype, docname):
|
def remove_doc_link(doctype, docname):
|
||||||
if not doctype or not docname:
|
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||||
return
|
linked_doc_data.update(
|
||||||
|
{
|
||||||
try:
|
"reference_doctype": None,
|
||||||
linked_doc_data = frappe.get_doc(doctype, docname)
|
"reference_docname": None,
|
||||||
if doctype == "CRM Notification":
|
}
|
||||||
delete_notification_type = {
|
)
|
||||||
"notification_type_doctype": "",
|
linked_doc_data.save(ignore_permissions=True)
|
||||||
"notification_type_doc": "",
|
|
||||||
}
|
|
||||||
delete_references = {
|
|
||||||
"reference_doctype": "",
|
|
||||||
"reference_name": "",
|
|
||||||
}
|
|
||||||
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
|
|
||||||
delete_references.update(delete_notification_type)
|
|
||||||
|
|
||||||
linked_doc_data.update(delete_references)
|
|
||||||
else:
|
|
||||||
linked_doc_data.update(
|
|
||||||
{
|
|
||||||
"reference_doctype": "",
|
|
||||||
"reference_docname": "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
linked_doc_data.save(ignore_permissions=True)
|
|
||||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def remove_contact_link(doctype, docname):
|
def remove_contact_link(doctype, docname):
|
||||||
if not doctype or not docname:
|
linked_doc_data = frappe.get_doc(doctype, docname)
|
||||||
return
|
linked_doc_data.update(
|
||||||
|
{
|
||||||
try:
|
"contact": None,
|
||||||
linked_doc_data = frappe.get_doc(doctype, docname)
|
"contacts": [],
|
||||||
linked_doc_data.update(
|
}
|
||||||
{
|
)
|
||||||
"contact": None,
|
linked_doc_data.save(ignore_permissions=True)
|
||||||
"contacts": [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
linked_doc_data.save(ignore_permissions=True)
|
|
||||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -846,19 +806,13 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
|
|||||||
items = frappe.parse_json(items)
|
items = frappe.parse_json(items)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if not item.get("doctype") or not item.get("docname"):
|
if remove_contact:
|
||||||
continue
|
remove_contact_link(item["doctype"], item["docname"])
|
||||||
|
else:
|
||||||
|
remove_doc_link(item["doctype"], item["docname"])
|
||||||
|
|
||||||
try:
|
if delete:
|
||||||
if remove_contact:
|
frappe.delete_doc(item["doctype"], item["docname"])
|
||||||
remove_contact_link(item["doctype"], item["docname"])
|
|
||||||
else:
|
|
||||||
remove_doc_link(item["doctype"], item["docname"])
|
|
||||||
if delete:
|
|
||||||
frappe.delete_doc(item["doctype"], item["docname"])
|
|
||||||
except (frappe.DoesNotExistError, frappe.ValidationError):
|
|
||||||
# Skip if document doesn't exist or has validation errors
|
|
||||||
continue
|
|
||||||
|
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
@ -867,40 +821,19 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
|
|||||||
def delete_bulk_docs(doctype, items, delete_linked=False):
|
def delete_bulk_docs(doctype, items, delete_linked=False):
|
||||||
from frappe.desk.reportview import delete_bulk
|
from frappe.desk.reportview import delete_bulk
|
||||||
|
|
||||||
if not doctype:
|
|
||||||
frappe.throw("Doctype is required")
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
frappe.throw("Items are required")
|
|
||||||
|
|
||||||
items = frappe.parse_json(items)
|
items = frappe.parse_json(items)
|
||||||
if not isinstance(items, list):
|
|
||||||
frappe.throw("Items must be a list")
|
|
||||||
|
|
||||||
for doc in items:
|
for doc in items:
|
||||||
try:
|
linked_docs = get_linked_docs_of_document(doctype, doc)
|
||||||
if not frappe.db.exists(doctype, doc):
|
for linked_doc in linked_docs:
|
||||||
frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
|
remove_linked_doc_reference(
|
||||||
continue
|
[
|
||||||
|
{
|
||||||
linked_docs = get_linked_docs_of_document(doctype, doc)
|
"doctype": linked_doc["reference_doctype"],
|
||||||
for linked_doc in linked_docs:
|
"docname": linked_doc["reference_docname"],
|
||||||
if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
|
}
|
||||||
continue
|
],
|
||||||
|
remove_contact=doctype == "Contact",
|
||||||
remove_linked_doc_reference(
|
delete=delete_linked,
|
||||||
[
|
|
||||||
{
|
|
||||||
"doctype": linked_doc["reference_doctype"],
|
|
||||||
"docname": linked_doc["reference_docname"],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
remove_contact=doctype == "Contact",
|
|
||||||
delete=delete_linked,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error(
|
|
||||||
f"Error processing linked docs for {doctype} {doc}: {str(e)}", "Bulk Delete Error"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(items) > 10:
|
if len(items) > 10:
|
||||||
|
|||||||
137
crm/api/todo.py
137
crm/api/todo.py
@ -1,79 +1,90 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
|
from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
|
||||||
|
|
||||||
|
|
||||||
def after_insert(doc, method):
|
def after_insert(doc, method):
|
||||||
if doc.reference_type in ["CRM Lead", "CRM Deal"] and doc.reference_name and doc.allocated_to:
|
if (
|
||||||
fieldname = "lead_owner" if doc.reference_type == "CRM Lead" else "deal_owner"
|
doc.reference_type in ["CRM Lead", "CRM Deal"]
|
||||||
owner = frappe.db.get_value(doc.reference_type, doc.reference_name, fieldname)
|
and doc.reference_name
|
||||||
if not owner:
|
and doc.allocated_to
|
||||||
frappe.db.set_value(
|
):
|
||||||
doc.reference_type, doc.reference_name, fieldname, doc.allocated_to, update_modified=False
|
fieldname = "lead_owner" if doc.reference_type == "CRM Lead" else "deal_owner"
|
||||||
)
|
lead_owner = frappe.db.get_value(
|
||||||
|
doc.reference_type, doc.reference_name, fieldname
|
||||||
|
)
|
||||||
|
if not lead_owner:
|
||||||
|
frappe.db.set_value(
|
||||||
|
doc.reference_type, doc.reference_name, fieldname, doc.allocated_to
|
||||||
|
)
|
||||||
|
|
||||||
if doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"] and doc.reference_name and doc.allocated_to:
|
if (
|
||||||
notify_assigned_user(doc)
|
doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"]
|
||||||
|
and doc.reference_name
|
||||||
|
and doc.allocated_to
|
||||||
|
):
|
||||||
|
notify_assigned_user(doc)
|
||||||
|
|
||||||
|
|
||||||
def on_update(doc, method):
|
def on_update(doc, method):
|
||||||
if (
|
if (
|
||||||
doc.has_value_changed("status")
|
doc.has_value_changed("status")
|
||||||
and doc.status == "Cancelled"
|
and doc.status == "Cancelled"
|
||||||
and doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"]
|
and doc.reference_type in ["CRM Lead", "CRM Deal", "CRM Task"]
|
||||||
and doc.reference_name
|
and doc.reference_name
|
||||||
and doc.allocated_to
|
and doc.allocated_to
|
||||||
):
|
):
|
||||||
notify_assigned_user(doc, is_cancelled=True)
|
notify_assigned_user(doc, is_cancelled=True)
|
||||||
|
|
||||||
|
|
||||||
def notify_assigned_user(doc, is_cancelled=False):
|
def notify_assigned_user(doc, is_cancelled=False):
|
||||||
_doc = frappe.get_doc(doc.reference_type, doc.reference_name)
|
_doc = frappe.get_doc(doc.reference_type, doc.reference_name)
|
||||||
owner = frappe.get_cached_value("User", frappe.session.user, "full_name")
|
owner = frappe.get_cached_value("User", frappe.session.user, "full_name")
|
||||||
notification_text = get_notification_text(owner, doc, _doc, is_cancelled)
|
notification_text = get_notification_text(owner, doc, _doc, is_cancelled)
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
_("Your assignment on {0} {1} has been removed by {2}").format(
|
_("Your assignment on {0} {1} has been removed by {2}").format(
|
||||||
doc.reference_type, doc.reference_name, owner
|
doc.reference_type, doc.reference_name, owner
|
||||||
)
|
)
|
||||||
if is_cancelled
|
if is_cancelled
|
||||||
else _("{0} assigned a {1} {2} to you").format(owner, doc.reference_type, doc.reference_name)
|
else _("{0} assigned a {1} {2} to you").format(
|
||||||
)
|
owner, doc.reference_type, doc.reference_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc)
|
redirect_to_doctype, redirect_to_name = get_redirect_to_doc(doc)
|
||||||
|
|
||||||
notify_user(
|
notify_user(
|
||||||
{
|
{
|
||||||
"owner": frappe.session.user,
|
"owner": frappe.session.user,
|
||||||
"assigned_to": doc.allocated_to,
|
"assigned_to": doc.allocated_to,
|
||||||
"notification_type": "Assignment",
|
"notification_type": "Assignment",
|
||||||
"message": message,
|
"message": message,
|
||||||
"notification_text": notification_text,
|
"notification_text": notification_text,
|
||||||
"reference_doctype": doc.reference_type,
|
"reference_doctype": doc.reference_type,
|
||||||
"reference_docname": doc.reference_name,
|
"reference_docname": doc.reference_name,
|
||||||
"redirect_to_doctype": redirect_to_doctype,
|
"redirect_to_doctype": redirect_to_doctype,
|
||||||
"redirect_to_docname": redirect_to_name,
|
"redirect_to_docname": redirect_to_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
||||||
name = doc.reference_name
|
name = doc.reference_name
|
||||||
doctype = doc.reference_type
|
doctype = doc.reference_type
|
||||||
|
|
||||||
if doctype.startswith("CRM "):
|
if doctype.startswith("CRM "):
|
||||||
doctype = doctype[4:].lower()
|
doctype = doctype[4:].lower()
|
||||||
|
|
||||||
if doctype in ["lead", "deal"]:
|
if doctype in ["lead", "deal"]:
|
||||||
name = (
|
name = (
|
||||||
reference_doc.lead_name or name
|
reference_doc.lead_name or name
|
||||||
if doctype == "lead"
|
if doctype == "lead"
|
||||||
else reference_doc.organization or reference_doc.lead_name or name
|
else reference_doc.organization or reference_doc.lead_name or name
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_cancelled:
|
if is_cancelled:
|
||||||
return f"""
|
return f"""
|
||||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||||
<span>{ _('Your assignment on {0} {1} has been removed by {2}').format(
|
<span>{ _('Your assignment on {0} {1} has been removed by {2}').format(
|
||||||
doctype,
|
doctype,
|
||||||
@ -83,7 +94,7 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||||
<span class="font-medium text-ink-gray-9">{ owner }</span>
|
<span class="font-medium text-ink-gray-9">{ owner }</span>
|
||||||
<span>{ _('assigned a {0} {1} to you').format(
|
<span>{ _('assigned a {0} {1} to you').format(
|
||||||
@ -93,9 +104,9 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if doctype == "task":
|
if doctype == "task":
|
||||||
if is_cancelled:
|
if is_cancelled:
|
||||||
return f"""
|
return f"""
|
||||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||||
<span>{ _('Your assignment on task {0} has been removed by {1}').format(
|
<span>{ _('Your assignment on task {0} has been removed by {1}').format(
|
||||||
f'<span class="font-medium text-ink-gray-9">{ reference_doc.title }</span>',
|
f'<span class="font-medium text-ink-gray-9">{ reference_doc.title }</span>',
|
||||||
@ -103,7 +114,7 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
|||||||
) }</span>
|
) }</span>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
return f"""
|
return f"""
|
||||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||||
<span class="font-medium text-ink-gray-9">{ owner }</span>
|
<span class="font-medium text-ink-gray-9">{ owner }</span>
|
||||||
<span>{ _('assigned a new task {0} to you').format(
|
<span>{ _('assigned a new task {0} to you').format(
|
||||||
@ -114,8 +125,8 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
|
|||||||
|
|
||||||
|
|
||||||
def get_redirect_to_doc(doc):
|
def get_redirect_to_doc(doc):
|
||||||
if doc.reference_type == "CRM Task":
|
if doc.reference_type == "CRM Task":
|
||||||
reference_doc = frappe.get_doc(doc.reference_type, doc.reference_name)
|
reference_doc = frappe.get_doc(doc.reference_type, doc.reference_name)
|
||||||
return reference_doc.reference_doctype, reference_doc.reference_docname
|
return reference_doc.reference_doctype, reference_doc.reference_docname
|
||||||
|
|
||||||
return doc.reference_type, doc.reference_name
|
return doc.reference_type, doc.reference_name
|
||||||
|
|||||||
@ -10,15 +10,8 @@ from crm.fcrm.doctype.crm_notification.crm_notification import notify_user
|
|||||||
def validate(doc, method):
|
def validate(doc, method):
|
||||||
if doc.type == "Incoming" and doc.get("from"):
|
if doc.type == "Incoming" and doc.get("from"):
|
||||||
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
|
name, doctype = get_lead_or_deal_from_number(doc.get("from"))
|
||||||
if name != None:
|
doc.reference_doctype = doctype
|
||||||
doc.reference_doctype = doctype
|
doc.reference_name = name
|
||||||
doc.reference_name = name
|
|
||||||
|
|
||||||
if doc.type == "Outgoing" and doc.get("to"):
|
|
||||||
name, doctype = get_lead_or_deal_from_number(doc.get("to"))
|
|
||||||
if name != None:
|
|
||||||
doc.reference_doctype = doctype
|
|
||||||
doc.reference_name = name
|
|
||||||
|
|
||||||
|
|
||||||
def on_update(doc, method):
|
def on_update(doc, method):
|
||||||
@ -36,7 +29,7 @@ def on_update(doc, method):
|
|||||||
def notify_agent(doc):
|
def notify_agent(doc):
|
||||||
if doc.type == "Incoming":
|
if doc.type == "Incoming":
|
||||||
doctype = doc.reference_doctype
|
doctype = doc.reference_doctype
|
||||||
if doctype and doctype.startswith("CRM "):
|
if doctype.startswith("CRM "):
|
||||||
doctype = doctype[4:].lower()
|
doctype = doctype[4:].lower()
|
||||||
notification_text = f"""
|
notification_text = f"""
|
||||||
<div class="mb-2 leading-5 text-ink-gray-5">
|
<div class="mb-2 leading-5 text-ink-gray-5">
|
||||||
|
|||||||
@ -26,9 +26,8 @@ def create_default_manager_dashboard(force=False):
|
|||||||
doc.title = "Manager Dashboard"
|
doc.title = "Manager Dashboard"
|
||||||
doc.layout = default_manager_dashboard_layout()
|
doc.layout = default_manager_dashboard_layout()
|
||||||
doc.insert(ignore_permissions=True)
|
doc.insert(ignore_permissions=True)
|
||||||
else:
|
elif force:
|
||||||
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
|
doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
|
||||||
if force:
|
doc.layout = default_manager_dashboard_layout()
|
||||||
doc.layout = default_manager_dashboard_layout()
|
doc.save(ignore_permissions=True)
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
return doc.layout
|
return doc.layout
|
||||||
|
|||||||
@ -1,5 +1,20 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
from crm.api.doc import get_fields_meta
|
||||||
|
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_deal(name):
|
||||||
|
deal = frappe.get_doc("CRM Deal", name)
|
||||||
|
deal.check_permission("read")
|
||||||
|
|
||||||
|
deal = deal.as_dict()
|
||||||
|
|
||||||
|
deal["fields_meta"] = get_fields_meta("CRM Deal")
|
||||||
|
deal["_form_script"] = get_form_script("CRM Deal")
|
||||||
|
return deal
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_deal_contacts(name):
|
def get_deal_contacts(name):
|
||||||
|
|||||||
@ -129,13 +129,15 @@
|
|||||||
"fieldname": "email",
|
"fieldname": "email",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Primary Email",
|
"label": "Primary Email",
|
||||||
"options": "Email"
|
"options": "Email",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "mobile_no",
|
"fieldname": "mobile_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Primary Mobile No",
|
"label": "Primary Mobile No",
|
||||||
"options": "Phone"
|
"options": "Phone",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Qualification",
|
"default": "Qualification",
|
||||||
@ -249,7 +251,8 @@
|
|||||||
"fieldname": "phone",
|
"fieldname": "phone",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Primary Phone",
|
"label": "Primary Phone",
|
||||||
"options": "Phone"
|
"options": "Phone",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "log_tab",
|
"fieldname": "log_tab",
|
||||||
@ -432,7 +435,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-26 12:12:56.324245",
|
"modified": "2025-07-13 11:54:20.608489",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -7,8 +7,10 @@ from frappe.desk.form.assign_to import add as assign
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
|
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
|
||||||
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
|
add_status_change_log,
|
||||||
|
)
|
||||||
|
from crm.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class CRMDeal(Document):
|
class CRMDeal(Document):
|
||||||
@ -25,7 +27,7 @@ class CRMDeal(Document):
|
|||||||
add_status_change_log(self)
|
add_status_change_log(self)
|
||||||
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
|
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
|
||||||
self.closed_date = frappe.utils.nowdate()
|
self.closed_date = frappe.utils.nowdate()
|
||||||
self.validate_forecasting_fields()
|
self.validate_forcasting_fields()
|
||||||
self.validate_lost_reason()
|
self.validate_lost_reason()
|
||||||
self.update_exchange_rate()
|
self.update_exchange_rate()
|
||||||
|
|
||||||
@ -137,12 +139,12 @@ class CRMDeal(Document):
|
|||||||
if sla:
|
if sla:
|
||||||
sla.apply(self)
|
sla.apply(self)
|
||||||
|
|
||||||
def update_closed_date(self):
|
def update_close_date(self):
|
||||||
"""
|
"""
|
||||||
Update the closed date based on the "Won" status.
|
Update the close date based on the "Won" status.
|
||||||
"""
|
"""
|
||||||
if self.status == "Won" and not self.closed_date:
|
if self.status == "Won" and not self.close_date:
|
||||||
self.closed_date = frappe.utils.nowdate()
|
self.close_date = frappe.utils.nowdate()
|
||||||
|
|
||||||
def update_default_probability(self):
|
def update_default_probability(self):
|
||||||
"""
|
"""
|
||||||
@ -151,26 +153,14 @@ class CRMDeal(Document):
|
|||||||
if not self.probability or self.probability == 0:
|
if not self.probability or self.probability == 0:
|
||||||
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
|
||||||
|
|
||||||
def update_expected_deal_value(self):
|
def validate_forcasting_fields(self):
|
||||||
"""
|
self.update_close_date()
|
||||||
Update the expected deal value based on the net total or total.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
frappe.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
|
|
||||||
and (self.net_total or self.total)
|
|
||||||
and self.expected_deal_value
|
|
||||||
):
|
|
||||||
self.expected_deal_value = self.net_total or self.total
|
|
||||||
|
|
||||||
def validate_forecasting_fields(self):
|
|
||||||
self.update_closed_date()
|
|
||||||
self.update_default_probability()
|
self.update_default_probability()
|
||||||
self.update_expected_deal_value()
|
|
||||||
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
|
||||||
if not self.expected_deal_value or self.expected_deal_value == 0:
|
if not self.deal_value or self.deal_value == 0:
|
||||||
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)
|
frappe.throw(_("Deal Value is required."), frappe.MandatoryError)
|
||||||
if not self.expected_closure_date:
|
if not self.close_date:
|
||||||
frappe.throw(_("Expected Closure Date is required."), frappe.MandatoryError)
|
frappe.throw(_("Close Date is required."), frappe.MandatoryError)
|
||||||
|
|
||||||
def validate_lost_reason(self):
|
def validate_lost_reason(self):
|
||||||
"""
|
"""
|
||||||
@ -187,7 +177,7 @@ class CRMDeal(Document):
|
|||||||
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
|
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
if self.currency and self.currency != system_currency:
|
if self.currency and self.currency != system_currency:
|
||||||
exchange_rate = get_exchange_rate(self.currency, system_currency)
|
exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
|
||||||
|
|
||||||
self.db_set("exchange_rate", exchange_rate)
|
self.db_set("exchange_rate", exchange_rate)
|
||||||
|
|
||||||
|
|||||||
16
crm/fcrm/doctype/crm_lead/api.py
Normal file
16
crm/fcrm/doctype/crm_lead/api.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from crm.api.doc import get_fields_meta
|
||||||
|
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_lead(name):
|
||||||
|
lead = frappe.get_doc("CRM Lead", name)
|
||||||
|
lead.check_permission("read")
|
||||||
|
|
||||||
|
lead = lead.as_dict()
|
||||||
|
|
||||||
|
lead["fields_meta"] = get_fields_meta("CRM Lead")
|
||||||
|
lead["_form_script"] = get_form_script("CRM Lead")
|
||||||
|
return lead
|
||||||
@ -4,7 +4,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
|
from crm.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class CRMOrganization(Document):
|
class CRMOrganization(Document):
|
||||||
@ -16,7 +16,7 @@ class CRMOrganization(Document):
|
|||||||
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
|
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
if self.currency and self.currency != system_currency:
|
if self.currency and self.currency != system_currency:
|
||||||
exchange_rate = get_exchange_rate(self.currency, system_currency)
|
exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
|
||||||
|
|
||||||
self.db_set("exchange_rate", exchange_rate)
|
self.db_set("exchange_rate", exchange_rate)
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,8 @@
|
|||||||
"fieldname": "twiml_sid",
|
"fieldname": "twiml_sid",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "TwiML SID",
|
"label": "TwiML SID",
|
||||||
"permlevel": 1
|
"permlevel": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_ssqj",
|
"fieldname": "section_break_ssqj",
|
||||||
@ -104,7 +105,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-19 13:36:19.823197",
|
"modified": "2025-01-15 19:35:13.406254",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Twilio Settings",
|
"name": "CRM Twilio Settings",
|
||||||
@ -151,9 +152,8 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -128,35 +128,14 @@ def get_quotation_url(crm_deal, organization):
|
|||||||
address = address.get("name") if address else None
|
address = address.get("name") if address else None
|
||||||
|
|
||||||
if not erpnext_crm_settings.is_erpnext_in_different_site:
|
if not erpnext_crm_settings.is_erpnext_in_different_site:
|
||||||
base_url = f"{get_url_to_list('Quotation')}/new"
|
quotation_url = get_url_to_list("Quotation")
|
||||||
params = {
|
return f"{quotation_url}/new?quotation_to=CRM Deal&crm_deal={crm_deal}&party_name={crm_deal}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}"
|
||||||
"quotation_to": "CRM Deal",
|
|
||||||
"crm_deal": crm_deal,
|
|
||||||
"party_name": crm_deal,
|
|
||||||
"company": erpnext_crm_settings.erpnext_company,
|
|
||||||
"contact_person": contact,
|
|
||||||
"customer_address": address
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
site_url = erpnext_crm_settings.get("erpnext_site_url")
|
site_url = erpnext_crm_settings.get("erpnext_site_url")
|
||||||
base_url = f"{site_url}/app/quotation/new"
|
quotation_url = f"{site_url}/app/quotation"
|
||||||
prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings)
|
|
||||||
params = {
|
|
||||||
"quotation_to": "Prospect",
|
|
||||||
"crm_deal": crm_deal,
|
|
||||||
"party_name": prospect,
|
|
||||||
"company": erpnext_crm_settings.erpnext_company,
|
|
||||||
"contact_person": contact,
|
|
||||||
"customer_address": address
|
|
||||||
}
|
|
||||||
|
|
||||||
# Filter out None values and build query string
|
|
||||||
query_string = "&".join(
|
|
||||||
f"{key}={value}" for key, value in params.items()
|
|
||||||
if value is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"{base_url}?{query_string}"
|
prospect = create_prospect_in_remote_site(crm_deal, erpnext_crm_settings)
|
||||||
|
return f"{quotation_url}/new?quotation_to=Prospect&crm_deal={crm_deal}&party_name={prospect}&company={erpnext_crm_settings.erpnext_company}&contact_person={contact}&customer_address={address}"
|
||||||
|
|
||||||
|
|
||||||
def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
|
def create_prospect_in_remote_site(crm_deal, erpnext_crm_settings):
|
||||||
|
|||||||
@ -8,13 +8,7 @@
|
|||||||
"defaults_tab",
|
"defaults_tab",
|
||||||
"restore_defaults",
|
"restore_defaults",
|
||||||
"enable_forecasting",
|
"enable_forecasting",
|
||||||
"auto_update_expected_deal_value",
|
|
||||||
"currency_tab",
|
|
||||||
"currency",
|
"currency",
|
||||||
"exchange_rate_provider_section",
|
|
||||||
"service_provider",
|
|
||||||
"column_break_vqck",
|
|
||||||
"access_key",
|
|
||||||
"branding_tab",
|
"branding_tab",
|
||||||
"brand_name",
|
"brand_name",
|
||||||
"brand_logo",
|
"brand_logo",
|
||||||
@ -78,47 +72,12 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"options": "Currency"
|
"options": "Currency"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "currency_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Currency"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "exchange_rate_provider_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Exchange Rate Provider"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "frankfurter.app",
|
|
||||||
"fieldname": "service_provider",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Service Provider",
|
|
||||||
"options": "frankfurter.app\nexchangerate.host"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
|
|
||||||
"fieldname": "access_key",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Access Key",
|
|
||||||
"mandatory_depends_on": "eval:doc.service_provider == 'exchangerate.host';"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_vqck",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"description": "Automatically update \"Expected Deal Value\" based on the total value of associated products in a deal",
|
|
||||||
"fieldname": "auto_update_expected_deal_value",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Auto Update Expected Deal Value"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-16 17:33:26.406549",
|
"modified": "2025-07-13 11:58:34.857638",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "FCRM Settings",
|
"name": "FCRM Settings",
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import requests
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@ -133,76 +132,3 @@ def get_forecasting_script():
|
|||||||
this.doc.probability = status.probability
|
this.doc.probability = status.probability
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_rate(from_currency, to_currency, date=None):
|
|
||||||
if not date:
|
|
||||||
date = "latest"
|
|
||||||
|
|
||||||
api_used = "frankfurter"
|
|
||||||
|
|
||||||
api_endpoint = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
|
|
||||||
res = requests.get(api_endpoint, timeout=5)
|
|
||||||
if res.ok:
|
|
||||||
data = res.json()
|
|
||||||
return data["rates"][to_currency]
|
|
||||||
|
|
||||||
# Fallback to exchangerate.host if Frankfurter API fails
|
|
||||||
settings = FCRMSettings("FCRM Settings")
|
|
||||||
if settings and settings.service_provider == "exchangerate.host":
|
|
||||||
api_used = "exchangerate.host"
|
|
||||||
if not settings.access_key:
|
|
||||||
frappe.throw(
|
|
||||||
_("Access Key is required for Service Provider: {0}").format(
|
|
||||||
frappe.bold(settings.service_provider)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"access_key": settings.access_key,
|
|
||||||
"from": from_currency,
|
|
||||||
"to": to_currency,
|
|
||||||
"amount": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if date != "latest":
|
|
||||||
params["date"] = date
|
|
||||||
|
|
||||||
api_endpoint = "https://api.exchangerate.host/convert"
|
|
||||||
|
|
||||||
res = requests.get(api_endpoint, params=params, timeout=5)
|
|
||||||
if res.ok:
|
|
||||||
data = res.json()
|
|
||||||
return data["result"]
|
|
||||||
|
|
||||||
frappe.log_error(
|
|
||||||
title="Exchange Rate Fetch Error",
|
|
||||||
message=f"Failed to fetch exchange rate from {from_currency} to {to_currency} using {api_used} API.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if api_used == "frankfurter":
|
|
||||||
user = frappe.session.user
|
|
||||||
is_manager = (
|
|
||||||
"System Manager" in frappe.get_roles(user)
|
|
||||||
or "Sales Manager" in frappe.get_roles(user)
|
|
||||||
or user == "Administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_manager:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Ask your manager to set up the Exchange Rate Provider, as default provider does not support currency conversion for {0} to {1}."
|
|
||||||
).format(from_currency, to_currency)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Setup the Exchange Rate Provider as 'Exchangerate Host' in settings, as default provider does not support currency conversion for {0} to {1}."
|
|
||||||
).format(from_currency, to_currency)
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Failed to fetch exchange rate from {0} to {1} on {2}. Please check your internet connection or try again later."
|
|
||||||
).format(from_currency, to_currency, date)
|
|
||||||
)
|
|
||||||
|
|||||||
@ -25,8 +25,6 @@ def after_install(force=False):
|
|||||||
add_standard_dropdown_items()
|
add_standard_dropdown_items()
|
||||||
add_default_scripts()
|
add_default_scripts()
|
||||||
create_default_manager_dashboard(force)
|
create_default_manager_dashboard(force)
|
||||||
create_assignment_rule_custom_fields()
|
|
||||||
add_assignment_rule_property_setters()
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@ -194,7 +192,7 @@ def add_default_fields_layout(force=False):
|
|||||||
},
|
},
|
||||||
"CRM Deal-Data Fields": {
|
"CRM Deal-Data Fields": {
|
||||||
"doctype": "CRM Deal",
|
"doctype": "CRM Deal",
|
||||||
"layout": '[{"name":"first_tab","sections":[{"label":"Details","name":"details_section","opened":true,"columns":[{"name":"column_z9XL","fields":["organization","annual_revenue","next_step"]},{"name":"column_gM4w","fields":["website","closed_date","deal_owner"]},{"name":"column_gWmE","fields":["territory","probability"]}]},{"label":"Products","name":"section_jHhQ","opened":true,"columns":[{"name":"column_xiNF","fields":["products"]}],"editingLabel":false,"hideLabel":true},{"label":"New Section","name":"section_WNOQ","opened":true,"columns":[{"name":"column_ziBW","fields":["total"]},{"label":"","name":"column_wuwA","fields":["net_total"]}],"hideBorder":true,"hideLabel":true}]}]',
|
"layout": '[{"label": "Details", "name": "details_section", "opened": true, "columns": [{"name": "column_z9XL", "fields": ["organization", "annual_revenue", "next_step"]}, {"name": "column_gM4w", "fields": ["website", "close_date", "deal_owner"]}, {"name": "column_gWmE", "fields": ["territory", "probability"]}]}]',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,80 +421,3 @@ def add_default_scripts():
|
|||||||
for doctype in ["CRM Lead", "CRM Deal"]:
|
for doctype in ["CRM Lead", "CRM Deal"]:
|
||||||
create_product_details_script(doctype)
|
create_product_details_script(doctype)
|
||||||
create_forecasting_script()
|
create_forecasting_script()
|
||||||
|
|
||||||
|
|
||||||
def add_assignment_rule_property_setters():
|
|
||||||
"""Add a property setter to the Assignment Rule DocType for assign_condition and unassign_condition."""
|
|
||||||
|
|
||||||
default_fields = {
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"doc_type": "Assignment Rule",
|
|
||||||
"property_type": "Data",
|
|
||||||
"is_system_generated": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-assign_condition-depends_on"}):
|
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
**default_fields,
|
|
||||||
"name": "Assignment Rule-assign_condition-depends_on",
|
|
||||||
"field_name": "assign_condition",
|
|
||||||
"property": "depends_on",
|
|
||||||
"value": "eval: !doc.assign_condition_json",
|
|
||||||
}
|
|
||||||
).insert()
|
|
||||||
else:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Property Setter",
|
|
||||||
{"name": "Assignment Rule-assign_condition-depends_on"},
|
|
||||||
"value",
|
|
||||||
"eval: !doc.assign_condition_json",
|
|
||||||
)
|
|
||||||
if not frappe.db.exists("Property Setter", {"name": "Assignment Rule-unassign_condition-depends_on"}):
|
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
**default_fields,
|
|
||||||
"name": "Assignment Rule-unassign_condition-depends_on",
|
|
||||||
"field_name": "unassign_condition",
|
|
||||||
"property": "depends_on",
|
|
||||||
"value": "eval: !doc.unassign_condition_json",
|
|
||||||
}
|
|
||||||
).insert()
|
|
||||||
else:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Property Setter",
|
|
||||||
{"name": "Assignment Rule-unassign_condition-depends_on"},
|
|
||||||
"value",
|
|
||||||
"eval: !doc.unassign_condition_json",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_assignment_rule_custom_fields():
|
|
||||||
if not frappe.get_meta("Assignment Rule").has_field("assign_condition_json"):
|
|
||||||
click.secho("* Installing Custom Fields in Assignment Rule")
|
|
||||||
|
|
||||||
create_custom_fields(
|
|
||||||
{
|
|
||||||
"Assignment Rule": [
|
|
||||||
{
|
|
||||||
"description": "Autogenerated field by CRM App",
|
|
||||||
"fieldname": "assign_condition_json",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"label": "Assign Condition JSON",
|
|
||||||
"insert_after": "assign_condition",
|
|
||||||
"depends_on": "eval: doc.assign_condition_json",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Autogenerated field by CRM App",
|
|
||||||
"fieldname": "unassign_condition_json",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"label": "Unassign Condition JSON",
|
|
||||||
"insert_after": "unassign_condition",
|
|
||||||
"depends_on": "eval: doc.unassign_condition_json",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.clear_cache(doctype="Assignment Rule")
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ def set_default_calling_medium(medium):
|
|||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "CRM Telephony Agent",
|
"doctype": "CRM Telephony Agent",
|
||||||
"user": frappe.session.user,
|
"agent": frappe.session.user,
|
||||||
"default_medium": medium,
|
"default_medium": medium,
|
||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|||||||
3717
crm/locale/ar.po
3717
crm/locale/ar.po
File diff suppressed because it is too large
Load Diff
3787
crm/locale/bs.po
3787
crm/locale/bs.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/cs.po
6392
crm/locale/cs.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/da.po
6392
crm/locale/da.po
File diff suppressed because it is too large
Load Diff
4089
crm/locale/de.po
4089
crm/locale/de.po
File diff suppressed because it is too large
Load Diff
3751
crm/locale/eo.po
3751
crm/locale/eo.po
File diff suppressed because it is too large
Load Diff
3733
crm/locale/es.po
3733
crm/locale/es.po
File diff suppressed because it is too large
Load Diff
3971
crm/locale/fa.po
3971
crm/locale/fa.po
File diff suppressed because it is too large
Load Diff
3727
crm/locale/fr.po
3727
crm/locale/fr.po
File diff suppressed because it is too large
Load Diff
5177
crm/locale/hr.po
5177
crm/locale/hr.po
File diff suppressed because it is too large
Load Diff
3735
crm/locale/hu.po
3735
crm/locale/hu.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/id.po
6392
crm/locale/id.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/it.po
6392
crm/locale/it.po
File diff suppressed because it is too large
Load Diff
2637
crm/locale/main.pot
2637
crm/locale/main.pot
File diff suppressed because it is too large
Load Diff
6392
crm/locale/nb.po
6392
crm/locale/nb.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/nl.po
6392
crm/locale/nl.po
File diff suppressed because it is too large
Load Diff
3795
crm/locale/pl.po
3795
crm/locale/pl.po
File diff suppressed because it is too large
Load Diff
4407
crm/locale/pt.po
4407
crm/locale/pt.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/pt_BR.po
6392
crm/locale/pt_BR.po
File diff suppressed because it is too large
Load Diff
3755
crm/locale/ru.po
3755
crm/locale/ru.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/sr.po
6392
crm/locale/sr.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/sr_CS.po
6392
crm/locale/sr_CS.po
File diff suppressed because it is too large
Load Diff
3801
crm/locale/sv.po
3801
crm/locale/sv.po
File diff suppressed because it is too large
Load Diff
4119
crm/locale/th.po
4119
crm/locale/th.po
File diff suppressed because it is too large
Load Diff
3719
crm/locale/tr.po
3719
crm/locale/tr.po
File diff suppressed because it is too large
Load Diff
6392
crm/locale/vi.po
6392
crm/locale/vi.po
File diff suppressed because it is too large
Load Diff
4747
crm/locale/zh.po
4747
crm/locale/zh.po
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,4 @@ crm.patches.v1_0.update_layouts_to_new_format
|
|||||||
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
|
||||||
crm.patches.v1_0.create_default_scripts # 13-06-2025
|
crm.patches.v1_0.create_default_scripts # 13-06-2025
|
||||||
crm.patches.v1_0.update_deal_status_probabilities
|
crm.patches.v1_0.update_deal_status_probabilities
|
||||||
crm.patches.v1_0.update_deal_status_type
|
crm.patches.v1_0.update_deal_status_type
|
||||||
crm.patches.v1_0.create_default_lost_reasons
|
|
||||||
crm.patches.v1_0.add_fields_in_assignment_rule
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
from crm.install import (
|
|
||||||
add_assignment_rule_property_setters,
|
|
||||||
create_assignment_rule_custom_fields,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
create_assignment_rule_custom_fields()
|
|
||||||
add_assignment_rule_property_setters()
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from crm.install import add_default_lost_reasons
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
add_default_lost_reasons()
|
|
||||||
@ -267,3 +267,20 @@ def sales_user_only(fn):
|
|||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_rate(from_currency, to_currency, date=None):
|
||||||
|
if not date:
|
||||||
|
date = "latest"
|
||||||
|
|
||||||
|
url = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
|
||||||
|
|
||||||
|
response = requests.get(url)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
rate = data["rates"].get(to_currency)
|
||||||
|
return rate
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Failed to fetch historical exchange rate from external API. Please try again later."))
|
||||||
|
return None
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
files:
|
files:
|
||||||
- source: /crm/locale/main.pot
|
- source: /crm/locale/main.pot
|
||||||
translation: /crm/locale/%two_letters_code%.po
|
translation: /crm/locale/%two_letters_code%.po
|
||||||
pull_request_title: "chore: sync translations from crowdin"
|
|
||||||
pull_request_labels:
|
|
||||||
- translation
|
|
||||||
commit_message: "chore: %language% translations"
|
|
||||||
append_commit_message: false
|
|
||||||
@ -8,21 +8,21 @@ else
|
|||||||
echo "Creating new bench..."
|
echo "Creating new bench..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bench init --skip-redis-config-generation frappe-bench --version version-15
|
bench init --skip-redis-config-generation frappe-bench
|
||||||
|
|
||||||
cd frappe-bench
|
cd frappe-bench
|
||||||
|
|
||||||
# Use containers instead of localhost
|
# Use containers instead of localhost
|
||||||
bench set-mariadb-host mariadb
|
bench set-mariadb-host mariadb
|
||||||
bench set-redis-cache-host redis://redis:6379
|
bench set-redis-cache-host redis:6379
|
||||||
bench set-redis-queue-host redis://redis:6379
|
bench set-redis-queue-host redis:6379
|
||||||
bench set-redis-socketio-host redis://redis:6379
|
bench set-redis-socketio-host redis:6379
|
||||||
|
|
||||||
# Remove redis, watch from Procfile
|
# Remove redis, watch from Procfile
|
||||||
sed -i '/redis/d' ./Procfile
|
sed -i '/redis/d' ./Procfile
|
||||||
sed -i '/watch/d' ./Procfile
|
sed -i '/watch/d' ./Procfile
|
||||||
|
|
||||||
bench get-app crm --branch main
|
bench get-app crm --branch develop
|
||||||
|
|
||||||
bench new-site crm.localhost \
|
bench new-site crm.localhost \
|
||||||
--force \
|
--force \
|
||||||
@ -32,9 +32,8 @@ bench new-site crm.localhost \
|
|||||||
|
|
||||||
bench --site crm.localhost install-app crm
|
bench --site crm.localhost install-app crm
|
||||||
bench --site crm.localhost set-config developer_mode 1
|
bench --site crm.localhost set-config developer_mode 1
|
||||||
bench --site crm.localhost set-config mute_emails 1
|
|
||||||
bench --site crm.localhost set-config server_script_enabled 1
|
|
||||||
bench --site crm.localhost clear-cache
|
bench --site crm.localhost clear-cache
|
||||||
|
bench --site crm.localhost set-config mute_emails 1
|
||||||
bench use crm.localhost
|
bench use crm.localhost
|
||||||
|
|
||||||
bench start
|
bench start
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit c9a0fc937cc897864857271b3708a0c675379015
|
Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a
|
||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -2,5 +2,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
components.d.ts
|
|
||||||
10
frontend/auto-imports.d.ts
vendored
10
frontend/auto-imports.d.ts
vendored
@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
// Generated by unplugin-auto-import
|
|
||||||
// biome-ignore lint: disable
|
|
||||||
export {}
|
|
||||||
declare global {
|
|
||||||
|
|
||||||
}
|
|
||||||
17
frontend/components.d.ts
vendored
17
frontend/components.d.ts
vendored
@ -25,7 +25,6 @@ declare module 'vue' {
|
|||||||
AscendingIcon: typeof import('./src/components/Icons/AscendingIcon.vue')['default']
|
AscendingIcon: typeof import('./src/components/Icons/AscendingIcon.vue')['default']
|
||||||
AssignmentModal: typeof import('./src/components/Modals/AssignmentModal.vue')['default']
|
AssignmentModal: typeof import('./src/components/Modals/AssignmentModal.vue')['default']
|
||||||
AssignTo: typeof import('./src/components/AssignTo.vue')['default']
|
AssignTo: typeof import('./src/components/AssignTo.vue')['default']
|
||||||
AssignToBody: typeof import('./src/components/AssignToBody.vue')['default']
|
|
||||||
AttachmentArea: typeof import('./src/components/Activities/AttachmentArea.vue')['default']
|
AttachmentArea: typeof import('./src/components/Activities/AttachmentArea.vue')['default']
|
||||||
AttachmentIcon: typeof import('./src/components/Icons/AttachmentIcon.vue')['default']
|
AttachmentIcon: typeof import('./src/components/Icons/AttachmentIcon.vue')['default']
|
||||||
AttachmentItem: typeof import('./src/components/AttachmentItem.vue')['default']
|
AttachmentItem: typeof import('./src/components/AttachmentItem.vue')['default']
|
||||||
@ -33,7 +32,7 @@ declare module 'vue' {
|
|||||||
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
|
||||||
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
|
||||||
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
|
||||||
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
|
BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
|
||||||
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
|
||||||
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
|
||||||
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
|
||||||
@ -63,7 +62,6 @@ declare module 'vue' {
|
|||||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||||
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||||
CurrencySettings: typeof import('./src/components/Settings/CurrencySettings.vue')['default']
|
|
||||||
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
|
||||||
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
|
||||||
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
|
||||||
@ -85,6 +83,7 @@ declare module 'vue' {
|
|||||||
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
|
DragIcon: typeof import('./src/components/Icons/DragIcon.vue')['default']
|
||||||
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
|
DragVerticalIcon: typeof import('./src/components/Icons/DragVerticalIcon.vue')['default']
|
||||||
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
|
Dropdown: typeof import('./src/components/frappe-ui/Dropdown.vue')['default']
|
||||||
|
DropdownItem: typeof import('./src/components/DropdownItem.vue')['default']
|
||||||
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
|
DuplicateIcon: typeof import('./src/components/Icons/DuplicateIcon.vue')['default']
|
||||||
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
|
DurationIcon: typeof import('./src/components/Icons/DurationIcon.vue')['default']
|
||||||
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
|
EditEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/EditEmailTemplate.vue')['default']
|
||||||
@ -127,10 +126,11 @@ declare module 'vue' {
|
|||||||
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
|
||||||
Filter: typeof import('./src/components/Filter.vue')['default']
|
Filter: typeof import('./src/components/Filter.vue')['default']
|
||||||
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
|
||||||
ForecastingSettings: typeof import('./src/components/Settings/ForecastingSettings.vue')['default']
|
|
||||||
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
|
||||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
||||||
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
|
||||||
|
GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
|
||||||
|
GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
|
||||||
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
|
||||||
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
|
||||||
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
|
||||||
@ -141,7 +141,7 @@ declare module 'vue' {
|
|||||||
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
|
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
|
||||||
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
|
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
|
||||||
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
|
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
|
||||||
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default']
|
HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
|
||||||
Icon: typeof import('./src/components/Icon.vue')['default']
|
Icon: typeof import('./src/components/Icon.vue')['default']
|
||||||
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
|
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
|
||||||
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
|
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
|
||||||
@ -168,6 +168,9 @@ declare module 'vue' {
|
|||||||
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
|
||||||
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
|
||||||
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
LucideCalendar: typeof import('~icons/lucide/calendar')['default']
|
||||||
|
LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
|
LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
|
||||||
|
LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
|
||||||
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
|
||||||
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
|
||||||
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
|
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
|
||||||
@ -182,6 +185,7 @@ declare module 'vue' {
|
|||||||
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
|
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
|
||||||
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
|
MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
|
||||||
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
|
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
|
||||||
|
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
|
||||||
NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default']
|
NewEmailTemplate: typeof import('./src/components/Settings/EmailTemplate/NewEmailTemplate.vue')['default']
|
||||||
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
|
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
|
||||||
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
|
NoteIcon: typeof import('./src/components/Icons/NoteIcon.vue')['default']
|
||||||
@ -200,8 +204,6 @@ declare module 'vue' {
|
|||||||
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
|
||||||
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
|
||||||
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
|
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
|
||||||
PrimaryDropdown: typeof import('./src/components/PrimaryDropdown.vue')['default']
|
|
||||||
PrimaryDropdownItem: typeof import('./src/components/PrimaryDropdownItem.vue')['default']
|
|
||||||
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
|
||||||
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
|
||||||
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
|
||||||
@ -228,7 +230,6 @@ declare module 'vue' {
|
|||||||
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
|
||||||
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
SortBy: typeof import('./src/components/SortBy.vue')['default']
|
||||||
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
|
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
|
||||||
SparkleIcon: typeof import('./src/components/Icons/SparkleIcon.vue')['default']
|
|
||||||
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
|
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
|
||||||
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
|
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
|
||||||
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
|
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"@tiptap/extension-paragraph": "^2.12.0",
|
"@tiptap/extension-paragraph": "^2.12.0",
|
||||||
"@twilio/voice-sdk": "^2.10.2",
|
"@twilio/voice-sdk": "^2.10.2",
|
||||||
"@vueuse/integrations": "^10.3.0",
|
"@vueuse/integrations": "^10.3.0",
|
||||||
"frappe-ui": "^0.1.201",
|
"frappe-ui": "^0.1.171",
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<FrappeUIProvider>
|
<FrappeUIProvider>
|
||||||
<Layout v-if="session().isLoggedIn">
|
<Layout v-if="session().isLoggedIn">
|
||||||
<router-view :key="$route.fullPath"/>
|
<router-view />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Dialogs />
|
<Dialogs />
|
||||||
</FrappeUIProvider>
|
</FrappeUIProvider>
|
||||||
|
|||||||
@ -50,13 +50,11 @@
|
|||||||
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-2 px-3 sm:gap-4 sm:px-10"
|
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-2 px-3 sm:gap-4 sm:px-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="z-0 relative flex justify-center before:absolute before:left-[50%] before:-z-[1] before:top-0 before:border-l before:border-outline-gray-modals"
|
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-outline-gray-modals"
|
||||||
:class="
|
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
|
||||||
i != activities.length - 1 ? 'before:h-full' : 'before:h-4'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-7 items-center justify-center bg-surface-white"
|
class="z-10 flex h-8 w-7 items-center justify-center bg-surface-white"
|
||||||
>
|
>
|
||||||
<CommentIcon class="text-ink-gray-8" />
|
<CommentIcon class="text-ink-gray-8" />
|
||||||
</div>
|
</div>
|
||||||
@ -74,13 +72,11 @@
|
|||||||
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-3 sm:px-10"
|
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-3 sm:px-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="z-0 relative flex justify-center before:absolute before:left-[50%] before:-z-[1] before:top-0 before:border-l before:border-outline-gray-modals"
|
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-outline-gray-modals"
|
||||||
:class="
|
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
|
||||||
i != activities.length - 1 ? 'before:h-full' : 'before:h-4'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-7 items-center justify-center bg-surface-white text-ink-gray-8"
|
class="z-10 flex h-8 w-7 items-center justify-center bg-surface-white text-ink-gray-8"
|
||||||
>
|
>
|
||||||
<MissedCallIcon
|
<MissedCallIcon
|
||||||
v-if="call.status == 'No Answer'"
|
v-if="call.status == 'No Answer'"
|
||||||
@ -120,11 +116,11 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="['Activity', 'Emails'].includes(title)"
|
v-if="['Activity', 'Emails'].includes(title)"
|
||||||
class="z-0 relative flex justify-center before:absolute before:left-[50%] before:-z-[1] before:top-0 before:border-l before:border-outline-gray-modals"
|
class="relative flex justify-center before:absolute before:left-[50%] before:top-0 before:-z-10 before:border-l before:border-outline-gray-modals"
|
||||||
:class="[i != activities.length - 1 ? 'before:h-full' : 'before:h-4']"
|
:class="[i != activities.length - 1 ? 'before:h-full' : 'before:h-4']"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-7 w-7 items-center justify-center bg-surface-white"
|
class="z-10 flex h-7 w-7 items-center justify-center bg-surface-white"
|
||||||
:class="{
|
:class="{
|
||||||
'mt-2.5': ['communication'].includes(activity.activity_type),
|
'mt-2.5': ['communication'].includes(activity.activity_type),
|
||||||
'bg-surface-white': ['added', 'removed', 'changed'].includes(
|
'bg-surface-white': ['added', 'removed', 'changed'].includes(
|
||||||
@ -238,9 +234,12 @@
|
|||||||
<Button
|
<Button
|
||||||
class="!size-4"
|
class="!size-4"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:icon="SelectIcon"
|
|
||||||
@click="activity.show_others = !activity.show_others"
|
@click="activity.show_others = !activity.show_others"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SelectIcon />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@ -368,7 +367,7 @@
|
|||||||
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
|
<div v-else-if="title == 'Data'" class="h-full flex flex-col px-3 sm:px-10">
|
||||||
<DataFields
|
<DataFields
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:docname="docname"
|
:docname="doc.data.name"
|
||||||
@beforeSave="(data) => emit('beforeSave', data)"
|
@beforeSave="(data) => emit('beforeSave', data)"
|
||||||
@afterSave="(data) => emit('afterSave', data)"
|
@afterSave="(data) => emit('afterSave', data)"
|
||||||
/>
|
/>
|
||||||
@ -439,9 +438,10 @@
|
|||||||
:doc="doc"
|
:doc="doc"
|
||||||
/>
|
/>
|
||||||
<FilesUploader
|
<FilesUploader
|
||||||
|
v-if="doc.data?.name"
|
||||||
v-model="showFilesUploader"
|
v-model="showFilesUploader"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:docname="docname"
|
:docname="doc.data.name"
|
||||||
@after="
|
@after="
|
||||||
() => {
|
() => {
|
||||||
all_activities.reload()
|
all_activities.reload()
|
||||||
@ -490,7 +490,6 @@ import { timeAgo, formatDate, startCase } from '@/utils'
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
import { useDocument } from '@/data/document'
|
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { Button, Tooltip, createResource } from 'frappe-ui'
|
import { Button, Tooltip, createResource } from 'frappe-ui'
|
||||||
import { useElementVisibility } from '@vueuse/core'
|
import { useElementVisibility } from '@vueuse/core'
|
||||||
@ -514,10 +513,6 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'CRM Lead',
|
default: 'CRM Lead',
|
||||||
},
|
},
|
||||||
docname: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
tabs: {
|
tabs: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@ -528,13 +523,10 @@ const emit = defineEmits(['beforeSave', 'afterSave'])
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const doc = defineModel()
|
||||||
const reload = defineModel('reload')
|
const reload = defineModel('reload')
|
||||||
const tabIndex = defineModel('tabIndex')
|
const tabIndex = defineModel('tabIndex')
|
||||||
|
|
||||||
const { document: _document } = useDocument(props.doctype, props.docname)
|
|
||||||
|
|
||||||
const doc = computed(() => _document.doc || {})
|
|
||||||
|
|
||||||
const reload_email = ref(false)
|
const reload_email = ref(false)
|
||||||
const modalRef = ref(null)
|
const modalRef = ref(null)
|
||||||
const showFilesUploader = ref(false)
|
const showFilesUploader = ref(false)
|
||||||
@ -550,25 +542,24 @@ const changeTabTo = (tabName) => {
|
|||||||
|
|
||||||
const all_activities = createResource({
|
const all_activities = createResource({
|
||||||
url: 'crm.api.activities.get_activities',
|
url: 'crm.api.activities.get_activities',
|
||||||
params: { name: props.docname },
|
params: { name: doc.value.data.name },
|
||||||
cache: ['activity', props.docname],
|
cache: ['activity', doc.value.data.name],
|
||||||
auto: true,
|
auto: true,
|
||||||
transform: ([versions, calls, notes, tasks, attachments]) => {
|
transform: ([versions, calls, notes, tasks, attachments]) => {
|
||||||
return { versions, calls, notes, tasks, attachments }
|
return { versions, calls, notes, tasks, attachments }
|
||||||
},
|
},
|
||||||
onSuccess: () => nextTick(() => scroll()),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const showWhatsappTemplates = ref(false)
|
const showWhatsappTemplates = ref(false)
|
||||||
|
|
||||||
const whatsappMessages = createResource({
|
const whatsappMessages = createResource({
|
||||||
url: 'crm.api.whatsapp.get_whatsapp_messages',
|
url: 'crm.api.whatsapp.get_whatsapp_messages',
|
||||||
cache: ['whatsapp_messages', props.docname],
|
cache: ['whatsapp_messages', doc.value.data.name],
|
||||||
params: {
|
params: {
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_name: props.docname,
|
reference_name: doc.value.data.name,
|
||||||
},
|
},
|
||||||
auto: whatsappEnabled.value,
|
auto: true,
|
||||||
transform: (data) => sortByCreation(data),
|
transform: (data) => sortByCreation(data),
|
||||||
onSuccess: () => nextTick(() => scroll()),
|
onSuccess: () => nextTick(() => scroll()),
|
||||||
})
|
})
|
||||||
@ -581,7 +572,7 @@ onMounted(() => {
|
|||||||
$socket.on('whatsapp_message', (data) => {
|
$socket.on('whatsapp_message', (data) => {
|
||||||
if (
|
if (
|
||||||
data.reference_doctype === props.doctype &&
|
data.reference_doctype === props.doctype &&
|
||||||
data.reference_name === props.docname
|
data.reference_name === doc.value.data.name
|
||||||
) {
|
) {
|
||||||
whatsappMessages.reload()
|
whatsappMessages.reload()
|
||||||
}
|
}
|
||||||
@ -603,8 +594,8 @@ function sendTemplate(template) {
|
|||||||
url: 'crm.api.whatsapp.send_whatsapp_template',
|
url: 'crm.api.whatsapp.send_whatsapp_template',
|
||||||
params: {
|
params: {
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_name: props.docname,
|
reference_name: doc.value.data.name,
|
||||||
to: doc.value.mobile_no,
|
to: doc.value.data.mobile_no,
|
||||||
template,
|
template,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
@ -776,7 +767,6 @@ const whatsappBox = ref(null)
|
|||||||
watch([reload, reload_email], ([reload_value, reload_email_value]) => {
|
watch([reload, reload_email], ([reload_value, reload_email_value]) => {
|
||||||
if (reload_value || reload_email_value) {
|
if (reload_value || reload_email_value) {
|
||||||
all_activities.reload()
|
all_activities.reload()
|
||||||
_document.reload()
|
|
||||||
reload.value = false
|
reload.value = false
|
||||||
reload_email.value = false
|
reload_email.value = false
|
||||||
}
|
}
|
||||||
@ -802,12 +792,12 @@ function scroll(hash) {
|
|||||||
const callActions = computed(() => {
|
const callActions = computed(() => {
|
||||||
let actions = [
|
let actions = [
|
||||||
{
|
{
|
||||||
label: __('Log a Call'),
|
label: __('Create Call Log'),
|
||||||
onClick: () => modalRef.value.createCallLog(),
|
onClick: () => modalRef.value.createCallLog(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Make a Call'),
|
label: __('Make a Call'),
|
||||||
onClick: () => makeCall(doc.value.mobile_no),
|
onClick: () => makeCall(doc.data.mobile_no),
|
||||||
condition: () => callEnabled.value,
|
condition: () => callEnabled.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -9,17 +9,23 @@
|
|||||||
<Button
|
<Button
|
||||||
v-if="title == 'Emails'"
|
v-if="title == 'Emails'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('New Email')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="emailBox.show = true"
|
@click="emailBox.show = true"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('New Email') }}</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Comments'"
|
v-else-if="title == 'Comments'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('New Comment')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="emailBox.showComment = true"
|
@click="emailBox.showComment = true"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('New Comment') }}</span>
|
||||||
|
</Button>
|
||||||
<MultiActionButton
|
<MultiActionButton
|
||||||
v-else-if="title == 'Calls'"
|
v-else-if="title == 'Calls'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@ -28,45 +34,59 @@
|
|||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Notes'"
|
v-else-if="title == 'Notes'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('New Note')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="modalRef.showNote()"
|
@click="modalRef.showNote()"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('New Note') }}</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Tasks'"
|
v-else-if="title == 'Tasks'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('New Task')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="modalRef.showTask()"
|
@click="modalRef.showTask()"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('New Task') }}</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Attachments'"
|
v-else-if="title == 'Attachments'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:label="__('Upload Attachment')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="showFilesUploader = true"
|
@click="showFilesUploader = true"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('Upload Attachment') }}</span>
|
||||||
|
</Button>
|
||||||
<div class="flex gap-2 shrink-0" v-else-if="title == 'WhatsApp'">
|
<div class="flex gap-2 shrink-0" v-else-if="title == 'WhatsApp'">
|
||||||
<Button
|
<Button
|
||||||
:label="__('Send Template')"
|
:label="__('Send Template')"
|
||||||
@click="showWhatsappTemplates = true"
|
@click="showWhatsappTemplates = true"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button variant="solid" @click="whatsappBox.show()">
|
||||||
variant="solid"
|
<template #prefix>
|
||||||
:label="__('New Message')"
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
iconLeft="plus"
|
</template>
|
||||||
@click="whatsappBox.show()"
|
<span>{{ __('New Message') }}</span>
|
||||||
/>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown v-else :options="defaultActions" @click.stop>
|
<Dropdown v-else :options="defaultActions" @click.stop>
|
||||||
<template v-slot="{ open }">
|
<template v-slot="{ open }">
|
||||||
<Button
|
<Button variant="solid" class="flex items-center gap-1">
|
||||||
variant="solid"
|
<template #prefix>
|
||||||
class="flex items-center gap-1"
|
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||||
:label="__('New')"
|
</template>
|
||||||
iconLeft="plus"
|
<span>{{ __('New') }}</span>
|
||||||
:iconRight="open ? 'chevron-up' : 'chevron-down'"
|
<template #suffix>
|
||||||
/>
|
<FeatherIcon
|
||||||
|
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
@ -114,13 +134,13 @@ const defaultActions = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||||
label: __('Log a Call'),
|
label: __('Create Call Log'),
|
||||||
onClick: () => props.modalRef.createCallLog(),
|
onClick: () => props.modalRef.createCallLog(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||||
label: __('Make a Call'),
|
label: __('Make a Call'),
|
||||||
onClick: () => makeCall(props.doc.mobile_no),
|
onClick: () => makeCall(props.doc.data.mobile_no),
|
||||||
condition: () => callEnabled.value,
|
condition: () => callEnabled.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -157,14 +177,14 @@ function getTabIndex(name) {
|
|||||||
const callActions = computed(() => {
|
const callActions = computed(() => {
|
||||||
let actions = [
|
let actions = [
|
||||||
{
|
{
|
||||||
label: __('Log a Call'),
|
label: __('Create Call Log'),
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
onClick: () => props.modalRef.createCallLog(),
|
onClick: () => props.modalRef.createCallLog(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Make a Call'),
|
label: __('Make a Call'),
|
||||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||||
onClick: () => makeCall(props.doc.mobile_no),
|
onClick: () => makeCall(props.doc.data.mobile_no),
|
||||||
condition: () => callEnabled.value,
|
condition: () => callEnabled.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
v-model:reloadTasks="activities"
|
v-model:reloadTasks="activities"
|
||||||
:task="task"
|
:task="task"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:doc="doc?.name"
|
:doc="doc.data?.name"
|
||||||
@after="redirect('tasks')"
|
@after="redirect('tasks')"
|
||||||
/>
|
/>
|
||||||
<NoteModal
|
<NoteModal
|
||||||
@ -12,7 +12,7 @@
|
|||||||
v-model:reloadNotes="activities"
|
v-model:reloadNotes="activities"
|
||||||
:note="note"
|
:note="note"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:doc="doc?.name"
|
:doc="doc.data?.name"
|
||||||
@after="redirect('notes')"
|
@after="redirect('notes')"
|
||||||
/>
|
/>
|
||||||
<CallLogModal
|
<CallLogModal
|
||||||
@ -92,8 +92,8 @@ const referenceDoc = ref({})
|
|||||||
|
|
||||||
function createCallLog() {
|
function createCallLog() {
|
||||||
let doctype = props.doctype
|
let doctype = props.doctype
|
||||||
let docname = props.doc?.name
|
let docname = props.doc.data?.name
|
||||||
referenceDoc.value = { ...props.doc }
|
referenceDoc.value = { ...props.doc.data }
|
||||||
callLog.value = {
|
callLog.value = {
|
||||||
reference_doctype: doctype,
|
reference_doctype: doctype,
|
||||||
reference_docname: docname,
|
reference_docname: docname,
|
||||||
|
|||||||
@ -38,31 +38,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Button
|
<Tooltip
|
||||||
:tooltip="
|
:text="
|
||||||
attachment.is_private ? __('Make public') : __('Make private')
|
attachment.is_private ? __('Make public') : __('Make private')
|
||||||
"
|
"
|
||||||
class="!size-5"
|
|
||||||
@click.stop="
|
|
||||||
togglePrivate(attachment.name, attachment.is_private)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template #icon>
|
<div>
|
||||||
<FeatherIcon
|
<Button
|
||||||
:name="attachment.is_private ? 'lock' : 'unlock'"
|
class="!size-5"
|
||||||
class="size-3 text-ink-gray-7"
|
@click.stop="
|
||||||
/>
|
togglePrivate(attachment.name, attachment.is_private)
|
||||||
</template>
|
"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<FeatherIcon
|
||||||
:tooltip="__('Delete attachment')"
|
:name="attachment.is_private ? 'lock' : 'unlock'"
|
||||||
class="!size-5"
|
class="size-3 text-ink-gray-7"
|
||||||
@click.stop="() => deleteAttachment(attachment.name)"
|
/>
|
||||||
>
|
</Button>
|
||||||
<template #icon>
|
</div>
|
||||||
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
|
</Tooltip>
|
||||||
</template>
|
<Tooltip :text="__('Delete attachment')">
|
||||||
</Button>
|
<div>
|
||||||
|
<Button
|
||||||
|
class="!size-5"
|
||||||
|
@click.stop="() => deleteAttachment(attachment.name)"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="trash-2" class="size-3 text-ink-gray-7" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full text-sm text-ink-gray-5">
|
<div class="w-full text-sm text-ink-gray-5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button variant="ghost" @click="playPause">
|
||||||
variant="ghost"
|
<template #icon>
|
||||||
class="text-ink-gray-5"
|
<PlayIcon v-if="isPaused" class="size-4 text-ink-gray-5" />
|
||||||
:icon="isPaused ? PlayIcon : PauseIcon"
|
<PauseIcon v-else class="size-4 text-ink-gray-5" />
|
||||||
@click="playPause"
|
</template>
|
||||||
/>
|
</Button>
|
||||||
<div class="flex gap-2 items-center justify-between flex-1">
|
<div class="flex gap-2 items-center justify-between flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full slider !h-[0.5] bg-surface-gray-3 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
|
class="w-full slider !h-[0.5] bg-surface-gray-3 [&::-webkit-slider-thumb]:shadow [&::-webkit-slider-thumb:hover]:outline [&::-webkit-slider-thumb:hover]:outline-[0.5px]"
|
||||||
@ -61,11 +61,11 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown :options="options">
|
<Dropdown :options="options">
|
||||||
<Button
|
<Button variant="ghost" @click="showPlaybackSpeed = false">
|
||||||
icon="more-horizontal"
|
<template #icon>
|
||||||
variant="ghost"
|
<FeatherIcon class="size-4" name="more-horizontal" />
|
||||||
@click="showPlaybackSpeed = false"
|
</template>
|
||||||
/>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div @click="showCallLogDetailModal = true" class="cursor-pointer">
|
||||||
<div class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base">
|
<div class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base">
|
||||||
<div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
|
<div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -25,8 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@click="showCallLogDetailModal = true"
|
class="flex flex-col gap-2 border border-outline-gray-modals rounded-md bg-surface-cards px-3 py-2.5 text-ink-gray-9"
|
||||||
class="flex flex-col gap-2 border cursor-pointer border-outline-gray-modals rounded-md bg-surface-cards px-3 py-2.5 text-ink-gray-9"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="inline-flex gap-2 items-center text-base font-medium">
|
<div class="inline-flex gap-2 items-center text-base font-medium">
|
||||||
|
|||||||
@ -14,10 +14,12 @@
|
|||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="isManager() && !isMobileView"
|
v-if="isManager() && !isMobileView"
|
||||||
:tooltip="__('Edit fields layout')"
|
|
||||||
:icon="EditIcon"
|
|
||||||
@click="showDataFieldsModal = true"
|
@click="showDataFieldsModal = true"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<EditIcon />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
:disabled="!document.isDirty"
|
:disabled="!document.isDirty"
|
||||||
|
|||||||
@ -2,9 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="cursor-pointer flex flex-col rounded-md shadow bg-surface-cards px-3 py-1.5 text-base transition-all duration-300 ease-in-out"
|
class="cursor-pointer flex flex-col rounded-md shadow bg-surface-cards px-3 py-1.5 text-base transition-all duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<div
|
<div class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9">
|
||||||
class="-mb-0.5 flex items-center justify-between gap-2 truncate text-ink-gray-9"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2 truncate">
|
<div class="flex items-center gap-2 truncate">
|
||||||
<span>{{ activity.data.sender_full_name }}</span>
|
<span>{{ activity.data.sender_full_name }}</span>
|
||||||
<span class="sm:flex hidden text-sm text-ink-gray-5">
|
<span class="sm:flex hidden text-sm text-ink-gray-5">
|
||||||
@ -30,20 +28,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex gap-0.5">
|
<div class="flex gap-0.5">
|
||||||
<Button
|
<Tooltip :text="__('Reply')">
|
||||||
:tooltip="__('Reply')"
|
<div>
|
||||||
variant="ghost"
|
<Button
|
||||||
class="text-ink-gray-7"
|
variant="ghost"
|
||||||
:icon="ReplyIcon"
|
class="text-ink-gray-7"
|
||||||
@click="reply(activity.data)"
|
@click="reply(activity.data)"
|
||||||
/>
|
>
|
||||||
<Button
|
<template #icon>
|
||||||
:tooltip="__('Reply All')"
|
<ReplyIcon />
|
||||||
variant="ghost"
|
</template>
|
||||||
:icon="ReplyAllIcon"
|
</Button>
|
||||||
class="text-ink-gray-7"
|
</div>
|
||||||
@click="reply(activity.data, true)"
|
</Tooltip>
|
||||||
/>
|
<Tooltip :text="__('Reply All')">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="text-ink-gray-7"
|
||||||
|
@click="reply(activity.data, true)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ReplyAllIcon />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -41,13 +41,13 @@
|
|||||||
:options="taskStatusOptions(modalRef.updateTaskStatus, task)"
|
:options="taskStatusOptions(modalRef.updateTaskStatus, task)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip :text="__('Change Status')">
|
||||||
:tooltip="__('Change status')"
|
<div>
|
||||||
variant="ghosted"
|
<Button variant="ghosted" class="hover:bg-surface-gray-4">
|
||||||
class="hover:bg-surface-gray-4"
|
<TaskStatusIcon :status="task.status" />
|
||||||
>
|
</Button>
|
||||||
<TaskStatusIcon :status="task.status" />
|
</div>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
:options="[
|
:options="[
|
||||||
|
|||||||
@ -107,9 +107,9 @@ function sendTextMessage(event) {
|
|||||||
async function sendWhatsAppMessage() {
|
async function sendWhatsAppMessage() {
|
||||||
let args = {
|
let args = {
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_name: doc.value.name,
|
reference_name: doc.value.data.name,
|
||||||
message: content.value,
|
message: content.value,
|
||||||
to: doc.value.mobile_no,
|
to: doc.value.data.mobile_no,
|
||||||
attach: whatsapp.value.attach || '',
|
attach: whatsapp.value.attach || '',
|
||||||
reply_to: reply.value?.name || '',
|
reply_to: reply.value?.name || '',
|
||||||
content_type: whatsapp.value.content_type,
|
content_type: whatsapp.value.content_type,
|
||||||
|
|||||||
@ -1,99 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="bottom-end">
|
<component
|
||||||
<template #target="{ togglePopover }">
|
v-if="assignees?.length"
|
||||||
<div class="flex items-center" @click="togglePopover">
|
:is="assignees?.length == 1 ? 'Button' : 'div'"
|
||||||
<component
|
>
|
||||||
v-if="assignees?.length"
|
<MultipleAvatar :avatars="assignees" @click="showAssignmentModal = true" />
|
||||||
:is="assignees?.length == 1 ? 'Button' : 'div'"
|
</component>
|
||||||
>
|
<Button v-else @click="showAssignmentModal = true">
|
||||||
<MultipleAvatar :avatars="assignees" />
|
{{ __('Assign to') }}
|
||||||
</component>
|
</Button>
|
||||||
<Button v-else :label="__('Assign to')" />
|
<AssignmentModal
|
||||||
</div>
|
v-if="showAssignmentModal"
|
||||||
</template>
|
v-model="showAssignmentModal"
|
||||||
<template #body="{ isOpen }">
|
v-model:assignees="assignees"
|
||||||
<AssignToBody
|
:doctype="doctype"
|
||||||
v-show="isOpen"
|
:doc="data"
|
||||||
v-model="assignees"
|
/>
|
||||||
:docname="docname"
|
|
||||||
:doctype="doctype"
|
|
||||||
:open="isOpen"
|
|
||||||
:onUpdate="ownerField && saveAssignees"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import AssignToBody from '@/components/AssignToBody.vue'
|
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||||
import { useDocument } from '@/data/document'
|
import { ref } from 'vue'
|
||||||
import { toast, Popover } from 'frappe-ui'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
data: Object,
|
||||||
doctype: String,
|
doctype: String,
|
||||||
docname: String,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { document } = useDocument(props.doctype, props.docname)
|
const showAssignmentModal = ref(false)
|
||||||
|
|
||||||
const assignees = defineModel()
|
const assignees = defineModel()
|
||||||
|
|
||||||
const ownerField = computed(() => {
|
|
||||||
if (props.doctype === 'CRM Lead') {
|
|
||||||
return 'lead_owner'
|
|
||||||
} else if (props.doctype === 'CRM Deal') {
|
|
||||||
return 'deal_owner'
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function saveAssignees(
|
|
||||||
addedAssignees,
|
|
||||||
removedAssignees,
|
|
||||||
addAssignees,
|
|
||||||
removeAssignees,
|
|
||||||
) {
|
|
||||||
removedAssignees.length && (await removeAssignees.submit(removedAssignees))
|
|
||||||
addedAssignees.length && (await addAssignees.submit(addedAssignees))
|
|
||||||
|
|
||||||
const nextAssignee = assignees.value.find(
|
|
||||||
(a) => a.name !== document.doc[ownerField.value],
|
|
||||||
)
|
|
||||||
|
|
||||||
let owner = ownerField.value.replace('_', ' ')
|
|
||||||
|
|
||||||
if (
|
|
||||||
document.doc[ownerField.value] &&
|
|
||||||
removedAssignees.includes(document.doc[ownerField.value])
|
|
||||||
) {
|
|
||||||
document.doc[ownerField.value] = nextAssignee ? nextAssignee.name : ''
|
|
||||||
document.save.submit()
|
|
||||||
|
|
||||||
if (nextAssignee) {
|
|
||||||
toast.info(
|
|
||||||
__(
|
|
||||||
'Since you removed {0} from the assignee, the {0} has been changed to the next available assignee {1}.',
|
|
||||||
[owner, nextAssignee.label || nextAssignee.name],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
toast.info(
|
|
||||||
__(
|
|
||||||
'Since you removed {0} from the assignee, the {0} has also been removed.',
|
|
||||||
[owner],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (!document.doc[ownerField.value] && nextAssignee) {
|
|
||||||
document.doc[ownerField.value] = nextAssignee ? nextAssignee.name : ''
|
|
||||||
toast.info(
|
|
||||||
__('Since you added a new assignee, the {0} has been set to {1}.', [
|
|
||||||
owner,
|
|
||||||
nextAssignee.label || nextAssignee.name,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,211 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-2 my-2 w-[470px] rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black p-3 ring-opacity-5 focus:outline-none"
|
|
||||||
>
|
|
||||||
<div class="text-base text-ink-gray-5">{{ __('Assign to') }}</div>
|
|
||||||
<Link
|
|
||||||
class="form-control"
|
|
||||||
value=""
|
|
||||||
doctype="User"
|
|
||||||
@change="(option) => addValue(option) && ($refs.input.value = '')"
|
|
||||||
:placeholder="__('John Doe')"
|
|
||||||
:filters="{
|
|
||||||
name: ['in', users.data.crmUsers?.map((user) => user.name)],
|
|
||||||
}"
|
|
||||||
:hideMe="true"
|
|
||||||
>
|
|
||||||
<template #target="{ togglePopover }">
|
|
||||||
<div
|
|
||||||
class="w-full min-h-12 flex flex-wrap items-center gap-1.5 p-1.5 pb-5 rounded-lg bg-surface-gray-2 cursor-text"
|
|
||||||
@click.stop="togglePopover"
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
:text="assignee.name"
|
|
||||||
v-for="assignee in assignees"
|
|
||||||
:key="assignee.name"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center text-sm p-0.5 text-ink-gray-6 border border-outline-gray-1 bg-surface-modal rounded-full cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<UserAvatar :user="assignee.name" size="sm" />
|
|
||||||
<div class="ml-1">{{ getUser(assignee.name).full_name }}</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="rounded-full !size-4 m-1"
|
|
||||||
@click.stop="removeValue(assignee.name)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<FeatherIcon name="x" class="h-3 w-3 text-ink-gray-6" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item-prefix="{ option }">
|
|
||||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-label="{ option }">
|
|
||||||
<Tooltip :text="option.value">
|
|
||||||
<div class="cursor-pointer text-ink-gray-9">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div
|
|
||||||
class="text-base text-ink-gray-5 cursor-pointer select-none"
|
|
||||||
@click="assignToMe = !assignToMe"
|
|
||||||
>
|
|
||||||
{{ __('Assign to me') }}
|
|
||||||
</div>
|
|
||||||
<Switch v-model="assignToMe" @click.stop />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
|
||||||
import { capture } from '@/telemetry'
|
|
||||||
import { Tooltip, Switch, createResource } from 'frappe-ui'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
doctype: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
docname: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
open: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
onUpdate: {
|
|
||||||
type: Function,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['reload'])
|
|
||||||
|
|
||||||
const assignees = defineModel()
|
|
||||||
const oldAssignees = ref([])
|
|
||||||
const assignToMe = ref(false)
|
|
||||||
|
|
||||||
const error = ref('')
|
|
||||||
|
|
||||||
const { users, getUser } = usersStore()
|
|
||||||
|
|
||||||
const removeValue = (value) => {
|
|
||||||
if (value === getUser('').name) {
|
|
||||||
assignToMe.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
assignees.value = assignees.value.filter(
|
|
||||||
(assignee) => assignee.name !== value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addValue = (value) => {
|
|
||||||
if (value === getUser('').name) {
|
|
||||||
assignToMe.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
error.value = ''
|
|
||||||
let obj = {
|
|
||||||
name: value,
|
|
||||||
image: getUser(value).user_image,
|
|
||||||
label: getUser(value).full_name,
|
|
||||||
}
|
|
||||||
if (!assignees.value.find((assignee) => assignee.name === value)) {
|
|
||||||
assignees.value.push(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(assignToMe, (val) => {
|
|
||||||
let user = getUser('')
|
|
||||||
if (val) {
|
|
||||||
addValue(user.name)
|
|
||||||
} else {
|
|
||||||
removeValue(user.name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.open,
|
|
||||||
(val) => {
|
|
||||||
if (val) {
|
|
||||||
oldAssignees.value = [...(assignees.value || [])]
|
|
||||||
|
|
||||||
assignToMe.value = assignees.value.some(
|
|
||||||
(assignee) => assignee.name === getUser('').name,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
updateAssignees()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
async function updateAssignees() {
|
|
||||||
if (JSON.stringify(oldAssignees.value) === JSON.stringify(assignees.value))
|
|
||||||
return
|
|
||||||
|
|
||||||
const removedAssignees = oldAssignees.value
|
|
||||||
.filter(
|
|
||||||
(assignee) => !assignees.value.find((a) => a.name === assignee.name),
|
|
||||||
)
|
|
||||||
.map((assignee) => assignee.name)
|
|
||||||
|
|
||||||
const addedAssignees = assignees.value
|
|
||||||
.filter(
|
|
||||||
(assignee) => !oldAssignees.value.find((a) => a.name === assignee.name),
|
|
||||||
)
|
|
||||||
.map((assignee) => assignee.name)
|
|
||||||
|
|
||||||
if (props.onUpdate) {
|
|
||||||
props.onUpdate(
|
|
||||||
addedAssignees,
|
|
||||||
removedAssignees,
|
|
||||||
addAssignees,
|
|
||||||
removeAssignees,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (removedAssignees.length) {
|
|
||||||
await removeAssignees.submit(removedAssignees)
|
|
||||||
}
|
|
||||||
if (addedAssignees.length) {
|
|
||||||
addAssignees.submit(addedAssignees)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addAssignees = createResource({
|
|
||||||
url: 'frappe.desk.form.assign_to.add',
|
|
||||||
makeParams: (addedAssignees) => ({
|
|
||||||
doctype: props.doctype,
|
|
||||||
name: props.docname,
|
|
||||||
assign_to: addedAssignees,
|
|
||||||
}),
|
|
||||||
onSuccess: () => {
|
|
||||||
capture('assign_to', { doctype: props.doctype })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeAssignees = createResource({
|
|
||||||
url: 'crm.api.doc.remove_assignments',
|
|
||||||
makeParams: (removedAssignees) => ({
|
|
||||||
doctype: props.doctype,
|
|
||||||
name: props.docname,
|
|
||||||
assignees: removedAssignees,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -5,9 +5,11 @@
|
|||||||
:label="label"
|
:label="label"
|
||||||
theme="gray"
|
theme="gray"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:iconLeft="getIcon()"
|
|
||||||
@click="toggleDialog()"
|
@click="toggleDialog()"
|
||||||
>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<component :is="getIcon()" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<slot name="suffix" />
|
<slot name="suffix" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-ink-gray-5 text-base">
|
<div class="text-ink-gray-5">
|
||||||
{{
|
{{
|
||||||
__('Are you sure you want to delete {0} items?', [
|
__('Are you sure you want to delete {0} items?', [
|
||||||
props.items?.length,
|
props.items?.length,
|
||||||
@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-ink-gray-5 text-base">
|
<div class="text-ink-gray-5">
|
||||||
{{
|
{{
|
||||||
confirmDeleteInfo.delete
|
confirmDeleteInfo.delete
|
||||||
? __(
|
? __(
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="bottom-end">
|
<NestedPopover>
|
||||||
<template #target="{ togglePopover }">
|
<template #target>
|
||||||
<Button :label="__('Columns')" @click="togglePopover">
|
<Button :label="__('Columns')">
|
||||||
<template v-if="hideLabel">
|
<template v-if="hideLabel">
|
||||||
<ColumnsIcon class="h-4" />
|
<ColumnsIcon class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
@ -65,28 +65,37 @@
|
|||||||
<Button
|
<Button
|
||||||
class="w-full !justify-start !text-ink-gray-5"
|
class="w-full !justify-start !text-ink-gray-5"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
@click="togglePopover()"
|
||||||
:label="__('Add Column')"
|
:label="__('Add Column')"
|
||||||
iconLeft="plus"
|
>
|
||||||
@click="togglePopover"
|
<template #prefix>
|
||||||
/>
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
<Button
|
<Button
|
||||||
v-if="columnsUpdated"
|
v-if="columnsUpdated"
|
||||||
class="w-full !justify-start !text-ink-gray-5"
|
class="w-full !justify-start !text-ink-gray-5"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:label="__('Reset Changes')"
|
|
||||||
:iconLeft="ReloadIcon"
|
|
||||||
@click="reset(close)"
|
@click="reset(close)"
|
||||||
/>
|
:label="__('Reset Changes')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<ReloadIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="!is_default"
|
v-if="!is_default"
|
||||||
class="w-full !justify-start !text-ink-gray-5"
|
class="w-full !justify-start !text-ink-gray-5"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:label="__('Reset to Default')"
|
|
||||||
:iconLeft="ReloadIcon"
|
|
||||||
@click="resetToDefault(close)"
|
@click="resetToDefault(close)"
|
||||||
/>
|
:label="__('Reset to Default')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<ReloadIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@ -135,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</NestedPopover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -143,9 +152,9 @@ import ColumnsIcon from '@/components/Icons/ColumnsIcon.vue'
|
|||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import DragIcon from '@/components/Icons/DragIcon.vue'
|
import DragIcon from '@/components/Icons/DragIcon.vue'
|
||||||
import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
|
import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
|
||||||
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
import { isTouchScreenDevice } from '@/utils'
|
import { isTouchScreenDevice } from '@/utils'
|
||||||
import { Popover } from 'frappe-ui'
|
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { watchOnce } from '@vueuse/core'
|
import { watchOnce } from '@vueuse/core'
|
||||||
@ -210,7 +219,6 @@ const fields = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function addColumn(c) {
|
function addColumn(c) {
|
||||||
if (!c) return
|
|
||||||
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type)
|
let align = ['Float', 'Int', 'Percent', 'Currency'].includes(c.type)
|
||||||
? 'right'
|
? 'right'
|
||||||
: 'left'
|
: 'left'
|
||||||
|
|||||||
@ -45,12 +45,11 @@
|
|||||||
v-slot="{ togglePopover }"
|
v-slot="{ togglePopover }"
|
||||||
@update:modelValue="() => appendEmoji()"
|
@update:modelValue="() => appendEmoji()"
|
||||||
>
|
>
|
||||||
<Button
|
<Button variant="ghost" @click="togglePopover()">
|
||||||
:tooltip="__('Insert Emoji')"
|
<template #icon>
|
||||||
:icon="SmileIcon"
|
<SmileIcon class="h-4" />
|
||||||
variant="ghost"
|
</template>
|
||||||
@click="togglePopover()"
|
</Button>
|
||||||
/>
|
|
||||||
</IconPicker>
|
</IconPicker>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
:upload-args="{
|
:upload-args="{
|
||||||
@ -62,11 +61,14 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ openFileSelector }">
|
<template #default="{ openFileSelector }">
|
||||||
<Button
|
<Button
|
||||||
:tooltip="__('Attach a file')"
|
theme="gray"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:icon="AttachmentIcon"
|
|
||||||
@click="openFileSelector()"
|
@click="openFileSelector()"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<AttachmentIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,18 +8,24 @@
|
|||||||
showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
showEmailBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
||||||
]"
|
]"
|
||||||
:label="__('Reply')"
|
:label="__('Reply')"
|
||||||
:iconLeft="Email2Icon"
|
|
||||||
@click="toggleEmailBox()"
|
@click="toggleEmailBox()"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Email2Icon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:label="__('Comment')"
|
:label="__('Comment')"
|
||||||
:class="[
|
:class="[
|
||||||
showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
showCommentBox ? '!bg-surface-gray-4 hover:!bg-surface-gray-3' : '',
|
||||||
]"
|
]"
|
||||||
:iconLeft="CommentIcon"
|
|
||||||
@click="toggleCommentBox()"
|
@click="toggleCommentBox()"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<CommentIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -39,7 +45,7 @@
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
showEmailBox = false
|
showEmailBox = false
|
||||||
newEmailEditor.subject = subject
|
newEmailEditor.subject = subject
|
||||||
newEmailEditor.toEmails = doc.email ? [doc.email] : []
|
newEmailEditor.toEmails = doc.data.email ? [doc.data.email] : []
|
||||||
newEmailEditor.ccEmails = []
|
newEmailEditor.ccEmails = []
|
||||||
newEmailEditor.bccEmails = []
|
newEmailEditor.bccEmails = []
|
||||||
newEmailEditor.cc = false
|
newEmailEditor.cc = false
|
||||||
@ -48,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:editable="showEmailBox"
|
:editable="showEmailBox"
|
||||||
v-model="doc"
|
v-model="doc.data"
|
||||||
v-model:attachments="attachments"
|
v-model:attachments="attachments"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:subject="subject"
|
:subject="subject"
|
||||||
@ -73,7 +79,7 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
:editable="showCommentBox"
|
:editable="showCommentBox"
|
||||||
v-model="doc"
|
v-model="doc.data"
|
||||||
v-model:attachments="attachments"
|
v-model:attachments="attachments"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:placeholder="__('@John, can you please check this?')"
|
:placeholder="__('@John, can you please check this?')"
|
||||||
@ -119,12 +125,12 @@ const attachments = ref([])
|
|||||||
|
|
||||||
const subject = computed(() => {
|
const subject = computed(() => {
|
||||||
let prefix = ''
|
let prefix = ''
|
||||||
if (doc.value?.lead_name) {
|
if (doc.value.data?.lead_name) {
|
||||||
prefix = doc.value.lead_name
|
prefix = doc.value.data.lead_name
|
||||||
} else if (doc.value?.organization) {
|
} else if (doc.value.data?.organization) {
|
||||||
prefix = doc.value.organization
|
prefix = doc.value.data.organization
|
||||||
}
|
}
|
||||||
return `${prefix} (#${doc.value.name})`
|
return `${prefix} (#${doc.value.data.name})`
|
||||||
})
|
})
|
||||||
|
|
||||||
const signature = createResource({
|
const signature = createResource({
|
||||||
@ -193,7 +199,7 @@ async function sendMail() {
|
|||||||
subject: subject,
|
subject: subject,
|
||||||
content: newEmail.value,
|
content: newEmail.value,
|
||||||
doctype: props.doctype,
|
doctype: props.doctype,
|
||||||
name: doc.value.name,
|
name: doc.value.data.name,
|
||||||
send_email: 1,
|
send_email: 1,
|
||||||
sender: getUser().email,
|
sender: getUser().email,
|
||||||
sender_full_name: getUser()?.full_name || undefined,
|
sender_full_name: getUser()?.full_name || undefined,
|
||||||
@ -203,7 +209,7 @@ async function sendMail() {
|
|||||||
async function sendComment() {
|
async function sendComment() {
|
||||||
let comment = await call('frappe.desk.form.utils.add_comment', {
|
let comment = await call('frappe.desk.form.utils.add_comment', {
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_name: doc.value.name,
|
reference_name: doc.value.data.name,
|
||||||
content: newComment.value,
|
content: newComment.value,
|
||||||
comment_email: getUser().email,
|
comment_email: getUser().email,
|
||||||
comment_by: getUser()?.full_name || undefined,
|
comment_by: getUser()?.full_name || undefined,
|
||||||
|
|||||||
@ -1,454 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex gap-2"
|
|
||||||
:class="[
|
|
||||||
{
|
|
||||||
'items-center': !props.isGroup,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex gap-2 w-full"
|
|
||||||
:class="[
|
|
||||||
{
|
|
||||||
'items-center justify-between': !props.isGroup,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div :class="'text-end text-base text-gray-600'">
|
|
||||||
<div v-if="props.itemIndex == 0" class="min-w-[66px] text-start">
|
|
||||||
{{ __('Where') }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="min-w-[66px] flex items-start">
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
class="w-max"
|
|
||||||
@click="toggleConjunction"
|
|
||||||
icon-right="refresh-cw"
|
|
||||||
:disabled="props.itemIndex > 2"
|
|
||||||
:label="conjunction"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!props.isGroup" class="flex items-center gap-2 w-full">
|
|
||||||
<div id="fieldname" class="w-full">
|
|
||||||
<Autocomplete
|
|
||||||
:options="filterableFields.data"
|
|
||||||
v-model="props.condition[0]"
|
|
||||||
:placeholder="__('Field')"
|
|
||||||
@update:modelValue="updateField"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="operator">
|
|
||||||
<FormControl
|
|
||||||
v-if="!props.condition[0]"
|
|
||||||
disabled
|
|
||||||
type="text"
|
|
||||||
:placeholder="__('operator')"
|
|
||||||
class="w-[100px]"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
:disabled="!props.condition[0]"
|
|
||||||
type="select"
|
|
||||||
v-model="props.condition[1]"
|
|
||||||
@change="updateOperator"
|
|
||||||
:options="getOperators()"
|
|
||||||
class="w-max min-w-[100px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="value" class="w-full">
|
|
||||||
<FormControl
|
|
||||||
v-if="!props.condition[0]"
|
|
||||||
disabled
|
|
||||||
type="text"
|
|
||||||
:placeholder="__('condition')"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<component
|
|
||||||
v-else
|
|
||||||
:is="getValueControl()"
|
|
||||||
v-model="props.condition[2]"
|
|
||||||
@change="updateValue"
|
|
||||||
:placeholder="__('condition')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<CFConditions
|
|
||||||
v-if="props.isGroup && !(props.level == 2 || props.level == 4)"
|
|
||||||
:conditions="props.condition"
|
|
||||||
:isChild="true"
|
|
||||||
:level="props.level"
|
|
||||||
:disableAddCondition="props.disableAddCondition"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
v-if="props.isGroup && (props.level == 2 || props.level == 4)"
|
|
||||||
@click="show = true"
|
|
||||||
:label="__('Open nested conditions')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div :class="'w-max'">
|
|
||||||
<Dropdown placement="right" :options="dropdownOptions">
|
|
||||||
<Button variant="ghost" icon="more-horizontal" />
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Dialog
|
|
||||||
v-model="show"
|
|
||||||
:options="{ size: '3xl', title: __('Nested conditions') }"
|
|
||||||
>
|
|
||||||
<template #body-content>
|
|
||||||
<CFConditions
|
|
||||||
:conditions="props.condition"
|
|
||||||
:isChild="true"
|
|
||||||
:level="props.level"
|
|
||||||
:disableAddCondition="props.disableAddCondition"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
Autocomplete,
|
|
||||||
Button,
|
|
||||||
DatePicker,
|
|
||||||
DateRangePicker,
|
|
||||||
DateTimePicker,
|
|
||||||
Dialog,
|
|
||||||
Dropdown,
|
|
||||||
FormControl,
|
|
||||||
Rating,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { computed, defineEmits, h, ref } from 'vue'
|
|
||||||
import GroupIcon from '~icons/lucide/group'
|
|
||||||
import UnGroupIcon from '~icons/lucide/ungroup'
|
|
||||||
import CFConditions from './CFConditions.vue'
|
|
||||||
import { filterableFields } from './filterableFields'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
|
|
||||||
const show = ref(false)
|
|
||||||
const emit = defineEmits([
|
|
||||||
'remove',
|
|
||||||
'unGroupConditions',
|
|
||||||
'toggleConjunction',
|
|
||||||
'turnIntoGroup',
|
|
||||||
])
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
condition: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isChild: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
itemIndex: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
level: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
isGroup: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
conjunction: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
disableAddCondition: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dropdownOptions = computed(() => {
|
|
||||||
const options = []
|
|
||||||
|
|
||||||
if (!props.isGroup && props.level < 4) {
|
|
||||||
options.push({
|
|
||||||
label: __('Turn into a group'),
|
|
||||||
icon: () => h(GroupIcon),
|
|
||||||
onClick: () => {
|
|
||||||
emit('turnIntoGroup')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.isGroup) {
|
|
||||||
options.push({
|
|
||||||
label: __('Ungroup conditions'),
|
|
||||||
icon: () => h(UnGroupIcon),
|
|
||||||
onClick: () => {
|
|
||||||
emit('unGroupConditions')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
label: __('Remove'),
|
|
||||||
icon: 'trash-2',
|
|
||||||
variant: 'red',
|
|
||||||
onClick: () => emit('remove'),
|
|
||||||
condition: () => !props.isGroup,
|
|
||||||
})
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
label: __('Remove group'),
|
|
||||||
icon: 'trash-2',
|
|
||||||
variant: 'red',
|
|
||||||
onClick: () => emit('remove'),
|
|
||||||
condition: () => props.isGroup,
|
|
||||||
})
|
|
||||||
|
|
||||||
return options
|
|
||||||
})
|
|
||||||
|
|
||||||
const typeCheck = ['Check']
|
|
||||||
const typeLink = ['Link', 'Dynamic Link']
|
|
||||||
const typeNumber = ['Float', 'Int', 'Currency', 'Percent']
|
|
||||||
const typeSelect = ['Select']
|
|
||||||
const typeString = ['Data', 'Long Text', 'Small Text', 'Text Editor', 'Text']
|
|
||||||
const typeDate = ['Date', 'Datetime']
|
|
||||||
const typeRating = ['Rating']
|
|
||||||
|
|
||||||
function toggleConjunction() {
|
|
||||||
emit('toggleConjunction', props.conjunction)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateField = (field) => {
|
|
||||||
props.condition[0] = field?.fieldname
|
|
||||||
resetConditionValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetConditionValue = () => {
|
|
||||||
props.condition[2] = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValueControl() {
|
|
||||||
const [field, operator] = props.condition
|
|
||||||
if (!field) return null
|
|
||||||
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
|
|
||||||
if (!fieldData) return null
|
|
||||||
const { fieldtype, options } = fieldData
|
|
||||||
if (operator == 'is') {
|
|
||||||
return h(FormControl, {
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: 'Set',
|
|
||||||
value: 'set',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Not Set',
|
|
||||||
value: 'not set',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if (['like', 'not like', 'in', 'not in'].includes(operator)) {
|
|
||||||
return h(FormControl, { type: 'text' })
|
|
||||||
} else if (typeSelect.includes(fieldtype) || typeCheck.includes(fieldtype)) {
|
|
||||||
const _options =
|
|
||||||
fieldtype == 'Check' ? ['Yes', 'No'] : getSelectOptions(options)
|
|
||||||
return h(FormControl, {
|
|
||||||
type: 'select',
|
|
||||||
options: _options.map((o) => ({
|
|
||||||
label: o,
|
|
||||||
value: o,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
} else if (typeLink.includes(fieldtype)) {
|
|
||||||
if (fieldtype == 'Dynamic Link') {
|
|
||||||
return h(FormControl, { type: 'text' })
|
|
||||||
}
|
|
||||||
return h(Link, {
|
|
||||||
class: 'form-control',
|
|
||||||
doctype: options,
|
|
||||||
value: props.condition[2],
|
|
||||||
})
|
|
||||||
} else if (typeNumber.includes(fieldtype)) {
|
|
||||||
return h(FormControl, { type: 'number' })
|
|
||||||
} else if (typeDate.includes(fieldtype) && operator == 'between') {
|
|
||||||
return h(DateRangePicker, { value: props.condition[2], iconLeft: '' })
|
|
||||||
} else if (typeDate.includes(fieldtype)) {
|
|
||||||
return h(fieldtype == 'Date' ? DatePicker : DateTimePicker, {
|
|
||||||
value: props.condition[2],
|
|
||||||
iconLeft: '',
|
|
||||||
})
|
|
||||||
} else if (typeRating.includes(fieldtype)) {
|
|
||||||
return h(Rating, {
|
|
||||||
modelValue: props.condition[2] || 0,
|
|
||||||
class: 'truncate',
|
|
||||||
'update:modelValue': (v) => updateValue(v),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return h(FormControl, { type: 'text' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateValue(value) {
|
|
||||||
value = value.target ? value.target.value : value
|
|
||||||
if (props.condition[1] === 'between') {
|
|
||||||
props.condition[2] = [value.split(',')[0], value.split(',')[1]]
|
|
||||||
} else {
|
|
||||||
props.condition[2] = value + ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectOptions(options) {
|
|
||||||
return options.split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOperator(event) {
|
|
||||||
let oldOperatorValue = event.target._value
|
|
||||||
let newOperatorValue = event.target.value
|
|
||||||
props.condition[1] = event.target.value
|
|
||||||
if (!isSameTypeOperator(oldOperatorValue, newOperatorValue)) {
|
|
||||||
props.condition[2] = getDefaultValue(props.condition[0])
|
|
||||||
}
|
|
||||||
resetConditionValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperators() {
|
|
||||||
let options = []
|
|
||||||
const field = props.condition[0]
|
|
||||||
if (!field) return options
|
|
||||||
const fieldData = filterableFields.data?.find((f) => f.fieldname == field)
|
|
||||||
if (!fieldData) return options
|
|
||||||
const { fieldtype, fieldname } = fieldData
|
|
||||||
if (typeString.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'Like', value: 'like' },
|
|
||||||
{ label: 'Not Like', value: 'not like' },
|
|
||||||
{ label: 'In', value: 'in' },
|
|
||||||
{ label: 'Not In', value: 'not in' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (fieldname === '_assign') {
|
|
||||||
options = [
|
|
||||||
{ label: 'Like', value: 'like' },
|
|
||||||
{ label: 'Not Like', value: 'not like' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if (typeNumber.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'Like', value: 'like' },
|
|
||||||
{ label: 'Not Like', value: 'not like' },
|
|
||||||
{ label: 'In', value: 'in' },
|
|
||||||
{ label: 'Not In', value: 'not in' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
{ label: '<', value: '<' },
|
|
||||||
{ label: '>', value: '>' },
|
|
||||||
{ label: '<=', value: '<=' },
|
|
||||||
{ label: '>=', value: '>=' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeSelect.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'In', value: 'in' },
|
|
||||||
{ label: 'Not In', value: 'not in' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeLink.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'Like', value: 'like' },
|
|
||||||
{ label: 'Not Like', value: 'not like' },
|
|
||||||
{ label: 'In', value: 'in' },
|
|
||||||
{ label: 'Not In', value: 'not in' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeCheck.includes(fieldtype)) {
|
|
||||||
options.push(...[{ label: 'Equals', value: '==' }])
|
|
||||||
}
|
|
||||||
if (['Duration'].includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Like', value: 'like' },
|
|
||||||
{ label: 'Not Like', value: 'not like' },
|
|
||||||
{ label: 'In', value: 'in' },
|
|
||||||
{ label: 'Not In', value: 'not in' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeDate.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
{ label: '>', value: '>' },
|
|
||||||
{ label: '<', value: '<' },
|
|
||||||
{ label: '>=', value: '>=' },
|
|
||||||
{ label: '<=', value: '<=' },
|
|
||||||
{ label: 'Between', value: 'between' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (typeRating.includes(fieldtype)) {
|
|
||||||
options.push(
|
|
||||||
...[
|
|
||||||
{ label: 'Equals', value: '==' },
|
|
||||||
{ label: 'Not Equals', value: '!=' },
|
|
||||||
{ label: 'Is', value: 'is' },
|
|
||||||
{ label: '>', value: '>' },
|
|
||||||
{ label: '<', value: '<' },
|
|
||||||
{ label: '>=', value: '>=' },
|
|
||||||
{ label: '<=', value: '<=' },
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const op = options.find((o) => o.value == props.condition[1])
|
|
||||||
props.condition[1] = op?.value || options[0].value
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultValue(field) {
|
|
||||||
if (typeSelect.includes(field.fieldtype)) {
|
|
||||||
return getSelectOptions(field.options)[0]
|
|
||||||
}
|
|
||||||
if (typeCheck.includes(field.fieldtype)) {
|
|
||||||
return 'Yes'
|
|
||||||
}
|
|
||||||
if (typeDate.includes(field.fieldtype)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (typeRating.includes(field.fieldtype)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSameTypeOperator(oldOperator, newOperator) {
|
|
||||||
let textOperators = ['==', '!=', 'in', 'not in', '>', '<', '>=', '<=']
|
|
||||||
if (
|
|
||||||
textOperators.includes(oldOperator) &&
|
|
||||||
textOperators.includes(newOperator)
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="rounded-lg border border-outline-gray-2 p-3 flex flex-col gap-4 w-full">
|
|
||||||
<template v-for="(condition, i) in props.conditions" :key="condition.field">
|
|
||||||
<CFCondition
|
|
||||||
v-if="Array.isArray(condition)"
|
|
||||||
:condition="condition"
|
|
||||||
:isChild="props.isChild"
|
|
||||||
:itemIndex="i"
|
|
||||||
@remove="removeCondition(condition)"
|
|
||||||
@unGroupConditions="unGroupConditions(condition)"
|
|
||||||
:level="props.level + 1"
|
|
||||||
@toggleConjunction="toggleConjunction"
|
|
||||||
:isGroup="isGroupCondition(condition[0])"
|
|
||||||
:conjunction="getConjunction()"
|
|
||||||
@turnIntoGroup="turnIntoGroup(condition)"
|
|
||||||
:disableAddCondition="props.disableAddCondition"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div v-if="props.isChild" class="flex">
|
|
||||||
<Dropdown v-slot="{ open }" :options="dropdownOptions">
|
|
||||||
<Button
|
|
||||||
:disabled="props.disableAddCondition"
|
|
||||||
:label="__('Add condition')"
|
|
||||||
icon-left="plus"
|
|
||||||
:icon-right="open ? 'chevron-up' : 'chevron-down'"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Button, Dropdown } from 'frappe-ui'
|
|
||||||
import { computed, watch } from 'vue'
|
|
||||||
import CFCondition from './CFCondition.vue'
|
|
||||||
import { filterableFields } from './filterableFields'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
conditions: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isChild: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
level: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
disableAddCondition: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
doctype: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const getConjunction = () => {
|
|
||||||
let conjunction = 'and'
|
|
||||||
props.conditions.forEach((condition) => {
|
|
||||||
if (typeof condition == 'string') {
|
|
||||||
conjunction = condition
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return conjunction
|
|
||||||
}
|
|
||||||
|
|
||||||
const turnIntoGroup = (condition) => {
|
|
||||||
props.conditions.splice(props.conditions.indexOf(condition), 1, [condition])
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGroupCondition = (condition) => {
|
|
||||||
return Array.isArray(condition)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropdownOptions = computed(() => {
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: __('Add condition'),
|
|
||||||
onClick: () => {
|
|
||||||
const conjunction = getConjunction()
|
|
||||||
props.conditions.push(conjunction, ['', '', ''])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (props.level < 3) {
|
|
||||||
options.push({
|
|
||||||
label: __('Add condition group'),
|
|
||||||
onClick: () => {
|
|
||||||
const conjunction = getConjunction()
|
|
||||||
props.conditions.push(conjunction, [[]])
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
})
|
|
||||||
|
|
||||||
function removeCondition(condition) {
|
|
||||||
const conditionIndex = props.conditions.indexOf(condition)
|
|
||||||
if (conditionIndex == 0) {
|
|
||||||
props.conditions.splice(conditionIndex, 2)
|
|
||||||
} else {
|
|
||||||
props.conditions.splice(conditionIndex - 1, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unGroupConditions(condition) {
|
|
||||||
const conjunction = getConjunction()
|
|
||||||
const newConditions = condition.map((c) => {
|
|
||||||
if (typeof c == 'string') {
|
|
||||||
return conjunction
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
})
|
|
||||||
|
|
||||||
const index = props.conditions.indexOf(condition)
|
|
||||||
if (index !== -1) {
|
|
||||||
props.conditions.splice(index, 1, ...newConditions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleConjunction(conjunction) {
|
|
||||||
for (let i = 0; i < props.conditions.length; i++) {
|
|
||||||
if (typeof props.conditions[i] == 'string') {
|
|
||||||
props.conditions[i] = conjunction == 'and' ? 'or' : 'and'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.doctype,
|
|
||||||
(doctype) => {
|
|
||||||
filterableFields.submit({
|
|
||||||
doctype,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { createResource } from 'frappe-ui'
|
|
||||||
|
|
||||||
export const filterableFields = createResource({
|
|
||||||
url: 'crm.api.doc.get_filterable_fields',
|
|
||||||
transform: (data) => {
|
|
||||||
data = data
|
|
||||||
.filter((field) => !field.fieldname.startsWith('_'))
|
|
||||||
.map((field) => {
|
|
||||||
return {
|
|
||||||
label: field.label,
|
|
||||||
value: field.fieldname,
|
|
||||||
...field,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -52,14 +52,16 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-12">
|
<div class="w-12">
|
||||||
<Button
|
<Button
|
||||||
:tooltip="__('Edit grid fields')"
|
class="flex w-full items-center justify-center rounded !bg-surface-gray-2 border-0"
|
||||||
class="rounded !bg-surface-gray-2 border-0 !text-ink-gray-5"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon="settings"
|
|
||||||
@click="showGridFieldsEditorModal = true"
|
@click="showGridFieldsEditorModal = true"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FeatherIcon name="settings" class="size-4 text-ink-gray-7" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Rows -->
|
<!-- Rows -->
|
||||||
@ -70,7 +72,6 @@
|
|||||||
:delay="isTouchScreenDevice() ? 200 : 0"
|
:delay="isTouchScreenDevice() ? 200 : 0"
|
||||||
group="rows"
|
group="rows"
|
||||||
item-key="name"
|
item-key="name"
|
||||||
@end="reorder"
|
|
||||||
>
|
>
|
||||||
<template #item="{ element: row, index }">
|
<template #item="{ element: row, index }">
|
||||||
<div
|
<div
|
||||||
@ -276,14 +277,16 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-row flex items-center justify-center w-12">
|
<div class="edit-row w-12">
|
||||||
<Button
|
<Button
|
||||||
:tooltip="__('Edit row')"
|
class="flex w-full items-center justify-center rounded border-0"
|
||||||
class="rounded border-0 !text-ink-gray-7"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:icon="EditIcon"
|
|
||||||
@click="showRowList[index] = true"
|
@click="showRowList[index] = true"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<EditIcon class="text-ink-gray-7" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<GridRowModal
|
<GridRowModal
|
||||||
v-if="showRowList[index]"
|
v-if="showRowList[index]"
|
||||||
@ -347,6 +350,7 @@ import { usersStore } from '@/stores/users'
|
|||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import { createDocument } from '@/composables/document'
|
import { createDocument } from '@/composables/document'
|
||||||
import {
|
import {
|
||||||
|
FeatherIcon,
|
||||||
FormControl,
|
FormControl,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DateTimePicker,
|
DateTimePicker,
|
||||||
@ -516,13 +520,6 @@ const deleteRows = () => {
|
|||||||
selectedRows.clear()
|
selectedRows.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
const reorder = () => {
|
|
||||||
rows.value.forEach((row, index) => {
|
|
||||||
row.idx = index + 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function fieldChange(value, field, row) {
|
function fieldChange(value, field, row) {
|
||||||
triggerOnChange(field.fieldname, value, row)
|
triggerOnChange(field.fieldname, value, row)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,10 +54,13 @@
|
|||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<Button
|
<Button
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
:label="__('Add Field')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #item-label="{ option }">
|
<template #item-label="{ option }">
|
||||||
<div class="flex flex-col gap-1 text-ink-gray-9">
|
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||||
@ -72,7 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex items-center gap-2 justify-end">
|
<div class="flex flex-col gap-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="dirty"
|
v-if="dirty"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|||||||
@ -11,18 +11,19 @@
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="isManager()"
|
v-if="isManager()"
|
||||||
:tooltip="__('Edit fields layout')"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-7"
|
class="w-7"
|
||||||
:icon="EditIcon"
|
|
||||||
@click="openGridRowFieldsModal"
|
@click="openGridRowFieldsModal"
|
||||||
/>
|
>
|
||||||
<Button
|
<template #icon>
|
||||||
icon="x"
|
<EditIcon />
|
||||||
variant="ghost"
|
</template>
|
||||||
class="w-7"
|
</Button>
|
||||||
@click="show = false"
|
<Button variant="ghost" class="w-7" @click="show = false">
|
||||||
/>
|
<template #icon>
|
||||||
|
<FeatherIcon name="x" class="size-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
:file-types="image_type"
|
:file-types="image_type"
|
||||||
|
class="text-base"
|
||||||
@success="
|
@success="
|
||||||
(file) => {
|
(file) => {
|
||||||
$emit('upload', file.file_url)
|
$emit('upload', file.file_url)
|
||||||
@ -9,28 +10,21 @@
|
|||||||
>
|
>
|
||||||
<template v-slot="{ progress, uploading, openFileSelector }">
|
<template v-slot="{ progress, uploading, openFileSelector }">
|
||||||
<div class="flex items-end space-x-1">
|
<div class="flex items-end space-x-1">
|
||||||
<Button
|
<Button @click="openFileSelector">
|
||||||
@click="openFileSelector"
|
{{
|
||||||
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
|
|
||||||
:label="
|
|
||||||
uploading
|
uploading
|
||||||
? __('Uploading {0}%', [progress])
|
? `Uploading ${progress}%`
|
||||||
: image_url
|
: image_url
|
||||||
? __('Change')
|
? 'Change'
|
||||||
: __('Upload')
|
: 'Upload'
|
||||||
"
|
}}
|
||||||
/>
|
</Button>
|
||||||
<Button
|
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
|
||||||
v-if="image_url"
|
|
||||||
:label="__('Remove')"
|
|
||||||
@click="$emit('remove')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ImageUpIcon from '~icons/lucide/image-up'
|
|
||||||
import { FileUploader, Button } from 'frappe-ui'
|
import { FileUploader, Button } from 'frappe-ui'
|
||||||
|
|
||||||
const prop = defineProps({
|
const prop = defineProps({
|
||||||
@ -39,6 +33,10 @@ const prop = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'image/*',
|
default: 'image/*',
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['upload', 'remove'])
|
const emit = defineEmits(['upload', 'remove'])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -48,18 +48,24 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full !justify-start"
|
class="w-full !justify-start"
|
||||||
:label="__('Create New')"
|
:label="__('Create New')"
|
||||||
iconLeft="plus"
|
|
||||||
@click="() => attrs.onCreate(value, close)"
|
@click="() => attrs.onCreate(value, close)"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full !justify-start"
|
class="w-full !justify-start"
|
||||||
:label="__('Clear')"
|
:label="__('Clear')"
|
||||||
iconLeft="x"
|
|
||||||
@click="() => clearValue(close)"
|
@click="() => clearValue(close)"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="x" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
|
|||||||
@ -18,10 +18,14 @@
|
|||||||
:key="g.label"
|
:key="g.label"
|
||||||
>
|
>
|
||||||
<Dropdown :options="g.action" v-slot="{ open }">
|
<Dropdown :options="g.action" v-slot="{ open }">
|
||||||
<Button
|
<Button :label="g.label">
|
||||||
:label="g.label"
|
<template #suffix>
|
||||||
:iconRight="open ? 'chevron-up' : 'chevron-down'"
|
<FeatherIcon
|
||||||
/>
|
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,12 +5,7 @@
|
|||||||
class="flex h-full w-full rounded shadow overflow-hidden cursor-pointer"
|
class="flex h-full w-full rounded shadow overflow-hidden cursor-pointer"
|
||||||
>
|
>
|
||||||
<Tooltip :text="__(item.data.tooltip)">
|
<Tooltip :text="__(item.data.tooltip)">
|
||||||
<NumberChart
|
<NumberChart v-if="item.data" :key="index" :config="item.data" />
|
||||||
class="!items-start"
|
|
||||||
v-if="item.data"
|
|
||||||
:key="index"
|
|
||||||
:config="item.data"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<Dialog v-model="show" :options="{ size: 'xl' }">
|
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||||
<template #body v-if="!confirmDeleteInfo.show">
|
<template #body v-if="!confirmDeleteInfo.show">
|
||||||
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
|
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
|
||||||
{{
|
{{
|
||||||
@ -32,12 +32,11 @@
|
|||||||
{
|
{
|
||||||
label: 'Document',
|
label: 'Document',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
width: '19rem',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Master',
|
label: 'Master',
|
||||||
key: 'reference_doctype',
|
key: 'reference_doctype',
|
||||||
width: '12rem',
|
width: '30%',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
@selectionsChanged="
|
@selectionsChanged="
|
||||||
|
|||||||
@ -19,36 +19,53 @@
|
|||||||
v-if="editMode"
|
v-if="editMode"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:label="__('Save')"
|
:label="__('Save')"
|
||||||
|
size="sm"
|
||||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||||
@click="saveOption"
|
@click="saveOption"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Tooltip text="Set As Primary" v-if="!isNew && !option.selected">
|
||||||
v-if="!isNew && !option.selected"
|
<div>
|
||||||
:tooltip="__('Set As Primary')"
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:icon="SuccessIcon"
|
size="sm"
|
||||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||||
@click="option.onClick"
|
@click="option.onClick"
|
||||||
/>
|
>
|
||||||
<Button
|
<template #icon>
|
||||||
v-if="!editMode"
|
<SuccessIcon />
|
||||||
:tooltip="__('Edit')"
|
</template>
|
||||||
variant="ghost"
|
</Button>
|
||||||
:icon="EditIcon"
|
</div>
|
||||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
</Tooltip>
|
||||||
@click="toggleEditMode"
|
<Tooltip v-if="!editMode" text="Edit">
|
||||||
/>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
:tooltip="__('Delete')"
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
icon="x"
|
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||||
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
@click="toggleEditMode"
|
||||||
@click="() => option.onDelete(option, isNew)"
|
>
|
||||||
/>
|
<template #icon>
|
||||||
|
<EditIcon />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Delete">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
size="sm"
|
||||||
|
class="opacity-0 hover:bg-surface-gray-4 group-hover:opacity-100"
|
||||||
|
@click="() => option.onDelete(option, isNew)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="option.selected">
|
<div v-if="option.selected">
|
||||||
<FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" />
|
<FeatherIcon name="check" class="text-ink-gray-5 h-4 w-6" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -56,7 +73,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import { TextInput } from 'frappe-ui'
|
import { TextInput, Tooltip } from 'frappe-ui'
|
||||||
import { nextTick, ref, onMounted } from 'vue'
|
import { nextTick, ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -123,12 +123,11 @@
|
|||||||
v-slot="{ togglePopover }"
|
v-slot="{ togglePopover }"
|
||||||
@update:modelValue="() => appendEmoji()"
|
@update:modelValue="() => appendEmoji()"
|
||||||
>
|
>
|
||||||
<Button
|
<Button variant="ghost" @click="togglePopover()">
|
||||||
:tooltip="__('Insert Emoji')"
|
<template #icon>
|
||||||
:icon="SmileIcon"
|
<SmileIcon class="h-4" />
|
||||||
variant="ghost"
|
</template>
|
||||||
@click="togglePopover()"
|
</Button>
|
||||||
/>
|
|
||||||
</IconPicker>
|
</IconPicker>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
:upload-args="{
|
:upload-args="{
|
||||||
@ -139,20 +138,21 @@
|
|||||||
@success="(f) => attachments.push(f)"
|
@success="(f) => attachments.push(f)"
|
||||||
>
|
>
|
||||||
<template #default="{ openFileSelector }">
|
<template #default="{ openFileSelector }">
|
||||||
<Button
|
<Button variant="ghost" @click="openFileSelector()">
|
||||||
:tooltip="__('Attach a file')"
|
<template #icon>
|
||||||
:icon="AttachmentIcon"
|
<AttachmentIcon class="h-4" />
|
||||||
variant="ghost"
|
</template>
|
||||||
@click="openFileSelector()"
|
</Button>
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</FileUploader>
|
</FileUploader>
|
||||||
<Button
|
<Button
|
||||||
:tooltip="__('Insert Email Template')"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:icon="EmailTemplateIcon"
|
|
||||||
@click="showEmailTemplateSelectorModal = true"
|
@click="showEmailTemplateSelectorModal = true"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<EmailTemplateIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
|
<div class="mt-2 flex items-center justify-end space-x-2 sm:mt-0">
|
||||||
<Button v-bind="discardButtonProps || {}" :label="__('Discard')" />
|
<Button v-bind="discardButtonProps || {}" :label="__('Discard')" />
|
||||||
|
|||||||
@ -89,9 +89,12 @@
|
|||||||
v-if="data[field.fieldname] && field.edit"
|
v-if="data[field.fieldname] && field.edit"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
:label="__('Edit')"
|
:label="__('Edit')"
|
||||||
:iconLeft="EditIcon"
|
|
||||||
@click="field.edit(data[field.fieldname])"
|
@click="field.edit(data[field.fieldname])"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<EditIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TableMultiselectInput
|
<TableMultiselectInput
|
||||||
|
|||||||
@ -169,10 +169,13 @@
|
|||||||
<Button
|
<Button
|
||||||
class="w-full !h-8 !bg-surface-modal"
|
class="w-full !h-8 !bg-surface-modal"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:label="__('Add Field')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #item-label="{ option }">
|
<template #item-label="{ option }">
|
||||||
@ -195,7 +198,6 @@
|
|||||||
class="w-full h-8"
|
class="w-full h-8"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
:label="__('Add Section')"
|
:label="__('Add Section')"
|
||||||
iconLeft="plus"
|
|
||||||
@click="
|
@click="
|
||||||
tabs[tabIndex].sections.push({
|
tabs[tabIndex].sections.push({
|
||||||
label: __('New Section'),
|
label: __('New Section'),
|
||||||
@ -204,7 +206,11 @@
|
|||||||
columns: [{ name: 'column_' + getRandom(), fields: [] }],
|
columns: [{ name: 'column_' + getRandom(), fields: [] }],
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -29,7 +29,6 @@
|
|||||||
filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera
|
filesUploaderArea?.showWebLink || filesUploaderArea?.showCamera
|
||||||
"
|
"
|
||||||
:label="isMobileView ? __('Back') : __('Back to file upload')"
|
:label="isMobileView ? __('Back') : __('Back to file upload')"
|
||||||
iconLeft="arrow-left"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
filesUploaderArea.showWebLink = false
|
filesUploaderArea.showWebLink = false
|
||||||
@ -38,7 +37,11 @@
|
|||||||
filesUploaderArea.cameraImage = null
|
filesUploaderArea.cameraImage = null
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="arrow-left" class="size-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="
|
v-if="
|
||||||
filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage
|
filesUploaderArea?.showCamera && !filesUploaderArea?.cameraImage
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="bottom-end">
|
<NestedPopover>
|
||||||
<template #target="{ togglePopover, close }">
|
<template #target>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
:label="__('Filter')"
|
:label="__('Filter')"
|
||||||
:class="filters?.size ? 'rounded-r-none' : ''"
|
:class="filters?.size ? 'rounded-r-none' : ''"
|
||||||
:iconLeft="FilterIcon"
|
|
||||||
@click="togglePopover"
|
|
||||||
>
|
>
|
||||||
|
<template #prefix><FilterIcon class="h-4" /></template>
|
||||||
<template v-if="filters?.size" #suffix>
|
<template v-if="filters?.size" #suffix>
|
||||||
<div
|
<div
|
||||||
class="flex h-5 w-5 items-center justify-center rounded-[5px] bg-surface-white pt-px text-xs font-medium text-ink-gray-8 shadow-sm"
|
class="flex h-5 w-5 items-center justify-center rounded-[5px] bg-surface-white pt-px text-xs font-medium text-ink-gray-8 shadow-sm"
|
||||||
@ -16,13 +15,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
|
||||||
v-if="filters?.size"
|
<div>
|
||||||
:tooltip="__('Clear all Filter')"
|
<Button
|
||||||
class="rounded-l-none border-l"
|
class="rounded-l-none border-l"
|
||||||
icon="x"
|
icon="x"
|
||||||
@click.stop="clearfilter(close)"
|
@click.stop="clearfilter(false)"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #body="{ close }">
|
<template #body="{ close }">
|
||||||
@ -133,10 +134,13 @@
|
|||||||
<Button
|
<Button
|
||||||
class="!text-ink-gray-5"
|
class="!text-ink-gray-5"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:label="__('Add Filter')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
:label="__('Add Filter')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
<Button
|
<Button
|
||||||
@ -150,16 +154,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</NestedPopover>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
import FilterIcon from '@/components/Icons/FilterIcon.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
createResource,
|
createResource,
|
||||||
Popover,
|
Tooltip,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DateTimePicker,
|
DateTimePicker,
|
||||||
DateRangePicker,
|
DateRangePicker,
|
||||||
@ -480,7 +485,7 @@ function removeFilter(index) {
|
|||||||
function clearfilter(close) {
|
function clearfilter(close) {
|
||||||
filters.value.clear()
|
filters.value.clear()
|
||||||
apply()
|
apply()
|
||||||
close()
|
close && close()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateValue(value, filter) {
|
function updateValue(value, filter) {
|
||||||
|
|||||||
@ -7,10 +7,18 @@
|
|||||||
? groupByValue?.label
|
? groupByValue?.label
|
||||||
: __('Group By: ') + groupByValue?.label
|
: __('Group By: ') + groupByValue?.label
|
||||||
"
|
"
|
||||||
:iconLeft="DetailsIcon"
|
|
||||||
:iconRight="isOpen ? 'chevron-up' : 'chevron-down'"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<DetailsIcon />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
:name="isOpen ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Popover } from 'frappe-ui'
|
import Popover from '@/components/frappe-ui/Popover.vue'
|
||||||
import { gemoji } from 'gemoji'
|
import { gemoji } from 'gemoji'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-settings2-icon lucide-settings-2"
|
|
||||||
>
|
|
||||||
<path d="M14 17H5" />
|
|
||||||
<path d="M19 7h-9" />
|
|
||||||
<circle cx="17" cy="17" r="3" />
|
|
||||||
<circle cx="7" cy="7" r="3" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M3.37543 1.93494L4.21632 2.21494C4.35232 2.26027 4.44388 2.38738 4.44388 2.53138C4.44388 2.67538 4.35143 2.80249 4.21543 2.84783L3.37454 3.12783L3.09365 3.9696C3.04921 4.10472 2.92121 4.19716 2.7781 4.19716C2.63499 4.19716 2.50787 4.1056 2.46254 3.9696L2.18165 3.12783L1.34076 2.84783C1.20476 2.80249 1.11232 2.67538 1.11232 2.53138C1.11232 2.38738 1.20476 2.26027 1.34076 2.21494L2.18165 1.93494L2.46254 1.09316C2.55321 0.82116 3.00387 0.82116 3.09454 1.09316L3.37543 1.93494ZM8.44852 1.33394C8.3643 1.16325 8.19046 1.05518 8.00012 1.05518C7.80978 1.05518 7.63595 1.16325 7.55173 1.33394L5.67697 5.13368L1.48388 5.74214C1.29552 5.76947 1.13901 5.90137 1.08017 6.08238C1.02133 6.26339 1.07036 6.46211 1.20665 6.59497L4.24065 9.55281L3.52421 13.7284C3.49203 13.916 3.56913 14.1056 3.7231 14.2174C3.87706 14.3293 4.08119 14.3441 4.24966 14.2555L8.11188 12.2253C8.35631 12.0968 8.4503 11.7945 8.32181 11.5501C8.19333 11.3057 7.89102 11.2117 7.64659 11.3402L4.68114 12.899L5.2707 9.46284C5.29853 9.30065 5.24477 9.13514 5.12693 9.02027L2.63025 6.58626L6.08082 6.08555C6.24373 6.06191 6.38457 5.95959 6.45741 5.81196L8.00012 2.6852L9.54284 5.81196C9.61568 5.95959 9.75652 6.06191 9.91943 6.08555L13.37 6.58625L11.6235 8.2887C11.4258 8.48146 11.4218 8.79802 11.6145 8.99575C11.8073 9.19349 12.1239 9.19752 12.3216 9.00476L14.7936 6.59498C14.9299 6.46212 14.9789 6.2634 14.9201 6.08239C14.8612 5.90138 14.7047 5.76947 14.5164 5.74214L10.3233 5.13368L8.44852 1.33394ZM13.4744 11.9911L12.3517 11.6168L11.9775 10.4942C11.8557 10.1315 11.2557 10.1315 11.1339 10.4942L10.7597 11.6168L9.63702 11.9911C9.45569 12.0515 9.33302 12.2213 9.33302 12.4124C9.33302 12.6035 9.45569 12.7733 9.63702 12.8337L10.7597 13.2079L11.1339 14.3306C11.1944 14.5119 11.365 14.6346 11.5561 14.6346C11.7472 14.6346 11.917 14.5119 11.9784 14.3306L12.3526 13.2079L13.4752 12.8337C13.6566 12.7733 13.7792 12.6035 13.7792 12.4124C13.7792 12.2213 13.6566 12.0515 13.4752 11.9911H13.4744ZM13.3333 2.88883C13.3333 3.25702 13.0349 3.5555 12.6667 3.5555C12.2985 3.5555 12 3.25702 12 2.88883C12 2.52064 12.2985 2.22217 12.6667 2.22217C13.0349 2.22217 13.3333 2.52064 13.3333 2.88883Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
@ -3,8 +3,11 @@
|
|||||||
:label="__('Kanban Settings')"
|
:label="__('Kanban Settings')"
|
||||||
@click="showDialog = true"
|
@click="showDialog = true"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:iconLeft="KanbanIcon"
|
>
|
||||||
/>
|
<template #prefix>
|
||||||
|
<KanbanIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }">
|
<Dialog v-model="showDialog" :options="{ title: __('Kanban Settings') }">
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div>
|
<div>
|
||||||
@ -20,8 +23,8 @@
|
|||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<Button
|
<Button
|
||||||
class="w-full !justify-start"
|
class="w-full !justify-start"
|
||||||
:label="columnField.label"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
|
:label="columnField.label"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
@ -77,10 +80,13 @@
|
|||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<Button
|
<Button
|
||||||
class="w-full mt-2"
|
class="w-full mt-2"
|
||||||
:label="__('Add Field')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #item-label="{ option }">
|
<template #item-label="{ option }">
|
||||||
<div class="flex flex-col gap-1 text-ink-gray-9">
|
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||||
|
|||||||
@ -15,18 +15,17 @@
|
|||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center group justify-between">
|
<div class="flex gap-2 items-center group justify-between">
|
||||||
<div class="flex items-center text-base">
|
<div class="flex items-center text-base">
|
||||||
<Popover>
|
<NestedPopover>
|
||||||
<template #target="{ togglePopover }">
|
<template #target>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="hover:!bg-surface-gray-2"
|
class="hover:!bg-surface-gray-2"
|
||||||
@click="togglePopover"
|
|
||||||
>
|
>
|
||||||
<IndicatorIcon :class="parseColor(column.column.color)" />
|
<IndicatorIcon :class="parseColor(column.column.color)" />
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body="{ close }">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 px-3 py-2.5 min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="flex flex-col gap-3 px-3 py-2.5 min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
>
|
>
|
||||||
@ -49,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</NestedPopover>
|
||||||
<div class="text-ink-gray-9">{{ column.column.name }}</div>
|
<div class="text-ink-gray-9">{{ column.column.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@ -154,10 +153,13 @@
|
|||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<Button
|
<Button
|
||||||
class="w-full mt-2.5 mb-1 mr-5"
|
class="w-full mt-2.5 mb-1 mr-5"
|
||||||
:label="__('Add Column')"
|
|
||||||
iconLeft="plus"
|
|
||||||
@click="togglePopover()"
|
@click="togglePopover()"
|
||||||
/>
|
:label="__('Add Column')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
</div>
|
</div>
|
||||||
@ -165,10 +167,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import { isTouchScreenDevice, colors, parseColor } from '@/utils'
|
import { isTouchScreenDevice, colors, parseColor } from '@/utils'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { Dropdown, Popover } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
|
class="relative flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
|
||||||
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
|
:class="isSidebarCollapsed ? 'w-12' : 'w-[220px]'"
|
||||||
>
|
>
|
||||||
<div class="p-2">
|
<div>
|
||||||
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<div class="mb-3 flex flex-col">
|
<div class="mb-3 flex flex-col">
|
||||||
@ -197,50 +197,51 @@ const isSidebarCollapsed = useStorage('isSidebarCollapsed', false)
|
|||||||
const isFCSite = ref(window.is_fc_site)
|
const isFCSite = ref(window.is_fc_site)
|
||||||
const isDemoSite = ref(window.is_demo_site)
|
const isDemoSite = ref(window.is_demo_site)
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
label: 'Dashboard',
|
|
||||||
icon: LucideLayoutDashboard,
|
|
||||||
to: 'Dashboard',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Leads',
|
|
||||||
icon: LeadsIcon,
|
|
||||||
to: 'Leads',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Deals',
|
|
||||||
icon: DealsIcon,
|
|
||||||
to: 'Deals',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Contacts',
|
|
||||||
icon: ContactsIcon,
|
|
||||||
to: 'Contacts',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Organizations',
|
|
||||||
icon: OrganizationsIcon,
|
|
||||||
to: 'Organizations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Notes',
|
|
||||||
icon: NoteIcon,
|
|
||||||
to: 'Notes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tasks',
|
|
||||||
icon: TaskIcon,
|
|
||||||
to: 'Tasks',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Call Logs',
|
|
||||||
icon: PhoneIcon,
|
|
||||||
to: 'Call Logs',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const allViews = computed(() => {
|
const allViews = computed(() => {
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
icon: LucideLayoutDashboard,
|
||||||
|
to: 'Dashboard',
|
||||||
|
condition: () => isManager(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Leads',
|
||||||
|
icon: LeadsIcon,
|
||||||
|
to: 'Leads',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deals',
|
||||||
|
icon: DealsIcon,
|
||||||
|
to: 'Deals',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Contacts',
|
||||||
|
icon: ContactsIcon,
|
||||||
|
to: 'Contacts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Organizations',
|
||||||
|
icon: OrganizationsIcon,
|
||||||
|
to: 'Organizations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Notes',
|
||||||
|
icon: NoteIcon,
|
||||||
|
to: 'Notes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tasks',
|
||||||
|
icon: TaskIcon,
|
||||||
|
to: 'Tasks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Call Logs',
|
||||||
|
icon: PhoneIcon,
|
||||||
|
to: 'Call Logs',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
let _views = [
|
let _views = [
|
||||||
{
|
{
|
||||||
name: 'All Views',
|
name: 'All Views',
|
||||||
|
|||||||
@ -106,8 +106,6 @@ function convertToDeal(selections, unselectAll) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteValues(selections, unselectAll) {
|
function deleteValues(selections, unselectAll) {
|
||||||
unselectAllAction.value = unselectAll
|
|
||||||
|
|
||||||
const selectedDocs = Array.from(selections)
|
const selectedDocs = Array.from(selections)
|
||||||
if (selectedDocs.length == 1) {
|
if (selectedDocs.length == 1) {
|
||||||
showDeleteDocModal.value = {
|
showDeleteDocModal.value = {
|
||||||
@ -219,12 +217,6 @@ function bulkActions(selections, unselectAll) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reload(unselectAll) {
|
function reload(unselectAll) {
|
||||||
showDeleteDocModal.value = {
|
|
||||||
showLinkedDocsModal: false,
|
|
||||||
showDeleteModal: false,
|
|
||||||
docname: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
unselectAllAction.value?.()
|
unselectAllAction.value?.()
|
||||||
unselectAll?.()
|
unselectAll?.()
|
||||||
list.value?.reload()
|
list.value?.reload()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user