Compare commits

...

27 Commits

Author SHA1 Message Date
Shariq Ansari
899b09ac40
Merge branch 'develop' into pot_develop_2025-03-30 2025-04-07 16:51:07 +05:30
Shariq Ansari
debc9fc1cb
Merge pull request #716 from shariquerik/make-create-call
fix: Create & Make call
2025-04-07 16:49:38 +05:30
Shariq Ansari
5c76adedf3
Merge pull request #712 from shariquerik/dynamic-link
feat: Dynamic Link field support
2025-04-07 16:49:30 +05:30
Shariq Ansari
1ebb26e4c2
Merge pull request #708 from frappe/pot_develop_2025-04-06
chore: update POT file
2025-04-07 16:44:59 +05:30
Shariq Ansari
67378c1f52
Merge pull request #719 from pratikb64/default-assigned-to
fix: default "assigned to" in deals and leads list view
2025-04-07 16:44:32 +05:30
Pratik
469a22ef5f fix: default "assigned to" in deals and leads list view 2025-04-07 16:37:19 +05:30
Shariq Ansari
fdceb51fdc fix: added multi action button to make and create call 2025-04-07 15:34:46 +05:30
Shariq Ansari
97a132e05f fix: show call tab always 2025-04-07 15:32:34 +05:30
Shariq Ansari
26fabddcbe fix: handle feather icon in multi action button 2025-04-07 15:32:09 +05:30
Shariq Ansari
40370067b2 fix: dynamic variant 2025-04-07 14:13:55 +05:30
Shariq Ansari
f0bf6962e7 fix: do not show dropdown if only one option 2025-04-07 14:07:41 +05:30
Shariq Ansari
3b432a0209 fix: added multi action button 2025-04-07 13:58:58 +05:30
Shariq Ansari
c7a03922a0 feat: Dynamic Link field support 2025-04-07 13:16:52 +05:30
frappe-pr-bot
e70b4c091e chore: update POT file 2025-04-06 09:35:28 +00:00
Pratik Badhe
7e38d5e405
Merge pull request #707 from pratikb64/kanban-filter-fix
fix: kanban filter
2025-04-04 17:14:46 +05:30
Pratik
f810e82b45 fix: kanban filter 2025-04-04 17:07:54 +05:30
Pratik Badhe
dff9f93a6b
Merge pull request #704 from pratikb64/make-fields-mandatory
fix: add mandatory fields
2025-04-04 10:26:29 +05:30
Shariq Ansari
c4109ad6ac build(deps): bump frappeui to 0.1.123 2025-04-04 10:09:18 +05:30
Pratik
7a6efb900e fix: add mandatory fields 2025-04-01 17:26:46 +05:30
frappe-pr-bot
e080e47a35 chore: update POT file 2025-03-30 09:35:00 +00:00
Pratik Badhe
82599f91d8
Merge pull request #698 from pratikb64/email-settings-fix
fix: ui alignment
2025-03-28 15:34:36 +05:30
Pratik
8fa156f625 fix: ui alignment 2025-03-28 15:33:40 +05:30
Pratik Badhe
55112cefa9
Merge pull request #697 from pratikb64/email-setting-fix
fix: broken images
2025-03-27 17:38:28 +05:30
Pratik
152c7c8a91 fix: broken images 2025-03-27 17:37:39 +05:30
Pratik Badhe
aa1c0da80e
Merge pull request #696 from pratikb64/add-email-setting
feat: add email account
2025-03-27 15:33:20 +05:30
Pratik
87174f207d feat: add email account 2025-03-27 15:32:37 +05:30
Shariq Ansari
400f879d29 fix: only allow invite by email for Sales Manager & Sales User role 2025-03-26 14:44:40 +05:30
42 changed files with 1209 additions and 333 deletions

View File

@ -94,8 +94,13 @@ def accept_invitation(key: str | None = None):
@frappe.whitelist() @frappe.whitelist()
def invite_by_email(emails: str, role: str): def invite_by_email(emails: str, role: str):
frappe.only_for("Sales Manager") frappe.only_for("Sales Manager")
if role not in ["Sales Manager", "Sales User"]:
frappe.throw("Cannot invite for this role")
if not emails: if not emails:
return return
email_string = validate_email_address(emails, throw=False) email_string = validate_email_address(emails, throw=False)
email_list = split_emails(email_string) email_list = split_emails(email_string)
if not email_list: if not email_list:

View File

@ -418,16 +418,23 @@ def get_data(
rows.append(field) rows.append(field)
for kc in kanban_columns: for kc in kanban_columns:
column_filters = {column_field: kc.get("name")} # Start with base filters
column_filters = []
# Convert and add the main filters first
if filters:
base_filters = convert_filter_to_tuple(doctype, filters)
column_filters.extend(base_filters)
# Add the column-specific filter
if column_field and kc.get("name"):
column_filters.append([doctype, column_field, "=", kc.get("name")])
order = kc.get("order") order = kc.get("order")
if (column_field in filters and filters.get(column_field) != kc.get("name")) or kc.get("delete"): if kc.get("delete"):
column_data = [] column_data = []
else: else:
column_filters.update(filters.copy()) page_length = kc.get("page_length", 20)
page_length = 20
if kc.get("page_length"):
page_length = kc.get("page_length")
if order: if order:
column_data = get_records_based_on_order( column_data = get_records_based_on_order(
@ -437,26 +444,20 @@ def get_data(
column_data = frappe.get_list( column_data = frappe.get_list(
doctype, doctype,
fields=rows, fields=rows,
filters=convert_filter_to_tuple(doctype, column_filters), filters=column_filters,
order_by=order_by, order_by=order_by,
page_length=page_length, page_length=page_length,
) )
new_filters = filters.copy()
new_filters.update({column_field: kc.get("name")})
all_count = frappe.get_list( all_count = frappe.get_list(
doctype, doctype,
filters=convert_filter_to_tuple(doctype, new_filters), filters=column_filters,
fields="count(*) as total_count", fields="count(*) as total_count",
)[0].total_count )[0].total_count
kc["all_count"] = all_count kc["all_count"] = all_count
kc["count"] = len(column_data) kc["count"] = len(column_data)
for d in column_data:
getCounts(d, doctype)
if order: if order:
column_data = sorted( column_data = sorted(
column_data, column_data,

99
crm/api/settings.py Normal file
View File

@ -0,0 +1,99 @@
import frappe
@frappe.whitelist()
def create_email_account(data):
service = data.get("service")
service_config = email_service_config.get(service)
if not service_config:
return "Service not supported"
try:
email_doc = frappe.get_doc(
{
"doctype": "Email Account",
"email_id": data.get("email_id"),
"email_account_name": data.get("email_account_name"),
"service": service,
"enable_incoming": data.get("enable_incoming"),
"enable_outgoing": data.get("enable_outgoing"),
"default_incoming": data.get("default_incoming"),
"default_outgoing": data.get("default_outgoing"),
"email_sync_option": "ALL",
"initial_sync_count": 100,
"create_contact": 1,
"track_email_status": 1,
"use_tls": 1,
"use_imap": 1,
"smtp_port": 587,
**service_config,
}
)
if service == "Frappe Mail":
email_doc.api_key = data.get("api_key")
email_doc.api_secret = data.get("api_secret")
email_doc.frappe_mail_site = data.get("frappe_mail_site")
email_doc.append_to = "CRM Lead"
else:
email_doc.append("imap_folder", {"append_to": "CRM Lead", "folder_name": "INBOX"})
email_doc.password = data.get("password")
# validate whether the credentials are correct
email_doc.get_incoming_server()
# if correct credentials, save the email account
email_doc.save()
except Exception as e:
frappe.throw(str(e))
email_service_config = {
"Frappe Mail": {
"domain": None,
"password": None,
"awaiting_password": 0,
"ascii_encode_password": 0,
"login_id_is_different": 0,
"login_id": None,
"use_imap": 0,
"use_ssl": 0,
"validate_ssl_certificate": 0,
"use_starttls": 0,
"email_server": None,
"incoming_port": 0,
"always_use_account_email_id_as_sender": 1,
"use_tls": 0,
"use_ssl_for_outgoing": 0,
"smtp_server": None,
"smtp_port": None,
"no_smtp_authentication": 0,
},
"GMail": {
"email_server": "imap.gmail.com",
"use_ssl": 1,
"smtp_server": "smtp.gmail.com",
},
"Outlook": {
"email_server": "imap-mail.outlook.com",
"use_ssl": 1,
"smtp_server": "smtp-mail.outlook.com",
},
"Sendgrid": {
"smtp_server": "smtp.sendgrid.net",
"smtp_port": 587,
},
"SparkPost": {
"smtp_server": "smtp.sparkpostmail.com",
},
"Yahoo": {
"email_server": "imap.mail.yahoo.com",
"use_ssl": 1,
"smtp_server": "smtp.mail.yahoo.com",
"smtp_port": 587,
},
"Yandex": {
"email_server": "imap.yandex.com",
"use_ssl": 1,
"smtp_server": "smtp.yandex.com",
"smtp_port": 587,
},
}

View File

@ -41,13 +41,15 @@
"fieldname": "from", "fieldname": "from",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "From" "label": "From",
"reqd": 1
}, },
{ {
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled" "options": "Initiated\nRinging\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
"reqd": 1
}, },
{ {
"fieldname": "start_time", "fieldname": "start_time",
@ -69,13 +71,15 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Type", "label": "Type",
"options": "Incoming\nOutgoing" "options": "Incoming\nOutgoing",
"reqd": 1
}, },
{ {
"fieldname": "to", "fieldname": "to",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "To" "label": "To",
"reqd": 1
}, },
{ {
"description": "Call duration in seconds", "description": "Call duration in seconds",
@ -153,7 +157,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-01-22 17:57:59.289548", "modified": "2025-04-01 16:01:54.479309",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Call Log", "name": "CRM Call Log",

View File

@ -19,7 +19,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Title" "label": "Title",
"reqd": 1
}, },
{ {
"fieldname": "content", "fieldname": "content",
@ -49,7 +50,7 @@
"link_fieldname": "note" "link_fieldname": "note"
} }
], ],
"modified": "2024-01-19 21:56:30.123334", "modified": "2025-04-01 15:30:14.742001",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "FCRM Note", "name": "FCRM Note",

View File

@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Frappe CRM VERSION\n" "Project-Id-Version: Frappe CRM VERSION\n"
"Report-Msgid-Bugs-To: shariq@frappe.io\n" "Report-Msgid-Bugs-To: shariq@frappe.io\n"
"POT-Creation-Date: 2025-03-23 09:35+0000\n" "POT-Creation-Date: 2025-04-06 09:35+0000\n"
"PO-Revision-Date: 2025-03-23 09:35+0000\n" "PO-Revision-Date: 2025-04-06 09:35+0000\n"
"Last-Translator: shariq@frappe.io\n" "Last-Translator: shariq@frappe.io\n"
"Language-Team: shariq@frappe.io\n" "Language-Team: shariq@frappe.io\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -20,7 +20,7 @@ msgstr ""
msgid " (New)" msgid " (New)"
msgstr "" msgstr ""
#: frontend/src/components/Modals/TaskModal.vue:95 #: frontend/src/components/Modals/TaskModal.vue:66
#: frontend/src/components/Telephony/TaskPanel.vue:67 #: frontend/src/components/Telephony/TaskPanel.vue:67
msgid "01/04/2024 11:30 PM" msgid "01/04/2024 11:30 PM"
msgstr "" msgstr ""
@ -242,7 +242,7 @@ msgstr ""
msgid "Add to Holidays" msgid "Add to Holidays"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:381 #: frontend/src/components/Layouts/AppSidebar.vue:410
msgid "Add your first comment" msgid "Add your first comment"
msgstr "" msgstr ""
@ -380,7 +380,7 @@ msgstr ""
msgid "Assignment cleared successfully" msgid "Assignment cleared successfully"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:494 #: frontend/src/components/Layouts/AppSidebar.vue:541
msgid "Assignment rule" msgid "Assignment rule"
msgstr "" msgstr ""
@ -623,7 +623,7 @@ msgstr ""
msgid "Call duration in seconds" msgid "Call duration in seconds"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:473 #: frontend/src/components/Layouts/AppSidebar.vue:513
msgid "Call log" msgid "Call log"
msgstr "" msgstr ""
@ -631,8 +631,8 @@ msgstr ""
msgid "Call using {0}" msgid "Call using {0}"
msgstr "" msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:43 #: frontend/src/components/Modals/NoteModal.vue:30
#: frontend/src/components/Modals/TaskModal.vue:43 #: frontend/src/components/Modals/TaskModal.vue:30
msgid "Call with John Doe" msgid "Call with John Doe"
msgstr "" msgstr ""
@ -682,11 +682,20 @@ msgstr ""
msgid "Capture" msgid "Capture"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:518
msgid "Capturing leads"
msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:458
msgid "Change"
msgstr ""
#: frontend/src/components/Activities/TaskArea.vue:44 #: frontend/src/components/Activities/TaskArea.vue:44
msgid "Change Status" msgid "Change Status"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:421 #: frontend/src/components/Layouts/AppSidebar.vue:450
#: frontend/src/components/Layouts/AppSidebar.vue:457
msgid "Change deal status" msgid "Change deal status"
msgstr "" msgstr ""
@ -742,7 +751,7 @@ msgstr ""
msgid "Close Date" msgid "Close Date"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:97 #: frontend/src/components/Layouts/AppSidebar.vue:107
msgid "Collapse" msgid "Collapse"
msgstr "" msgstr ""
@ -777,7 +786,7 @@ msgstr ""
#: crm/fcrm/doctype/crm_notification/crm_notification.json #: crm/fcrm/doctype/crm_notification/crm_notification.json
#: frontend/src/components/CommentBox.vue:80 #: frontend/src/components/CommentBox.vue:80
#: frontend/src/components/CommunicationArea.vue:19 #: frontend/src/components/CommunicationArea.vue:19
#: frontend/src/components/Layouts/AppSidebar.vue:491 #: frontend/src/components/Layouts/AppSidebar.vue:538
msgid "Comment" msgid "Comment"
msgstr "" msgstr ""
@ -830,7 +839,7 @@ msgstr ""
#. Label of the contact (Link) field in DocType 'CRM Deal' #. Label of the contact (Link) field in DocType 'CRM Deal'
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json #: crm/fcrm/doctype/crm_contacts/crm_contacts.json
#: crm/fcrm/doctype/crm_deal/crm_deal.json #: crm/fcrm/doctype/crm_deal/crm_deal.json
#: frontend/src/components/Layouts/AppSidebar.vue:469 #: frontend/src/components/Layouts/AppSidebar.vue:509
#: frontend/src/pages/Lead.vue:262 frontend/src/pages/MobileLead.vue:133 #: frontend/src/pages/Lead.vue:262 frontend/src/pages/MobileLead.vue:133
msgid "Contact" msgid "Contact"
msgstr "" msgstr ""
@ -877,7 +886,7 @@ msgstr ""
#: crm/fcrm/doctype/fcrm_note/fcrm_note.json #: crm/fcrm/doctype/fcrm_note/fcrm_note.json
#: frontend/src/components/Modals/EmailTemplateModal.vue:61 #: frontend/src/components/Modals/EmailTemplateModal.vue:61
#: frontend/src/components/Modals/EmailTemplateModal.vue:74 #: frontend/src/components/Modals/EmailTemplateModal.vue:74
#: frontend/src/components/Modals/NoteModal.vue:47 #: frontend/src/components/Modals/NoteModal.vue:34
msgid "Content" msgid "Content"
msgstr "" msgstr ""
@ -885,13 +894,15 @@ msgstr ""
msgid "Content Type" msgid "Content Type"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:351
#: frontend/src/components/ListBulkActions.vue:70 #: frontend/src/components/ListBulkActions.vue:70
#: frontend/src/pages/Lead.vue:200 frontend/src/pages/MobileLead.vue:49 #: frontend/src/pages/Lead.vue:200 frontend/src/pages/MobileLead.vue:49
#: frontend/src/pages/MobileLead.vue:96 #: frontend/src/pages/MobileLead.vue:96
msgid "Convert" msgid "Convert"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:324 #: frontend/src/components/Layouts/AppSidebar.vue:343
#: frontend/src/components/Layouts/AppSidebar.vue:350
msgid "Convert lead to deal" msgid "Convert lead to deal"
msgstr "" msgstr ""
@ -912,14 +923,14 @@ msgid "Converted successfully"
msgstr "" msgstr ""
#: frontend/src/components/Modals/AddressModal.vue:100 #: frontend/src/components/Modals/AddressModal.vue:100
#: frontend/src/components/Modals/CallLogModal.vue:104 #: frontend/src/components/Modals/CallLogModal.vue:85
#: frontend/src/components/Modals/ContactModal.vue:37 #: frontend/src/components/Modals/ContactModal.vue:37
#: frontend/src/components/Modals/DealModal.vue:63 #: frontend/src/components/Modals/DealModal.vue:63
#: frontend/src/components/Modals/EmailTemplateModal.vue:9 #: frontend/src/components/Modals/EmailTemplateModal.vue:9
#: frontend/src/components/Modals/LeadModal.vue:34 #: frontend/src/components/Modals/LeadModal.vue:34
#: frontend/src/components/Modals/NoteModal.vue:8 #: frontend/src/components/Modals/NoteModal.vue:6
#: frontend/src/components/Modals/OrganizationModal.vue:37 #: frontend/src/components/Modals/OrganizationModal.vue:25
#: frontend/src/components/Modals/TaskModal.vue:8 #: frontend/src/components/Modals/TaskModal.vue:6
#: frontend/src/components/Modals/ViewModal.vue:16 #: frontend/src/components/Modals/ViewModal.vue:16
#: frontend/src/pages/CallLogs.vue:11 frontend/src/pages/Contacts.vue:13 #: frontend/src/pages/CallLogs.vue:11 frontend/src/pages/Contacts.vue:13
#: frontend/src/pages/Contacts.vue:57 frontend/src/pages/Deals.vue:13 #: frontend/src/pages/Contacts.vue:57 frontend/src/pages/Deals.vue:13
@ -952,12 +963,12 @@ msgid "Create New"
msgstr "" msgstr ""
#: frontend/src/components/Activities/Activities.vue:383 #: frontend/src/components/Activities/Activities.vue:383
#: frontend/src/components/Modals/NoteModal.vue:18 #: frontend/src/components/Modals/NoteModal.vue:15
msgid "Create Note" msgid "Create Note"
msgstr "" msgstr ""
#: frontend/src/components/Activities/Activities.vue:398 #: frontend/src/components/Activities/Activities.vue:398
#: frontend/src/components/Modals/TaskModal.vue:18 #: frontend/src/components/Modals/TaskModal.vue:15
msgid "Create Task" msgid "Create Task"
msgstr "" msgstr ""
@ -976,15 +987,15 @@ msgstr ""
msgid "Create lead" msgid "Create lead"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:303 #: frontend/src/components/Layouts/AppSidebar.vue:322
msgid "Create your first lead" msgid "Create your first lead"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:361 #: frontend/src/components/Layouts/AppSidebar.vue:390
msgid "Create your first note" msgid "Create your first note"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:341 #: frontend/src/components/Layouts/AppSidebar.vue:370
msgid "Create your first task" msgid "Create your first task"
msgstr "" msgstr ""
@ -995,23 +1006,23 @@ msgstr ""
msgid "Currency" msgid "Currency"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:503 #: frontend/src/components/Layouts/AppSidebar.vue:550
msgid "Custom actions" msgid "Custom actions"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:458 #: frontend/src/components/Layouts/AppSidebar.vue:498
msgid "Custom branding" msgid "Custom branding"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:502 #: frontend/src/components/Layouts/AppSidebar.vue:549
msgid "Custom fields" msgid "Custom fields"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:505 #: frontend/src/components/Layouts/AppSidebar.vue:552
msgid "Custom list actions" msgid "Custom list actions"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:504 #: frontend/src/components/Layouts/AppSidebar.vue:551
msgid "Custom statuses" msgid "Custom statuses"
msgstr "" msgstr ""
@ -1019,7 +1030,7 @@ msgstr ""
msgid "Customer created successfully" msgid "Customer created successfully"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:499 #: frontend/src/components/Layouts/AppSidebar.vue:546
msgid "Customization" msgid "Customization"
msgstr "" msgstr ""
@ -1028,7 +1039,7 @@ msgid "Customize quick filters"
msgstr "" msgstr ""
#: frontend/src/components/Activities/DataFields.vue:6 #: frontend/src/components/Activities/DataFields.vue:6
#: frontend/src/components/Layouts/AppSidebar.vue:492 #: frontend/src/components/Layouts/AppSidebar.vue:539
#: frontend/src/pages/Deal.vue:541 frontend/src/pages/Lead.vue:528 #: frontend/src/pages/Deal.vue:541 frontend/src/pages/Lead.vue:528
#: frontend/src/pages/MobileDeal.vue:456 frontend/src/pages/MobileLead.vue:359 #: frontend/src/pages/MobileDeal.vue:456 frontend/src/pages/MobileLead.vue:359
msgid "Data" msgid "Data"
@ -1044,7 +1055,7 @@ msgstr ""
msgid "Date" msgid "Date"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:468 #: frontend/src/components/Layouts/AppSidebar.vue:508
#: frontend/src/components/Telephony/ExotelCallUI.vue:205 #: frontend/src/components/Telephony/ExotelCallUI.vue:205
#: frontend/src/pages/Tasks.vue:129 #: frontend/src/pages/Tasks.vue:129
msgid "Deal" msgid "Deal"
@ -1186,7 +1197,7 @@ msgstr ""
#. Label of the description (Text Editor) field in DocType 'CRM Task' #. Label of the description (Text Editor) field in DocType 'CRM Task'
#: crm/fcrm/doctype/crm_holiday/crm_holiday.json #: crm/fcrm/doctype/crm_holiday/crm_holiday.json
#: crm/fcrm/doctype/crm_task/crm_task.json #: crm/fcrm/doctype/crm_task/crm_task.json
#: frontend/src/components/Modals/TaskModal.vue:48 #: frontend/src/components/Modals/TaskModal.vue:35
msgid "Description" msgid "Description"
msgstr "" msgstr ""
@ -1286,8 +1297,8 @@ msgstr ""
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:516 #: frontend/src/components/Layouts/AppSidebar.vue:563
#: frontend/src/components/Settings/Settings.vue:122 #: frontend/src/components/Settings/Settings.vue:130
msgid "ERPNext" msgid "ERPNext"
msgstr "" msgstr ""
@ -1332,7 +1343,7 @@ msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: frontend/src/components/Modals/CallLogModal.vue:100 #: frontend/src/components/Modals/CallLogModal.vue:81
msgid "Edit Call Log" msgid "Edit Call Log"
msgstr "" msgstr ""
@ -1352,7 +1363,7 @@ msgstr ""
msgid "Edit Grid Row Fields Layout" msgid "Edit Grid Row Fields Layout"
msgstr "" msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:18 #: frontend/src/components/Modals/NoteModal.vue:15
msgid "Edit Note" msgid "Edit Note"
msgstr "" msgstr ""
@ -1360,7 +1371,7 @@ msgstr ""
msgid "Edit Quick Entry Layout" msgid "Edit Quick Entry Layout"
msgstr "" msgstr ""
#: frontend/src/components/Modals/TaskModal.vue:18 #: frontend/src/components/Modals/TaskModal.vue:15
msgid "Edit Task" msgid "Edit Task"
msgstr "" msgstr ""
@ -1401,6 +1412,10 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: frontend/src/components/Settings/Settings.vue:107
msgid "Email Accounts"
msgstr ""
#. Label of the email_sent_at (Datetime) field in DocType 'CRM Invitation' #. Label of the email_sent_at (Datetime) field in DocType 'CRM Invitation'
#: crm/fcrm/doctype/crm_invitation/crm_invitation.json #: crm/fcrm/doctype/crm_invitation/crm_invitation.json
msgid "Email Sent At" msgid "Email Sent At"
@ -1410,7 +1425,7 @@ msgstr ""
msgid "Email Templates" msgid "Email Templates"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:490 #: frontend/src/components/Layouts/AppSidebar.vue:537
msgid "Email communication" msgid "Email communication"
msgstr "" msgstr ""
@ -1418,7 +1433,7 @@ msgstr ""
msgid "Email from Lead" msgid "Email from Lead"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:474 #: frontend/src/components/Layouts/AppSidebar.vue:514
msgid "Email template" msgid "Email template"
msgstr "" msgstr ""
@ -1535,7 +1550,7 @@ msgstr ""
#. Agent' #. Agent'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json #: crm/fcrm/doctype/crm_call_log/crm_call_log.json
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json #: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
#: frontend/src/components/Layouts/AppSidebar.vue:514 #: frontend/src/components/Layouts/AppSidebar.vue:561
#: frontend/src/components/Settings/TelephonySettings.vue:26 #: frontend/src/components/Settings/TelephonySettings.vue:26
#: frontend/src/components/Settings/TelephonySettings.vue:48 #: frontend/src/components/Settings/TelephonySettings.vue:48
msgid "Exotel" msgid "Exotel"
@ -1566,7 +1581,7 @@ msgstr ""
msgid "Exotel settings updated successfully" msgid "Exotel settings updated successfully"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:97 #: frontend/src/components/Layouts/AppSidebar.vue:107
msgid "Expand" msgid "Expand"
msgstr "" msgstr ""
@ -1708,7 +1723,7 @@ msgstr ""
msgid "Frappe CRM" msgid "Frappe CRM"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:520 #: frontend/src/components/Layouts/AppSidebar.vue:567
msgid "Frappe CRM mobile" msgid "Frappe CRM mobile"
msgstr "" msgstr ""
@ -1762,7 +1777,7 @@ msgid "Gender"
msgstr "" msgstr ""
#: frontend/src/components/Settings/GeneralSettings.vue:4 #: frontend/src/components/Settings/GeneralSettings.vue:4
#: frontend/src/components/Settings/Settings.vue:93 #: frontend/src/components/Settings/Settings.vue:95
msgid "General" msgid "General"
msgstr "" msgstr ""
@ -1791,7 +1806,7 @@ msgstr ""
msgid "Group By: " msgid "Group By: "
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:83 #: frontend/src/components/Layouts/AppSidebar.vue:93
msgid "Help" msgid "Help"
msgstr "" msgstr ""
@ -1850,7 +1865,7 @@ msgstr ""
msgid "Holidays" msgid "Holidays"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:459 #: frontend/src/components/Layouts/AppSidebar.vue:499
#: frontend/src/components/Settings/GeneralSettings.vue:97 #: frontend/src/components/Settings/GeneralSettings.vue:97
msgid "Home actions" msgid "Home actions"
msgstr "" msgstr ""
@ -1929,7 +1944,7 @@ msgstr ""
msgid "Initiating call..." msgid "Initiating call..."
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:510 #: frontend/src/components/Layouts/AppSidebar.vue:557
msgid "Integration" msgid "Integration"
msgstr "" msgstr ""
@ -1937,12 +1952,12 @@ msgstr ""
msgid "Integration Not Enabled" msgid "Integration Not Enabled"
msgstr "" msgstr ""
#: frontend/src/components/Settings/Settings.vue:107 #: frontend/src/components/Settings/Settings.vue:115
msgid "Integrations" msgid "Integrations"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:446 #: frontend/src/components/Layouts/AppSidebar.vue:486
#: frontend/src/components/Layouts/AppSidebar.vue:449 #: frontend/src/components/Layouts/AppSidebar.vue:489
msgid "Introduction" msgid "Introduction"
msgstr "" msgstr ""
@ -1963,7 +1978,7 @@ msgstr ""
msgid "Invalid credentials" msgid "Invalid credentials"
msgstr "" msgstr ""
#: frontend/src/components/Settings/Settings.vue:99 #: frontend/src/components/Settings/Settings.vue:101
msgid "Invite Members" msgid "Invite Members"
msgstr "" msgstr ""
@ -1975,11 +1990,11 @@ msgstr ""
msgid "Invite by email" msgid "Invite by email"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:460 #: frontend/src/components/Layouts/AppSidebar.vue:500
msgid "Invite members" msgid "Invite members"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:313 #: frontend/src/components/Layouts/AppSidebar.vue:332
msgid "Invite your team" msgid "Invite your team"
msgstr "" msgstr ""
@ -2044,7 +2059,7 @@ msgstr ""
#: frontend/src/components/Filter.vue:75 frontend/src/components/Filter.vue:108 #: frontend/src/components/Filter.vue:75 frontend/src/components/Filter.vue:108
#: frontend/src/components/Modals/AssignmentModal.vue:35 #: frontend/src/components/Modals/AssignmentModal.vue:35
#: frontend/src/components/Modals/TaskModal.vue:75 #: frontend/src/components/Modals/TaskModal.vue:51
#: frontend/src/components/Telephony/TaskPanel.vue:47 #: frontend/src/components/Telephony/TaskPanel.vue:47
msgid "John Doe" msgid "John Doe"
msgstr "" msgstr ""
@ -2132,7 +2147,7 @@ msgstr ""
#. Label of the lead (Link) field in DocType 'CRM Deal' #. Label of the lead (Link) field in DocType 'CRM Deal'
#: crm/fcrm/doctype/crm_deal/crm_deal.json #: crm/fcrm/doctype/crm_deal/crm_deal.json
#: frontend/src/components/Layouts/AppSidebar.vue:467 #: frontend/src/components/Layouts/AppSidebar.vue:507
#: frontend/src/components/Telephony/ExotelCallUI.vue:205 #: frontend/src/components/Telephony/ExotelCallUI.vue:205
#: frontend/src/pages/Tasks.vue:130 #: frontend/src/pages/Tasks.vue:130
msgid "Lead" msgid "Lead"
@ -2249,10 +2264,6 @@ msgstr ""
msgid "Logo" msgid "Logo"
msgstr "" msgstr ""
#: frontend/src/components/SignupBanner.vue:9
msgid "Loved the demo?"
msgstr ""
#. Option for the 'Priority' (Select) field in DocType 'CRM Task' #. Option for the 'Priority' (Select) field in DocType 'CRM Task'
#: crm/fcrm/doctype/crm_task/crm_task.json #: crm/fcrm/doctype/crm_task/crm_task.json
msgid "Low" msgid "Low"
@ -2321,7 +2332,7 @@ msgstr ""
msgid "Mark all as read" msgid "Mark all as read"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:464 #: frontend/src/components/Layouts/AppSidebar.vue:504
msgid "Masters" msgid "Masters"
msgstr "" msgstr ""
@ -2374,7 +2385,7 @@ msgstr ""
msgid "Mobile Number Missing" msgid "Mobile Number Missing"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:523 #: frontend/src/components/Layouts/AppSidebar.vue:570
msgid "Mobile app installation" msgid "Mobile app installation"
msgstr "" msgstr ""
@ -2435,7 +2446,7 @@ msgstr ""
msgid "New Address" msgid "New Address"
msgstr "" msgstr ""
#: frontend/src/components/Modals/CallLogModal.vue:100 #: frontend/src/components/Modals/CallLogModal.vue:81
msgid "New Call Log" msgid "New Call Log"
msgstr "" msgstr ""
@ -2660,7 +2671,7 @@ msgstr ""
#. Label of the note (Link) field in DocType 'CRM Call Log' #. Label of the note (Link) field in DocType 'CRM Call Log'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json #: crm/fcrm/doctype/crm_call_log/crm_call_log.json
#: frontend/src/components/Layouts/AppSidebar.vue:471 #: frontend/src/components/Layouts/AppSidebar.vue:511
msgid "Note" msgid "Note"
msgstr "" msgstr ""
@ -2674,7 +2685,7 @@ msgid "Notes View"
msgstr "" msgstr ""
#: frontend/src/components/Activities/EmailArea.vue:13 #: frontend/src/components/Activities/EmailArea.vue:13
#: frontend/src/components/Layouts/AppSidebar.vue:495 #: frontend/src/components/Layouts/AppSidebar.vue:542
msgid "Notification" msgid "Notification"
msgstr "" msgstr ""
@ -2724,13 +2735,13 @@ msgstr ""
msgid "Only one {0} can be set as primary." msgid "Only one {0} can be set as primary."
msgstr "" msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:25 #: frontend/src/components/Modals/NoteModal.vue:18
#: frontend/src/components/Modals/TaskModal.vue:25 #: frontend/src/components/Modals/TaskModal.vue:18
msgid "Open Deal" msgid "Open Deal"
msgstr "" msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:26 #: frontend/src/components/Modals/NoteModal.vue:19
#: frontend/src/components/Modals/TaskModal.vue:26 #: frontend/src/components/Modals/TaskModal.vue:19
msgid "Open Lead" msgid "Open Lead"
msgstr "" msgstr ""
@ -2769,7 +2780,7 @@ msgstr ""
#. Label of the organization (Data) field in DocType 'CRM Lead' #. Label of the organization (Data) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json #: crm/fcrm/doctype/crm_deal/crm_deal.json
#: crm/fcrm/doctype/crm_lead/crm_lead.json #: crm/fcrm/doctype/crm_lead/crm_lead.json
#: frontend/src/components/Layouts/AppSidebar.vue:470 #: frontend/src/components/Layouts/AppSidebar.vue:510
#: frontend/src/pages/Contact.vue:606 frontend/src/pages/Lead.vue:236 #: frontend/src/pages/Contact.vue:606 frontend/src/pages/Lead.vue:236
#: frontend/src/pages/MobileContact.vue:602 #: frontend/src/pages/MobileContact.vue:602
#: frontend/src/pages/MobileLead.vue:106 #: frontend/src/pages/MobileLead.vue:106
@ -2815,7 +2826,7 @@ msgstr ""
msgid "Organizations" msgid "Organizations"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:487 #: frontend/src/components/Layouts/AppSidebar.vue:534
msgid "Other features" msgid "Other features"
msgstr "" msgstr ""
@ -2903,7 +2914,7 @@ msgstr ""
msgid "Pinned Views" msgid "Pinned Views"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:483 #: frontend/src/components/Layouts/AppSidebar.vue:530
msgid "Pinned view" msgid "Pinned view"
msgstr "" msgstr ""
@ -2968,8 +2979,8 @@ msgstr ""
msgid "Probability" msgid "Probability"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:457 #: frontend/src/components/Layouts/AppSidebar.vue:497
#: frontend/src/components/Settings/Settings.vue:83 #: frontend/src/components/Settings/Settings.vue:85
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
@ -2986,7 +2997,7 @@ msgstr ""
msgid "Public Views" msgid "Public Views"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:482 #: frontend/src/components/Layouts/AppSidebar.vue:529
msgid "Public view" msgid "Public view"
msgstr "" msgstr ""
@ -3009,7 +3020,7 @@ msgstr ""
msgid "Quick Filters updated successfully" msgid "Quick Filters updated successfully"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:506 #: frontend/src/components/Layouts/AppSidebar.vue:553
msgid "Quick entry layout" msgid "Quick entry layout"
msgstr "" msgstr ""
@ -3320,7 +3331,7 @@ msgstr ""
#: frontend/src/components/Controls/GridRowFieldsModal.vue:26 #: frontend/src/components/Controls/GridRowFieldsModal.vue:26
#: frontend/src/components/DropdownItem.vue:21 #: frontend/src/components/DropdownItem.vue:21
#: frontend/src/components/Modals/AddressModal.vue:100 #: frontend/src/components/Modals/AddressModal.vue:100
#: frontend/src/components/Modals/CallLogModal.vue:104 #: frontend/src/components/Modals/CallLogModal.vue:85
#: frontend/src/components/Modals/DataFieldsModal.vue:26 #: frontend/src/components/Modals/DataFieldsModal.vue:26
#: frontend/src/components/Modals/QuickEntryModal.vue:26 #: frontend/src/components/Modals/QuickEntryModal.vue:26
#: frontend/src/components/Modals/SidePanelModal.vue:26 #: frontend/src/components/Modals/SidePanelModal.vue:26
@ -3340,7 +3351,7 @@ msgstr ""
msgid "Saved Views" msgid "Saved Views"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:481 #: frontend/src/components/Layouts/AppSidebar.vue:528
msgid "Saved view" msgid "Saved view"
msgstr "" msgstr ""
@ -3381,7 +3392,7 @@ msgstr ""
msgid "Send an email" msgid "Send an email"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:401 #: frontend/src/components/Layouts/AppSidebar.vue:430
msgid "Send email" msgid "Send email"
msgstr "" msgstr ""
@ -3395,7 +3406,7 @@ msgstr ""
msgid "Series" msgid "Series"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:493 #: frontend/src/components/Layouts/AppSidebar.vue:540
msgid "Service level agreement" msgid "Service level agreement"
msgstr "" msgstr ""
@ -3423,13 +3434,13 @@ msgstr ""
msgid "Set first name" msgid "Set first name"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:450 #: frontend/src/components/Layouts/AppSidebar.vue:490
msgid "Setting up" msgid "Setting up"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:454 #: frontend/src/components/Layouts/AppSidebar.vue:494
#: frontend/src/components/Settings/Settings.vue:11 #: frontend/src/components/Settings/Settings.vue:11
#: frontend/src/components/Settings/Settings.vue:79 #: frontend/src/components/Settings/Settings.vue:81
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -3462,10 +3473,6 @@ msgstr ""
msgid "Sidebar Items" msgid "Sidebar Items"
msgstr "" msgstr ""
#: frontend/src/components/SignupBanner.vue:15
msgid "Sign up now"
msgstr ""
#. Description of the 'Condition' (Code) field in DocType 'CRM Service Level #. Description of the 'Condition' (Code) field in DocType 'CRM Service Level
#. Agreement' #. Agreement'
#: crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.json #: crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.json
@ -3609,7 +3616,7 @@ msgstr ""
#. Option for the 'Type' (Select) field in DocType 'CRM Notification' #. Option for the 'Type' (Select) field in DocType 'CRM Notification'
#: crm/fcrm/doctype/crm_notification/crm_notification.json #: crm/fcrm/doctype/crm_notification/crm_notification.json
#: frontend/src/components/Layouts/AppSidebar.vue:472 #: frontend/src/components/Layouts/AppSidebar.vue:512
msgid "Task" msgid "Task"
msgstr "" msgstr ""
@ -3618,7 +3625,7 @@ msgstr ""
msgid "Tasks" msgid "Tasks"
msgstr "" msgstr ""
#: frontend/src/components/Settings/Settings.vue:110 #: frontend/src/components/Settings/Settings.vue:118
msgid "Telephony" msgid "Telephony"
msgstr "" msgstr ""
@ -3702,8 +3709,8 @@ msgstr ""
#. Label of the title (Data) field in DocType 'FCRM Note' #. Label of the title (Data) field in DocType 'FCRM Note'
#: crm/fcrm/doctype/crm_task/crm_task.json #: crm/fcrm/doctype/crm_task/crm_task.json
#: crm/fcrm/doctype/fcrm_note/fcrm_note.json #: crm/fcrm/doctype/fcrm_note/fcrm_note.json
#: frontend/src/components/Modals/NoteModal.vue:41 #: frontend/src/components/Modals/NoteModal.vue:30
#: frontend/src/components/Modals/TaskModal.vue:41 #: frontend/src/components/Modals/TaskModal.vue:30
msgid "Title" msgid "Title"
msgstr "" msgstr ""
@ -3750,8 +3757,8 @@ msgstr ""
msgid "Tomorrow" msgid "Tomorrow"
msgstr "" msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:56 #: frontend/src/components/Modals/NoteModal.vue:37
#: frontend/src/components/Modals/TaskModal.vue:58 #: frontend/src/components/Modals/TaskModal.vue:39
msgid "Took a call with John Doe and discussed the new project." msgid "Took a call with John Doe and discussed the new project."
msgstr "" msgstr ""
@ -3760,10 +3767,6 @@ msgstr ""
msgid "Total Holidays" msgid "Total Holidays"
msgstr "" msgstr ""
#: frontend/src/components/SignupBanner.vue:12
msgid "Try Frappe CRM for free with a 14-day trial."
msgstr ""
#. Option for the 'Weekly Off' (Select) field in DocType 'CRM Holiday List' #. Option for the 'Weekly Off' (Select) field in DocType 'CRM Holiday List'
#. Option for the 'Workday' (Select) field in DocType 'CRM Service Day' #. Option for the 'Workday' (Select) field in DocType 'CRM Service Day'
#: crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json #: crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json
@ -3782,7 +3785,7 @@ msgstr ""
#. Agent' #. Agent'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json #: crm/fcrm/doctype/crm_call_log/crm_call_log.json
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json #: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
#: frontend/src/components/Layouts/AppSidebar.vue:513 #: frontend/src/components/Layouts/AppSidebar.vue:560
#: frontend/src/components/Settings/TelephonySettings.vue:25 #: frontend/src/components/Settings/TelephonySettings.vue:25
#: frontend/src/components/Settings/TelephonySettings.vue:35 #: frontend/src/components/Settings/TelephonySettings.vue:35
msgid "Twilio" msgid "Twilio"
@ -3857,8 +3860,8 @@ msgstr ""
#: frontend/src/components/ColumnSettings.vue:134 #: frontend/src/components/ColumnSettings.vue:134
#: frontend/src/components/Modals/AssignmentModal.vue:17 #: frontend/src/components/Modals/AssignmentModal.vue:17
#: frontend/src/components/Modals/EmailTemplateModal.vue:9 #: frontend/src/components/Modals/EmailTemplateModal.vue:9
#: frontend/src/components/Modals/NoteModal.vue:8 #: frontend/src/components/Modals/NoteModal.vue:6
#: frontend/src/components/Modals/TaskModal.vue:8 #: frontend/src/components/Modals/TaskModal.vue:6
#: frontend/src/components/Settings/GeneralSettings.vue:112 #: frontend/src/components/Settings/GeneralSettings.vue:112
#: frontend/src/components/Settings/ProfileSettings.vue:71 #: frontend/src/components/Settings/ProfileSettings.vue:71
#: frontend/src/components/Settings/SettingsPage.vue:31 #: frontend/src/components/Settings/SettingsPage.vue:31
@ -3927,10 +3930,14 @@ msgstr ""
msgid "View Name" msgid "View Name"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:478 #: frontend/src/components/Layouts/AppSidebar.vue:525
msgid "Views" msgid "Views"
msgstr "" msgstr ""
#: frontend/src/components/Layouts/AppSidebar.vue:521
msgid "Web form"
msgstr ""
#. Label of the webhook_verify_token (Data) field in DocType 'CRM Exotel #. Label of the webhook_verify_token (Data) field in DocType 'CRM Exotel
#. Settings' #. Settings'
#: crm/fcrm/doctype/crm_exotel_settings/crm_exotel_settings.json #: crm/fcrm/doctype/crm_exotel_settings/crm_exotel_settings.json
@ -3975,8 +3982,8 @@ msgstr ""
#. Option for the 'Type' (Select) field in DocType 'CRM Notification' #. Option for the 'Type' (Select) field in DocType 'CRM Notification'
#: crm/fcrm/doctype/crm_notification/crm_notification.json #: crm/fcrm/doctype/crm_notification/crm_notification.json
#: frontend/src/components/Layouts/AppSidebar.vue:515 #: frontend/src/components/Layouts/AppSidebar.vue:562
#: frontend/src/components/Settings/Settings.vue:116 #: frontend/src/components/Settings/Settings.vue:124
#: frontend/src/pages/Deal.vue:567 frontend/src/pages/Lead.vue:554 #: frontend/src/pages/Deal.vue:567 frontend/src/pages/Lead.vue:554
#: frontend/src/pages/MobileDeal.vue:482 frontend/src/pages/MobileLead.vue:385 #: frontend/src/pages/MobileDeal.vue:482 frontend/src/pages/MobileLead.vue:385
msgid "WhatsApp" msgid "WhatsApp"
@ -4137,7 +4144,7 @@ msgstr ""
msgid "kanban" msgid "kanban"
msgstr "" msgstr ""
#: crm/api/doc.py:38 crm/api/doc.py:156 crm/api/doc.py:500 #: crm/api/doc.py:38 crm/api/doc.py:156 crm/api/doc.py:501
msgid "label" msgid "label"
msgstr "" msgstr ""

@ -1 +1 @@
Subproject commit 3423aa5b5c38d3a1b143ae8ab08cbde7360f9a7c Subproject commit 29307e4fffaacdbb3d9c5d95c5270b2f245a5607

View File

@ -78,11 +78,17 @@ declare module 'vue' {
EditIcon: typeof import('./src/components/Icons/EditIcon.vue')['default'] EditIcon: typeof import('./src/components/Icons/EditIcon.vue')['default']
EditValueModal: typeof import('./src/components/Modals/EditValueModal.vue')['default'] EditValueModal: typeof import('./src/components/Modals/EditValueModal.vue')['default']
Email2Icon: typeof import('./src/components/Icons/Email2Icon.vue')['default'] Email2Icon: typeof import('./src/components/Icons/Email2Icon.vue')['default']
EmailAccountCard: typeof import('./src/components/Settings/EmailAccountCard.vue')['default']
EmailAccountList: typeof import('./src/components/Settings/EmailAccountList.vue')['default']
EmailAdd: typeof import('./src/components/Settings/EmailAdd.vue')['default']
EmailArea: typeof import('./src/components/Activities/EmailArea.vue')['default'] EmailArea: typeof import('./src/components/Activities/EmailArea.vue')['default']
EmailAtIcon: typeof import('./src/components/Icons/EmailAtIcon.vue')['default'] EmailAtIcon: typeof import('./src/components/Icons/EmailAtIcon.vue')['default']
EmailConfig: typeof import('./src/components/Settings/EmailConfig.vue')['default']
EmailContent: typeof import('./src/components/Activities/EmailContent.vue')['default'] EmailContent: typeof import('./src/components/Activities/EmailContent.vue')['default']
EmailEdit: typeof import('./src/components/Settings/EmailEdit.vue')['default']
EmailEditor: typeof import('./src/components/EmailEditor.vue')['default'] EmailEditor: typeof import('./src/components/EmailEditor.vue')['default']
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default'] EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default'] EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default'] EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default'] EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
@ -140,6 +146,7 @@ declare module 'vue' {
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default'] ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default'] ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default'] LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LucidePlus: typeof import('~icons/lucide/plus')['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']
@ -149,6 +156,7 @@ declare module 'vue' {
MobileLayout: typeof import('./src/components/Layouts/MobileLayout.vue')['default'] MobileLayout: typeof import('./src/components/Layouts/MobileLayout.vue')['default']
MobileSidebar: typeof import('./src/components/Mobile/MobileSidebar.vue')['default'] MobileSidebar: typeof import('./src/components/Mobile/MobileSidebar.vue')['default']
MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default'] MoneyIcon: typeof import('./src/components/Icons/MoneyIcon.vue')['default']
MultiActionButton: typeof import('./src/components/MultiActionButton.vue')['default']
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default'] MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default'] MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default'] MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']

View File

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@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.121", "frappe-ui": "^0.1.123",
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.1", "mime": "^4.0.1",

View File

@ -373,11 +373,7 @@
> >
<component :is="emptyTextIcon" class="h-10 w-10" /> <component :is="emptyTextIcon" class="h-10 w-10" />
<span>{{ __(emptyText) }}</span> <span>{{ __(emptyText) }}</span>
<Button <MultiActionButton v-if="title == 'Calls'" :options="callActions" />
v-if="title == 'Calls'"
:label="__('Make a Call')"
@click="makeCall(doc.data.mobile_no)"
/>
<Button <Button
v-else-if="title == 'Notes'" v-else-if="title == 'Notes'"
:label="__('Create Note')" :label="__('Create Note')"
@ -470,6 +466,7 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue' import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue'
import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue' import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue'
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue' import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
import MultiActionButton from '@/components/MultiActionButton.vue'
import LeadsIcon from '@/components/Icons/LeadsIcon.vue' import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
import DealsIcon from '@/components/Icons/DealsIcon.vue' import DealsIcon from '@/components/Icons/DealsIcon.vue'
import DotIcon from '@/components/Icons/DotIcon.vue' import DotIcon from '@/components/Icons/DotIcon.vue'
@ -487,7 +484,7 @@ import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
import { timeAgo, formatDate, startCase } from '@/utils' 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 } from '@/composables/settings' import { whatsappEnabled, callEnabled } from '@/composables/settings'
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'
@ -785,5 +782,23 @@ function scroll(hash) {
}, 500) }, 500)
} }
const callActions = computed(() => {
let actions = [
{
label: __('Create Call Log'),
onClick: () => modalRef.value.createCallLog(),
},
{
label: __('Make a Call'),
onClick: () => makeCall(doc.data.mobile_no),
condition: () => callEnabled.value,
},
]
return actions.filter((action) =>
action.condition ? action.condition() : true,
)
})
defineExpose({ emailBox, all_activities }) defineExpose({ emailBox, all_activities })
</script> </script>

View File

@ -26,16 +26,11 @@
</template> </template>
<span>{{ __('New Comment') }}</span> <span>{{ __('New Comment') }}</span>
</Button> </Button>
<Button <MultiActionButton
v-else-if="title == 'Calls'" v-else-if="title == 'Calls'"
variant="solid" variant="solid"
@click="makeCall(doc.data.mobile_no)" :options="callActions"
> />
<template #prefix>
<PhoneIcon class="h-4 w-4" />
</template>
<span>{{ __('Make a Call') }}</span>
</Button>
<Button <Button
v-else-if="title == 'Notes'" v-else-if="title == 'Notes'"
variant="solid" variant="solid"
@ -97,6 +92,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import MultiActionButton from '@/components/MultiActionButton.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue' import Email2Icon from '@/components/Icons/Email2Icon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue' import CommentIcon from '@/components/Icons/CommentIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
@ -136,6 +132,11 @@ const defaultActions = computed(() => {
label: __('New Comment'), label: __('New Comment'),
onClick: () => (props.emailBox.showComment = true), onClick: () => (props.emailBox.showComment = true),
}, },
{
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Create Call Log'),
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'),
@ -172,4 +173,24 @@ const defaultActions = computed(() => {
function getTabIndex(name) { function getTabIndex(name) {
return props.tabs.findIndex((tab) => tab.name === name) return props.tabs.findIndex((tab) => tab.name === name)
} }
const callActions = computed(() => {
let actions = [
{
label: __('Create Call Log'),
icon: 'plus',
onClick: () => props.modalRef.createCallLog(),
},
{
label: __('Make a Call'),
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
onClick: () => makeCall(doc.data.mobile_no),
condition: () => callEnabled.value,
},
]
return actions.filter((action) =>
action.condition ? action.condition() : true,
)
})
</script> </script>

View File

@ -15,10 +15,16 @@
:doc="doc.data?.name" :doc="doc.data?.name"
@after="redirect('notes')" @after="redirect('notes')"
/> />
<CallLogModal
v-model="showCallLogModal"
v-model:callLog="callLog"
:options="{ afterInsert: () => activities.reload() }"
/>
</template> </template>
<script setup> <script setup>
import TaskModal from '@/components/Modals/TaskModal.vue' import TaskModal from '@/components/Modals/TaskModal.vue'
import NoteModal from '@/components/Modals/NoteModal.vue' import NoteModal from '@/components/Modals/NoteModal.vue'
import CallLogModal from '@/components/Modals/CallLogModal.vue'
import { call } from 'frappe-ui' import { call } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@ -77,6 +83,22 @@ function showNote(n) {
showNoteModal.value = true showNoteModal.value = true
} }
// Call Logs
const showCallLogModal = ref(false)
const callLog = ref({})
function createCallLog() {
let doctype = props.doctype
let docname = props.doc.data?.name
callLog.value = {
data: {
reference_doctype: doctype,
reference_docname: docname,
},
}
showCallLogModal.value = true
}
// common // common
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -95,5 +117,6 @@ defineExpose({
deleteTask, deleteTask,
updateTaskStatus, updateTaskStatus,
showNote, showNote,
createCallLog,
}) })
</script> </script>

View File

@ -100,10 +100,16 @@
:disabled="true" :disabled="true"
/> />
<Link <Link
v-else-if="field.fieldtype === 'Link'" v-else-if="
['Link', 'Dynamic Link'].includes(field.fieldtype)
"
class="text-sm text-ink-gray-8" class="text-sm text-ink-gray-8"
v-model="row[field.fieldname]" v-model="row[field.fieldname]"
:doctype="field.options" :doctype="
field.fieldtype == 'Link'
? field.options
: row[field.options]
"
:filters="field.filters" :filters="field.filters"
/> />
<Link <Link

View File

@ -159,6 +159,7 @@ const options = createResource({
}) })
function reload(val) { function reload(val) {
if (!props.doctype) return
if ( if (
options.data?.length && options.data?.length &&
val === options.params?.txt && val === options.params?.txt &&

View File

@ -59,11 +59,16 @@
<span class="text-ink-red-3" v-if="field.mandatory">*</span> <span class="text-ink-red-3" v-if="field.mandatory">*</span>
</label> </label>
</div> </div>
<div class="flex gap-1" v-else-if="field.fieldtype === 'Link'"> <div
class="flex gap-1"
v-else-if="['Link', 'Dynamic Link'].includes(field.fieldtype)"
>
<Link <Link
class="form-control flex-1 truncate" class="form-control flex-1 truncate"
:value="data[field.fieldname]" :value="data[field.fieldname]"
:doctype="field.options" :doctype="
field.fieldtype == 'Link' ? field.options : data[field.options]
"
:filters="field.filters" :filters="field.filters"
@change="(v) => (data[field.fieldname] = v)" @change="(v) => (data[field.fieldname] = v)"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"

View File

@ -1,55 +1,36 @@
<template> <template>
<Dialog v-model="show" :options="dialogOptions"> <Dialog v-model="show" :options="dialogOptions">
<template #body> <template #body>
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6"> <div class="px-4 pt-5 pb-6 bg-surface-modal sm:px-6">
<div class="mb-5 flex items-center justify-between"> <div class="flex items-center justify-between mb-5">
<div> <div>
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __(dialogOptions.title) || __('Untitled') }} {{ __(dialogOptions.title) || __('Untitled') }}
</h3> </h3>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button <Button v-if="isManager() && !isMobileView" variant="ghost" class="w-7" @click="openQuickEntryModal">
v-if="isManager() && !isMobileView" <EditIcon class="w-4 h-4" />
variant="ghost"
class="w-7"
@click="openQuickEntryModal"
>
<EditIcon class="h-4 w-4" />
</Button> </Button>
<Button variant="ghost" class="w-7" @click="show = false"> <Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" /> <FeatherIcon name="x" class="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
<div v-if="tabs.data"> <div v-if="tabs.data">
<FieldLayout <FieldLayout :tabs="tabs.data" :data="_callLog" doctype="CRM Call Log" />
:tabs="tabs.data" <ErrorMessage class="mt-8" :message="error" />
:data="_callLog"
doctype="CRM Call Log"
/>
<ErrorMessage class="mt-2" :message="error" />
</div> </div>
</div> </div>
<div class="px-4 pb-7 pt-4 sm:px-6"> <div class="px-4 pt-4 pb-7 sm:px-6">
<div class="space-y-2"> <div class="space-y-2">
<Button <Button class="w-full" v-for="action in dialogOptions.actions" :key="action.label" v-bind="action"
class="w-full" :label="__(action.label)" :loading="loading" />
v-for="action in dialogOptions.actions"
:key="action.label"
v-bind="action"
:label="__(action.label)"
:loading="loading"
/>
</div> </div>
</div> </div>
</template> </template>
</Dialog> </Dialog>
<QuickEntryModal <QuickEntryModal v-if="showQuickEntryModal" v-model="showQuickEntryModal" doctype="CRM Call Log" />
v-if="showQuickEntryModal"
v-model="showQuickEntryModal"
doctype="CRM Call Log"
/>
</template> </template>
<script setup> <script setup>
@ -67,7 +48,7 @@ const props = defineProps({
options: { options: {
type: Object, type: Object,
default: { default: {
afterInsert: () => {}, afterInsert: () => { },
}, },
}, },
}) })
@ -175,6 +156,13 @@ const createCallLog = createResource({
}, },
onError(err) { onError(err) {
loading.value = false loading.value = false
if (err.exc_type == 'MandatoryError') {
const errorMessage = err.messages
.map(msg => msg.split('Log:')[1].trim())
.join(', ')
error.value = `These fields are required: ${errorMessage}`
return
}
error.value = err error.value = err
}, },
}) })

View File

@ -1,34 +1,25 @@
<template> <template>
<Dialog <Dialog v-model="show" :options="{
v-model="show" size: 'xl',
:options="{ actions: [
size: 'xl', {
actions: [ label: editMode ? __('Update') : __('Create'),
{ variant: 'solid',
label: editMode ? __('Update') : __('Create'), onClick: () => updateNote(),
variant: 'solid', },
onClick: () => updateNote(), ],
}, }">
],
}"
>
<template #body-title> <template #body-title>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ editMode ? __('Edit Note') : __('Create Note') }} {{ editMode ? __('Edit Note') : __('Create Note') }}
</h3> </h3>
<Button <Button v-if="_note?.reference_docname" size="sm" :label="_note.reference_doctype == 'CRM Deal'
v-if="_note?.reference_docname" ? __('Open Deal')
size="sm" : __('Open Lead')
:label=" " @click="redirect()">
_note.reference_doctype == 'CRM Deal'
? __('Open Deal')
: __('Open Lead')
"
@click="redirect()"
>
<template #suffix> <template #suffix>
<ArrowUpRightIcon class="h-4 w-4" /> <ArrowUpRightIcon class="w-4 h-4" />
</template> </template>
</Button> </Button>
</div> </div>
@ -36,27 +27,17 @@
<template #body-content> <template #body-content>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<FormControl <FormControl ref="title" :label="__('Title')" v-model="_note.title" :placeholder="__('Call with John Doe')"
ref="title" required />
:label="__('Title')"
v-model="_note.title"
:placeholder="__('Call with John Doe')"
/>
</div> </div>
<div> <div>
<div class="mb-1.5 text-xs text-ink-gray-5">{{ __('Content') }}</div> <div class="mb-1.5 text-xs text-ink-gray-5">{{ __('Content') }}</div>
<TextEditor <TextEditor variant="outline" ref="content"
variant="outline"
ref="content"
editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors" editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors"
:bubbleMenu="true" :bubbleMenu="true" :content="_note.content" @change="(val) => (_note.content = val)" :placeholder="__('Took a call with John Doe and discussed the new project.')
:content="_note.content" " />
@change="(val) => (_note.content = val)"
:placeholder="
__('Took a call with John Doe and discussed the new project.')
"
/>
</div> </div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>
</template> </template>
</Dialog> </Dialog>
@ -94,17 +75,12 @@ const router = useRouter()
const { updateOnboardingStep } = useOnboarding('frappecrm') const { updateOnboardingStep } = useOnboarding('frappecrm')
const error = ref(null)
const title = ref(null) const title = ref(null)
const editMode = ref(false) const editMode = ref(false)
let _note = ref({}) let _note = ref({})
async function updateNote() { async function updateNote() {
if (
props.note.title === _note.value.title &&
props.note.content === _note.value.content
)
return
if (_note.value.name) { if (_note.value.name) {
let d = await call('frappe.client.set_value', { let d = await call('frappe.client.set_value', {
doctype: 'FCRM Note', doctype: 'FCRM Note',
@ -124,6 +100,12 @@ async function updateNote() {
reference_doctype: props.doctype, reference_doctype: props.doctype,
reference_docname: props.doc || '', reference_docname: props.doc || '',
}, },
}, {
onError: (err) => {
if (err.error.exc_type == 'MandatoryError') {
error.value = "Title is mandatory"
}
}
}) })
if (d.name) { if (d.name) {
updateOnboardingStep('create_first_note') updateOnboardingStep('create_first_note')

View File

@ -1,43 +1,28 @@
<template> <template>
<Dialog v-model="show" :options="{ size: 'xl' }"> <Dialog v-model="show" :options="{ size: 'xl' }">
<template #body> <template #body>
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6"> <div class="px-4 pt-5 pb-6 bg-surface-modal sm:px-6">
<div class="mb-5 flex items-center justify-between"> <div class="flex items-center justify-between mb-5">
<div> <div>
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('New Organization') }} {{ __('New Organization') }}
</h3> </h3>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button <Button v-if="isManager() && !isMobileView" variant="ghost" class="w-7" @click="openQuickEntryModal">
v-if="isManager() && !isMobileView" <EditIcon class="w-4 h-4" />
variant="ghost"
class="w-7"
@click="openQuickEntryModal"
>
<EditIcon class="h-4 w-4" />
</Button> </Button>
<Button variant="ghost" class="w-7" @click="show = false"> <Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" /> <FeatherIcon name="x" class="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
<FieldLayout <FieldLayout v-if="tabs.data?.length" :tabs="tabs.data" :data="_organization" doctype="CRM Organization" />
v-if="tabs.data?.length" <ErrorMessage class="mt-8" v-if="error" :message="__(error)" />
:tabs="tabs.data"
:data="_organization"
doctype="CRM Organization"
/>
</div> </div>
<div class="px-4 pb-7 pt-4 sm:px-6"> <div class="px-4 pt-4 pb-7 sm:px-6">
<div class="space-y-2"> <div class="space-y-2">
<Button <Button class="w-full" variant="solid" :label="__('Create')" :loading="loading" @click="createOrganization" />
class="w-full"
variant="solid"
:label="__('Create')"
:loading="loading"
@click="createOrganization"
/>
</div> </div>
</div> </div>
</template> </template>
@ -59,7 +44,7 @@ const props = defineProps({
type: Object, type: Object,
default: { default: {
redirect: true, redirect: true,
afterInsert: () => {}, afterInsert: () => { },
}, },
}, },
}) })
@ -84,6 +69,7 @@ let _organization = ref({
}) })
let doc = ref({}) let doc = ref({})
const error = ref(null)
async function createOrganization() { async function createOrganization() {
const doc = await call('frappe.client.insert', { const doc = await call('frappe.client.insert', {
@ -91,6 +77,12 @@ async function createOrganization() {
doctype: 'CRM Organization', doctype: 'CRM Organization',
..._organization.value, ..._organization.value,
}, },
}, {
onError: (err) => {
if (err.error.exc_type == 'ValidationError') {
error.value = err.error?.messages?.[0]
}
}
}) })
loading.value = false loading.value = false
if (doc.name) { if (doc.name) {

View File

@ -1,34 +1,25 @@
<template> <template>
<Dialog <Dialog v-model="show" :options="{
v-model="show" size: 'xl',
:options="{ actions: [
size: 'xl', {
actions: [ label: editMode ? __('Update') : __('Create'),
{ variant: 'solid',
label: editMode ? __('Update') : __('Create'), onClick: () => updateTask(),
variant: 'solid', },
onClick: () => updateTask(), ],
}, }">
],
}"
>
<template #body-title> <template #body-title>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9"> <h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ editMode ? __('Edit Task') : __('Create Task') }} {{ editMode ? __('Edit Task') : __('Create Task') }}
</h3> </h3>
<Button <Button v-if="task?.reference_docname" size="sm" :label="task.reference_doctype == 'CRM Deal'
v-if="task?.reference_docname" ? __('Open Deal')
size="sm" : __('Open Lead')
:label=" " @click="redirect()">
task.reference_doctype == 'CRM Deal'
? __('Open Deal')
: __('Open Lead')
"
@click="redirect()"
>
<template #suffix> <template #suffix>
<ArrowUpRightIcon class="h-4 w-4" /> <ArrowUpRightIcon class="w-4 h-4" />
</template> </template>
</Button> </Button>
</div> </div>
@ -36,74 +27,53 @@
<template #body-content> <template #body-content>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<FormControl <FormControl ref="title" :label="__('Title')" v-model="_task.title" :placeholder="__('Call with John Doe')"
ref="title" required />
:label="__('Title')"
v-model="_task.title"
:placeholder="__('Call with John Doe')"
/>
</div> </div>
<div> <div>
<div class="mb-1.5 text-xs text-ink-gray-5"> <div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Description') }} {{ __('Description') }}
</div> </div>
<TextEditor <TextEditor variant="outline" ref="description"
variant="outline"
ref="description"
editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors" editor-class="!prose-sm overflow-auto min-h-[180px] max-h-80 py-1.5 px-2 rounded border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors"
:bubbleMenu="true" :bubbleMenu="true" :content="_task.description" @change="(val) => (_task.description = val)" :placeholder="__('Took a call with John Doe and discussed the new project.')
:content="_task.description" " />
@change="(val) => (_task.description = val)"
:placeholder="
__('Took a call with John Doe and discussed the new project.')
"
/>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<Dropdown :options="taskStatusOptions(updateTaskStatus)"> <Dropdown :options="taskStatusOptions(updateTaskStatus)">
<Button :label="_task.status" class="w-full justify-between"> <Button :label="_task.status" class="justify-between w-full">
<template #prefix> <template #prefix>
<TaskStatusIcon :status="_task.status" /> <TaskStatusIcon :status="_task.status" />
</template> </template>
</Button> </Button>
</Dropdown> </Dropdown>
<Link <Link class="form-control" :value="getUser(_task.assigned_to).full_name" doctype="User"
class="form-control" @change="(option) => (_task.assigned_to = option)" :placeholder="__('John Doe')" :hideMe="true">
:value="getUser(_task.assigned_to).full_name" <template #prefix>
doctype="User" <UserAvatar class="mr-2 !h-4 !w-4" :user="_task.assigned_to" />
@change="(option) => (_task.assigned_to = option)" </template>
:placeholder="__('John Doe')" <template #item-prefix="{ option }">
:hideMe="true" <UserAvatar class="mr-2" :user="option.value" size="sm" />
> </template>
<template #prefix> <template #item-label="{ option }">
<UserAvatar class="mr-2 !h-4 !w-4" :user="_task.assigned_to" /> <Tooltip :text="option.value">
</template> <div class="cursor-pointer text-ink-gray-9">
<template #item-prefix="{ option }"> {{ getUser(option.value).full_name }}
<UserAvatar class="mr-2" :user="option.value" size="sm" /> </div>
</template> </Tooltip>
<template #item-label="{ option }"> </template>
<Tooltip :text="option.value">
<div class="cursor-pointer text-ink-gray-9">
{{ getUser(option.value).full_name }}
</div>
</Tooltip>
</template>
</Link> </Link>
<DateTimePicker <DateTimePicker class="datepicker w-36" v-model="_task.due_date" :placeholder="__('01/04/2024 11:30 PM')"
class="datepicker w-36" :formatter="(date) => getFormat(date, '', true, true)" input-class="border-none" />
v-model="_task.due_date"
:placeholder="__('01/04/2024 11:30 PM')"
:formatter="(date) => getFormat(date, '', true, true)"
input-class="border-none"
/>
<Dropdown :options="taskPriorityOptions(updateTaskPriority)"> <Dropdown :options="taskPriorityOptions(updateTaskPriority)">
<Button :label="_task.priority" class="w-full justify-between"> <Button :label="_task.priority" class="justify-between w-full">
<template #prefix> <template #prefix>
<TaskPriorityIcon :priority="_task.priority" /> <TaskPriorityIcon :priority="_task.priority" />
</template> </template>
</Button> </Button>
</Dropdown> </Dropdown>
</div> </div>
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>
</template> </template>
</Dialog> </Dialog>
@ -147,6 +117,7 @@ const router = useRouter()
const { getUser } = usersStore() const { getUser } = usersStore()
const { updateOnboardingStep } = useOnboarding('frappecrm') const { updateOnboardingStep } = useOnboarding('frappecrm')
const error = ref(null)
const title = ref(null) const title = ref(null)
const editMode = ref(false) const editMode = ref(false)
const _task = ref({ const _task = ref({
@ -200,6 +171,12 @@ async function updateTask() {
reference_docname: props.doc || null, reference_docname: props.doc || null,
..._task.value, ..._task.value,
}, },
}, {
onError: (err) => {
if (err.error.exc_type == 'MandatoryError') {
error.value = "Title is mandatory"
}
}
}) })
if (d.name) { if (d.name) {
updateOnboardingStep('create_first_task') updateOnboardingStep('create_first_task')

View File

@ -0,0 +1,71 @@
<template>
<div class="flex items-center">
<Button
:variant="$attrs.variant"
class="border-0"
:label="activeButton.label"
:size="$attrs.size"
:class="[
$attrs.class,
showDropdown ? 'rounded-br-none rounded-tr-none' : '',
]"
@click="() => activeButton.onClick()"
>
<template #prefix>
<FeatherIcon
v-if="activeButton.icon && typeof activeButton.icon === 'string'"
:name="activeButton.icon"
class="h-4 w-4"
/>
<component
v-else-if="activeButton.icon"
:is="activeButton.icon"
class="h-4 w-4"
/>
</template>
</Button>
<Dropdown
v-show="showDropdown"
:options="parsedOptions"
size="sm"
class="flex-1 [&>div>div>div]:w-full"
placement="right"
>
<template v-slot="{ togglePopover }">
<Button
:variant="$attrs.variant"
@click="togglePopover"
icon="chevron-down"
class="!w-6 justify-start rounded-bl-none rounded-tl-none border-0 pr-0 text-xs"
/>
</template>
</Dropdown>
</div>
</template>
<script setup>
import { Dropdown } from 'frappe-ui'
import { computed, ref } from 'vue'
const props = defineProps({
options: {
type: Array,
default: () => [],
},
})
const showDropdown = ref(props.options?.length > 1)
const activeButton = ref(props.options?.[0] || {})
const parsedOptions = computed(() => {
return (
props.options?.map((option) => {
return {
label: option.label,
onClick: () => {
activeButton.value = option
},
}
}) || []
)
})
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="flex items-center justify-between p-1 border-b border-gray-200 cursor-pointer">
<!-- avatar and name -->
<div class="flex items-center justify-between gap-2">
<EmailProviderIcon :logo="emailIcon[emailAccount.service]" />
<div>
<p class="text-sm font-semibold text-gray-700">
{{ emailAccount.email_account_name }}
</p>
<div class="text-sm text-gray-500">{{ emailAccount.email_id }}</div>
</div>
</div>
<div>
<Badge variant="subtle" :label="badgeTitleColor[0]" :theme="badgeTitleColor[1]" />
</div>
<!-- email id -->
</div>
</template>
<script setup>
import { emailIcon } from "./emailConfig";
import EmailProviderIcon from "./EmailProviderIcon.vue";
import { computed } from "vue";
const props = defineProps({
emailAccount: {
type: Object,
required: true
}
});
const badgeTitleColor = computed(() => {
if (
props.emailAccount.default_incoming &&
props.emailAccount.default_outgoing
) {
const color =
props.emailAccount.enable_incoming && props.emailAccount.enable_outgoing
? "blue"
: "gray";
return ["Default Sending and Inbox", color];
} else if (props.emailAccount.default_incoming) {
const color = props.emailAccount.enable_incoming ? "blue" : "gray";
return ["Default Inbox", color];
} else if (props.emailAccount.default_outgoing) {
const color = props.emailAccount.enable_outgoing ? "blue" : "gray";
return ["Default Sending", color];
} else {
const color = props.emailAccount.enable_incoming ? "blue" : "gray";
return ["Inbox", color];
}
});
</script>
<style scoped></style>

View File

@ -0,0 +1,50 @@
<template>
<div>
<!-- header -->
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold">Email Accounts</h1>
<Button label="Add Account" theme="gray" variant="solid" @click="emit('update:step', 'email-add')" class="mr-8">
<template #prefix>
<LucidePlus class="w-4 h-4" />
</template>
</Button>
</div>
<!-- list accounts -->
<div v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)" class="mt-4">
<div v-for="emailAccount in emailAccounts.data" :key="emailAccount.name">
<EmailAccountCard :emailAccount="emailAccount" @click="emit('update:step', 'email-edit', emailAccount)" />
</div>
</div>
<!-- fallback if no email accounts -->
<div v-else class="flex items-center justify-center h-64 text-gray-500">
Please add an email account to continue.
</div>
</div>
</template>
<script setup>
import { createListResource } from "frappe-ui";
import EmailAccountCard from "./EmailAccountCard.vue";
const emit = defineEmits(["update:step"]);
const emailAccounts = createListResource({
doctype: "Email Account",
cache: true,
fields: ["*"],
filters: {
email_id: ["Not Like", "%example%"],
},
pageLength: 10,
auto: true,
onSuccess: (accounts) => {
// convert 0 to false to handle boolean fields
accounts.forEach((account) => {
account.enable_incoming = Boolean(account.enable_incoming);
account.enable_outgoing = Boolean(account.enable_outgoing);
account.default_incoming = Boolean(account.default_incoming);
account.default_outgoing = Boolean(account.default_outgoing);
});
},
});
</script>

View File

@ -0,0 +1,121 @@
<template>
<div class="flex flex-col h-full gap-4">
<!-- title and desc -->
<div role="heading" aria-level="1" class="flex flex-col gap-1">
<h5 class="text-xl font-semibold">Setup Email</h5>
<p class="text-sm text-gray-600">
Choose the email service provider you want to configure.
</p>
</div>
<!-- email service provider selection -->
<div class="flex flex-wrap items-center">
<div v-for="s in services" :key="s.name" class="flex flex-col items-center gap-1 mt-4 w-[70px]"
@click="handleSelect(s)">
<EmailProviderIcon :service-name="s.name" :logo="s.icon" :selected="selectedService?.name === s?.name" />
</div>
</div>
<div v-if="selectedService" class="flex flex-col gap-4">
<!-- email service provider info -->
<div class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-200">
<CircleAlert class="w-5 h-6 text-blue-500 w-min-5 w-max-5 min-h-5 max-w-5" />
<div class="text-xs text-gray-700 text-wrap">
{{ selectedService.info }}
<a :href="selectedService.link" target="_blank" class="text-blue-500 underline">here</a>.
</div>
</div>
<!-- service provider fields -->
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4">
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1">
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type"
:placeholder="field.placeholder" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div v-for="field in incomingOutgoingFields" :key="field.name" class="flex flex-col gap-1">
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" />
<p class="text-gray-500 text-p-sm">{{ field.description }}</p>
</div>
</div>
<ErrorMessage v-if="error" class="ml-1" :message="error" />
</div>
</div>
<!-- action button -->
<div v-if="selectedService" class="flex justify-between mt-auto">
<Button label="Back" theme="gray" variant="outline" :disabled="addEmailRes.loading"
@click="emit('update:step', 'email-list')" />
<Button label="Create" variant="solid" :loading="addEmailRes.loading" @click="createEmailAccount" />
</div>
</div>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { createResource } from "frappe-ui";
import CircleAlert from "~icons/lucide/circle-alert";
import { createToast } from "@/utils";
import {
customProviderFields,
popularProviderFields,
services,
validateInputs,
incomingOutgoingFields,
} from "./emailConfig";
import EmailProviderIcon from "./EmailProviderIcon.vue";
const emit = defineEmits();
const state = reactive({
service: "",
email_account_name: "",
email_id: "",
password: "",
api_key: "",
api_secret: "",
frappe_mail_site: "",
enable_incoming: false,
enable_outgoing: false,
default_incoming: false,
default_outgoing: false,
});
const selectedService = ref(null);
const fields = computed(() =>
selectedService.value.custom ? customProviderFields : popularProviderFields
);
function handleSelect(service) {
selectedService.value = service;
state.service = service.name;
}
const addEmailRes = createResource({
url: "crm.api.settings.create_email_account",
makeParams: (val) => {
return {
...val,
};
},
onSuccess: () => {
createToast({
title: "Email account created successfully",
icon: "check",
iconClasses: "text-green-600",
});
emit("update:step", "email-list");
},
onError: () => {
error.value = "Failed to create email account, Invalid credentials";
},
});
const error = ref();
function createEmailAccount() {
error.value = validateInputs(state, selectedService.value.custom);
if (error.value) return;
addEmailRes.submit({ data: state });
}
</script>
<style scoped></style>

View File

@ -0,0 +1,27 @@
<template>
<div class="flex-1 p-8">
<div v-if="step === 'email-add'" class="h-full">
<EmailAdd @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-list'" class="h-full">
<EmailAccountList @update:step="updateStep" />
</div>
<div v-else-if="step === 'email-edit'" class="h-full">
<EmailEdit :account-data="accountData" @update:step="updateStep" />
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import EmailAdd from "./EmailAdd.vue";
import EmailAccountList from "./EmailAccountList.vue";
import EmailEdit from "./EmailEdit.vue";
const step = ref("email-list");
const accountData = ref(null);
function updateStep(newStep, data) {
step.value = newStep;
accountData.value = data;
}
</script>

View File

@ -0,0 +1,186 @@
<template>
<div class="flex flex-col h-full gap-4">
<!-- title and desc -->
<div role="heading" aria-level="1" class="flex justify-between gap-1">
<h5 class="text-lg font-semibold">Edit Email</h5>
</div>
<div class="w-fit">
<EmailProviderIcon :logo="emailIcon[accountData.service]" :service-name="accountData.service" />
</div>
<!-- banner for setting up email account -->
<div class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-200">
<CircleAlert class="w-5 h-6 text-blue-500 w-min-5 w-max-5 min-h-5 max-w-5" />
<div class="text-xs text-gray-700 text-wrap">
{{ info.description }}
<a :href="info.link" target="_blank" class="text-blue-500 underline">here</a>
.
</div>
</div>
<!-- fields -->
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-4">
<div v-for="field in fields" :key="field.name" class="flex flex-col gap-1">
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type"
:placeholder="field.placeholder" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div v-for="field in incomingOutgoingFields" :key="field.name" class="flex flex-col gap-1">
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" />
<p class="text-gray-500 text-p-sm">{{ field.description }}</p>
</div>
</div>
<ErrorMessage v-if="error" class="ml-1" :message="error" />
</div>
<!-- action buttons -->
<div class="flex justify-between mt-auto">
<Button label="Back" theme="gray" variant="outline" :disabled="loading"
@click="emit('update:step', 'email-list')" />
<Button label="Update Account" variant="solid" @click="updateAccount" :loading="loading" />
</div>
</div>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { call } from "frappe-ui";
import EmailProviderIcon from "./EmailProviderIcon.vue";
import {
emailIcon,
services,
popularProviderFields,
customProviderFields,
validateInputs,
incomingOutgoingFields,
} from "./emailConfig";
import { createToast } from "@/utils";
import CircleAlert from "~icons/lucide/circle-alert";
const props = defineProps({
accountData: null,
})
const emit = defineEmits();
const state = reactive({
email_account_name: props.accountData.email_account_name || "",
service: props.accountData.service || "",
email_id: props.accountData.email_id || "",
api_key: props.accountData?.api_key || null,
api_secret: props.accountData?.api_secret || null,
password: props.accountData?.password || null,
frappe_mail_site: props.accountData?.frappe_mail_site || "",
enable_incoming: props.accountData.enable_incoming || false,
enable_outgoing: props.accountData.enable_outgoing || false,
default_outgoing: props.accountData.default_outgoing || false,
default_incoming: props.accountData.default_incoming || false,
});
const info = {
description: "To know more about setting up email accounts, click",
link: "https://docs.erpnext.com/docs/user/manual/en/email-account",
};
const isCustomService = computed(() => {
return services.find((s) => s.name === props.accountData.service).custom;
});
const fields = computed(() => {
if (isCustomService.value) {
return customProviderFields;
}
return popularProviderFields;
});
const error = ref();
const loading = ref(false);
async function updateAccount() {
error.value = validateInputs(state, isCustomService.value);
if (error.value) return;
const old = { ...props.accountData };
const updatedEmailAccount = { ...state };
const nameChanged =
old.email_account_name !== updatedEmailAccount.email_account_name;
delete old.email_account_name;
delete updatedEmailAccount.email_account_name;
const otherFieldsChanged = isDirty.value;
const values = updatedEmailAccount;
if (!nameChanged && !otherFieldsChanged) {
createToast({
title: "No changes made",
icon: "info",
iconClasses: "text-blue-600",
});
return;
}
if (nameChanged) {
try {
loading.value = true;
await callRenameDoc();
succesHandler();
} catch (err) {
errorHandler();
}
}
if (otherFieldsChanged) {
try {
loading.value = true;
await callSetValue(values);
succesHandler();
} catch (err) {
errorHandler();
}
}
}
const isDirty = computed(() => {
return (
state.email_id !== props.accountData.email_id ||
state.api_key !== props.accountData.api_key ||
state.api_secret !== props.accountData.api_secret ||
state.password !== props.accountData.password ||
state.enable_incoming !== props.accountData.enable_incoming ||
state.enable_outgoing !== props.accountData.enable_outgoing ||
state.default_outgoing !== props.accountData.default_outgoing ||
state.default_incoming !== props.accountData.default_incoming ||
state.frappe_mail_site !== props.accountData.frappe_mail_site
);
});
async function callRenameDoc() {
const d = await call("frappe.client.rename_doc", {
doctype: "Email Account",
old_name: props.accountData.email_account_name,
new_name: state.email_account_name,
});
return d;
}
async function callSetValue(values) {
const d = await call("frappe.client.set_value", {
doctype: "Email Account",
name: state.email_account_name,
fieldname: values,
});
return d.name;
}
function succesHandler() {
emit("update:step", "email-list");
createToast({
title: "Email account updated successfully",
icon: "check",
iconClasses: "text-green-600",
});
}
function errorHandler() {
loading.value = false;
error.value = "Failed to update email account, Invalid credentials";
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<div class="flex items-center justify-center w-8 h-8 bg-gray-100 cursor-pointer rounded-xl hover:bg-gray-200"
:class="{ 'ring-2 ring-blue-500': selected }">
<img :src="logo" class="w-4 h-4" />
</div>
<p v-if="serviceName" class="text-xs text-center text-gray-700">
{{ serviceName }}
</p>
</template>
<script setup>
defineProps({
logo: {
type: String,
required: true
},
serviceName: {
type: String,
default: ''
},
selected: {
type: Boolean,
default: false
}
})
</script>
<style scoped></style>

View File

@ -6,8 +6,8 @@
> >
<template #body> <template #body>
<div class="flex h-[calc(100vh_-_8rem)]"> <div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2"> <div class="flex flex-col p-2 w-52 shrink-0 bg-surface-gray-2">
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9"> <h1 class="px-2 pt-2 mb-3 text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }} {{ __('Settings') }}
</h1> </h1>
<div v-for="tab in tabs"> <div v-for="tab in tabs">
@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<div <div
class="flex relative flex-1 flex-col overflow-y-auto bg-surface-modal" class="relative flex flex-col flex-1 overflow-y-auto bg-surface-modal"
> >
<Button <Button
class="absolute right-5 top-5" class="absolute right-5 top-5"
@ -53,12 +53,14 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue' import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import InviteIcon from '@/components/Icons/InviteIcon.vue' import InviteIcon from '@/components/Icons/InviteIcon.vue'
import Email2Icon from '@/components/Icons/Email2Icon.vue'
import GeneralSettings from '@/components/Settings/GeneralSettings.vue' import GeneralSettings from '@/components/Settings/GeneralSettings.vue'
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue' import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue' import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue' import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue' import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
import TelephonySettings from '@/components/Settings/TelephonySettings.vue' import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
import EmailConfig from '@/components/Settings/EmailConfig.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { import {
@ -101,6 +103,12 @@ const tabs = computed(() => {
component: markRaw(InviteMemberPage), component: markRaw(InviteMemberPage),
condition: () => isManager(), condition: () => isManager(),
}, },
{
label: __('Email Accounts'),
icon: Email2Icon,
component: markRaw(EmailConfig),
condition: () => isManager(),
},
], ],
}, },
{ {

View File

@ -0,0 +1,179 @@
import { validateEmail } from '../../utils'
import LogoGmail from '@/images/gmail.png'
import LogoOutlook from '@/images/outlook.png'
import LogoSendgrid from '@/images/sendgrid.png'
import LogoSparkpost from '@/images/sparkpost.webp'
import LogoYahoo from '@/images/yahoo.png'
import LogoYandex from '@/images/yandex.png'
import LogoFrappeMail from '@/images/frappe-mail.svg'
const fixedFields = [
{
label: 'Account Name',
name: 'email_account_name',
type: 'text',
placeholder: 'Support / Sales',
},
{
label: 'Email ID',
name: 'email_id',
type: 'email',
placeholder: 'johndoe@example.com',
},
]
export const incomingOutgoingFields = [
{
label: 'Enable Incoming',
name: 'enable_incoming',
type: 'checkbox',
description:
'If enabled, records can be created from the incoming emails on this account.',
},
{
label: 'Enable Outgoing',
name: 'enable_outgoing',
type: 'checkbox',
description: 'If enabled, outgoing emails can be sent from this account.',
},
{
label: 'Default Incoming',
name: 'default_incoming',
type: 'checkbox',
description:
'If enabled, all replies to your company (eg: replies@yourcomany.com) will come to this account. Note: Only one account can be default incoming.',
},
{
label: 'Default Outgoing',
name: 'default_outgoing',
type: 'checkbox',
description:
'If enabled, all outgoing emails will be sent from this account. Note: Only one account can be default outgoing.',
},
]
export const popularProviderFields = [
...fixedFields,
{
label: 'Password',
name: 'password',
type: 'password',
placeholder: '********',
},
]
export const customProviderFields = [
...fixedFields,
{
label: 'Frappe Mail Site',
name: 'frappe_mail_site',
type: 'text',
placeholder: 'https://frappemail.com',
},
{
label: 'API Key',
name: 'api_key',
type: 'text',
placeholder: '********',
},
{
label: 'API Secret',
name: 'api_secret',
type: 'password',
placeholder: '********',
},
]
export const services = [
{
name: 'GMail',
icon: LogoGmail,
info: `Setting up GMail requires you to enable two factor authentication
and app specific passwords. Read more`,
link: 'https://support.google.com/accounts/answer/185833',
custom: false,
},
{
name: 'Outlook',
icon: LogoOutlook,
info: `Setting up Outlook requires you to enable two factor authentication
and app specific passwords. Read more`,
link: 'https://support.microsoft.com/en-us/account-billing/how-to-get-and-use-app-passwords-5896ed9b-4263-e681-128a-a6f2979a7944',
custom: false,
},
{
name: 'Sendgrid',
icon: LogoSendgrid,
info: `Setting up Sendgrid requires you to enable two factor authentication
and app specific passwords. Read more `,
link: 'https://sendgrid.com/docs/ui/account-and-settings/two-factor-authentication/',
custom: false,
},
{
name: 'SparkPost',
icon: LogoSparkpost,
info: `Setting up SparkPost requires you to enable two factor authentication
and app specific passwords. Read more `,
link: 'https://support.sparkpost.com/docs/my-account-and-profile/enabling-two-factor-authentication',
custom: false,
},
{
name: 'Yahoo',
icon: LogoYahoo,
info: `Setting up Yahoo requires you to enable two factor authentication
and app specific passwords. Read more `,
link: 'https://help.yahoo.com/kb/SLN15241.html',
custom: false,
},
{
name: 'Yandex',
icon: LogoYandex,
info: `Setting up Yandex requires you to enable two factor authentication
and app specific passwords. Read more `,
link: 'https://yandex.com/support/id/authorization/app-passwords.html',
custom: false,
},
{
name: 'Frappe Mail',
icon: LogoFrappeMail,
info: `Setting up Frappe Mail requires you to have an API key and API Secret of your email account. Read more `,
link: 'https://github.com/frappe/mail',
custom: true,
},
]
export const emailIcon = {
GMail: LogoGmail,
Outlook: LogoOutlook,
Sendgrid: LogoSendgrid,
SparkPost: LogoSparkpost,
Yahoo: LogoYahoo,
Yandex: LogoYandex,
'Frappe Mail': LogoFrappeMail,
}
export function validateInputs(state, isCustom) {
if (!state.email_account_name) {
return 'Account name is required'
}
if (!state.email_id) {
return 'Email ID is required'
}
const validEmail = validateEmail(state.email_id)
if (!validEmail) {
return 'Invalid email ID'
}
if (!isCustom && !state.password) {
return 'Password is required'
}
if (isCustom) {
if (!state.api_key) {
return 'API Key is required'
}
if (!state.api_secret) {
return
}
}
return ''
}

View File

@ -215,10 +215,16 @@
</template> </template>
</Link> </Link>
<Link <Link
v-else-if="field.fieldtype === 'Link'" v-else-if="
['Link', 'Dynamic Link'].includes(field.fieldtype)
"
class="form-control select-text" class="form-control select-text"
:value="data[field.fieldname]" :value="data[field.fieldname]"
:doctype="field.options" :doctype="
field.fieldtype == 'Link'
? field.options
: data[field.options]
"
:filters="field.filters" :filters="field.filters"
:placeholder="field.placeholder" :placeholder="field.placeholder"
@change=" @change="

View File

@ -0,0 +1,4 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5714 44L31.4286 44C38.3716 44 44 38.3716 44 31.4286L44 12.5714C44 5.62842 38.3716 0 31.4286 0L12.5714 0C5.62842 0 0 5.62842 0 12.5714L0 31.4286C0 38.3716 5.62842 44 12.5714 44Z" fill="#0466DC"/>
<path d="M9.42859 12.5715V14.8972L12.5714 17.4587L18.5743 22.3458C19.5329 23.1315 20.7586 23.5715 22 23.5715C23.2414 23.5715 24.4672 23.1315 25.4257 22.3458L31.4286 17.443V28.2701H12.5714V21.5287L9.42859 18.9672V27.4844C9.42859 29.653 11.1886 31.413 13.3572 31.413H30.6429C32.8115 31.413 34.5715 29.653 34.5715 27.4844V12.5715H9.42859ZM23.4457 19.9101C22.6286 20.5701 21.3714 20.5701 20.57 19.9101L15.4157 15.7144H28.6L23.4457 19.9101Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -545,7 +545,6 @@ const tabs = computed(() => {
name: 'Calls', name: 'Calls',
label: __('Calls'), label: __('Calls'),
icon: PhoneIcon, icon: PhoneIcon,
condition: () => callEnabled.value,
}, },
{ {
name: 'Tasks', name: 'Tasks',

View File

@ -457,9 +457,6 @@ function parseRows(rows, columns = []) {
} }
} else if (row == '_assign') { } else if (row == '_assign') {
let assignees = JSON.parse(deal._assign || '[]') let assignees = JSON.parse(deal._assign || '[]')
if (!assignees.length && deal.deal_owner) {
assignees = [deal.deal_owner]
}
_rows[row] = assignees.map((user) => ({ _rows[row] = assignees.map((user) => ({
name: user, name: user,
image: getUser(user).user_image, image: getUser(user).user_image,

View File

@ -532,7 +532,6 @@ const tabs = computed(() => {
name: 'Calls', name: 'Calls',
label: __('Calls'), label: __('Calls'),
icon: PhoneIcon, icon: PhoneIcon,
condition: () => callEnabled.value,
}, },
{ {
name: 'Tasks', name: 'Tasks',

View File

@ -480,9 +480,6 @@ function parseRows(rows, columns = []) {
} }
} else if (row == '_assign') { } else if (row == '_assign') {
let assignees = JSON.parse(lead._assign || '[]') let assignees = JSON.parse(lead._assign || '[]')
if (!assignees.length && lead.lead_owner) {
assignees = [lead.lead_owner]
}
_rows[row] = assignees.map((user) => ({ _rows[row] = assignees.map((user) => ({
name: user, name: user,
image: getUser(user).user_image, image: getUser(user).user_image,

13
frontend/src/types.ts Normal file
View File

@ -0,0 +1,13 @@
export interface EmailAccount {
email_account_name: string
email_id: string
service: string
api_key?: string
api_secret?: string
password?: string
frappe_mail_site?: string
enable_outgoing?: boolean
enable_incoming?: boolean
default_outgoing?: boolean
default_incoming?: boolean
}

View File

@ -2542,10 +2542,10 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
frappe-ui@^0.1.121: frappe-ui@^0.1.123:
version "0.1.121" version "0.1.123"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.121.tgz#a8d37f300228edfcbb6b4fffb343f0773dcfd933" resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.123.tgz#501139a103a03e52648d9ee9ea85aa54bc8102e0"
integrity sha512-gvtKKZECPD2MU5X4MwPUKr2hSOs1+s1DA9laP3aPnmH0ukJRSFEhDOyjCMfH9k6ZdAe/vZCIbT4XucxLq/fOEA== integrity sha512-WkTnKZ+n82d9xZ9g9ZQXVkFyKU2wlcfT6/9g8/2biJuXMwmo/80I29EKGb9nrM1Liuj0Wtyg9nsqvfvgktdHbw==
dependencies: dependencies:
"@headlessui/vue" "^1.7.14" "@headlessui/vue" "^1.7.14"
"@popperjs/core" "^2.11.2" "@popperjs/core" "^2.11.2"