Compare commits

...

80 Commits

Author SHA1 Message Date
Frappe PR Bot
cf849f7dff chore(release): Bumped to Version 1.53.1 2025-09-25 16:33:11 +00:00
Shariq Ansari
3e9fdb8d20
Merge pull request #1287 from frappe/main-hotfix 2025-09-25 22:02:44 +05:30
Shariq Ansari
260755fd2e
Merge pull request #1286 from frappe/mergify/bp/main-hotfix/pr-1279 2025-09-25 21:57:28 +05:30
Shariq Ansari
85df56b464
Merge pull request #1285 from frappe/mergify/bp/main-hotfix/pr-1284 2025-09-25 21:53:53 +05:30
Shariq Ansari
a9e956b5fc chore: Norwegian Bokmal translations
(cherry picked from commit 9e92282e25cd1d3e5c33b87b02ad6dfdc09158cc)
2025-09-25 16:23:42 +00:00
Shariq Ansari
905eb63c5f chore: Serbian (Latin) translations
(cherry picked from commit 0138716e070b8098fd37c3ee67496c7a0d1ff473)
2025-09-25 16:23:42 +00:00
Shariq Ansari
3eee437014 chore: Serbian (Cyrillic) translations
(cherry picked from commit 589c95e263bf70fda044ef841a623ccda510c744)
2025-09-25 16:23:42 +00:00
Shariq Ansari
4498de2041 chore: Norwegian Bokmal translations
(cherry picked from commit a4e2663b2418c935c8279fab97934dff0d2221b7)
2025-09-25 16:23:41 +00:00
Shariq Ansari
bfb0d25765 chore: Norwegian Bokmal translations
(cherry picked from commit f91ac266ca921427fa2d38af4e8d8f847d46e111)
2025-09-25 16:23:41 +00:00
Shariq Ansari
9dd6ffa1e1 chore: Danish translations
(cherry picked from commit 415d5410ba707a59a6cc6a7cf8d0bafc8a6a7e50)
2025-09-25 16:23:41 +00:00
Shariq Ansari
05322e805f chore: Esperanto translations
(cherry picked from commit 9fc20a3078922b45440551343f7fb032d17d814b)
2025-09-25 16:23:40 +00:00
Shariq Ansari
8dc6db3218 chore: Croatian translations
(cherry picked from commit 43dadfe746f3ade875bf5420edca1b3bc92cc372)
2025-09-25 16:23:40 +00:00
Shariq Ansari
1b63384eac chore: Thai translations
(cherry picked from commit 56071d5e0dd520d96b24ce92d9079823921402aa)
2025-09-25 16:23:40 +00:00
Shariq Ansari
37bbbb6b4b chore: Persian translations
(cherry picked from commit b0e79d0aef3a3fe637c6d0d2d734118b095a52a5)
2025-09-25 16:23:40 +00:00
Shariq Ansari
6f0244c2b6 chore: Vietnamese translations
(cherry picked from commit 5848649955c084076afda007e21df1b0940394f6)
2025-09-25 16:23:39 +00:00
Shariq Ansari
e08bc9cd20 chore: Chinese Simplified translations
(cherry picked from commit 3c5ee979f86d9a0cdbd6717c91253622e47e3f8d)
2025-09-25 16:23:39 +00:00
Shariq Ansari
2043157567 chore: Turkish translations
(cherry picked from commit 2acd7476c8e1f92f2747a84666badafe5b204910)
2025-09-25 16:23:39 +00:00
Shariq Ansari
8093a422cd chore: Russian translations
(cherry picked from commit 66c586582836bcbd3210a4cd2e254edf3fc9b9f5)
2025-09-25 16:23:38 +00:00
Shariq Ansari
4bef919d38 chore: Portuguese translations
(cherry picked from commit 103a137af12912bcf388b06a14c84abadbad8979)
2025-09-25 16:23:38 +00:00
Shariq Ansari
874947b8ae chore: Dutch translations
(cherry picked from commit 52cc70d704a284bd41696e67e2c1fb24142dc6ee)
2025-09-25 16:23:38 +00:00
Shariq Ansari
ca90c0406e chore: Hungarian translations
(cherry picked from commit d72dcee7b663d35c83a5658815a1cdd529b445c3)
2025-09-25 16:23:38 +00:00
Shariq Ansari
6bf27d852b chore: Czech translations
(cherry picked from commit b473b27f9ace8d76023b13ec1cf57e81cc487835)
2025-09-25 16:23:37 +00:00
Shariq Ansari
793cb76789 chore: Arabic translations
(cherry picked from commit 8805560144baa224c6f3ac5758e9d3d288e60f45)
2025-09-25 16:23:37 +00:00
Shariq Ansari
ff07054ca3 chore: Spanish translations
(cherry picked from commit 3f0c4e9614d8081344dd8ef213bed525c69a4b1a)
2025-09-25 16:23:37 +00:00
Shariq Ansari
2ee5269d3e chore: French translations
(cherry picked from commit 2b13c3f27704a8b50facb7636f4a82810e843bd6)
2025-09-25 16:23:36 +00:00
Shariq Ansari
e27954da52 chore: German translations
(cherry picked from commit b6fa3bf32b3c21e82917061da94f450e81f48323)
2025-09-25 16:23:36 +00:00
Shariq Ansari
2faa0d0f04 chore: Serbian (Latin) translations
(cherry picked from commit ae9e59aa0002112fa22192d15df43491b1145c8c)
2025-09-25 16:23:36 +00:00
Shariq Ansari
f1664eec2f chore: Bosnian translations
(cherry picked from commit 4533becc620ea06bc61043eb58e8350b90445950)
2025-09-25 16:23:36 +00:00
Shariq Ansari
7f2efea7cb chore: Indonesian translations
(cherry picked from commit 57bd9fe70a6f09d336b8b17dd1215481f2f5d03d)
2025-09-25 16:23:35 +00:00
Shariq Ansari
d2cc6b7c2e chore: Portuguese, Brazilian translations
(cherry picked from commit 013c21a5d1b564632b0383893d4b45512afa9e4c)
2025-09-25 16:23:35 +00:00
Shariq Ansari
1afb6d6827 chore: Swedish translations
(cherry picked from commit 9a780039e589e369f76f4fe9b4e0dff0902bd859)
2025-09-25 16:23:35 +00:00
Shariq Ansari
ccaf136830 chore: Serbian (Cyrillic) translations
(cherry picked from commit 1bd62289dc720edc9515a3677ac377732ae1dc60)
2025-09-25 16:23:34 +00:00
Shariq Ansari
58a41d1b11 chore: Polish translations
(cherry picked from commit f7382f40ac78be3804f34866162f30329f742d76)
2025-09-25 16:23:34 +00:00
Shariq Ansari
777f3ac06c chore: Italian translations
(cherry picked from commit 3c870ce042a7d165ad914f90cf84fd5473ad8243)
2025-09-25 16:23:34 +00:00
Shariq Ansari
105c78e264
Merge pull request #1283 from frappe/mergify/bp/main-hotfix/pr-1282
fix: add validation for mandatory fields in useDocument (backport #1282)
2025-09-25 21:53:09 +05:30
Shariq Ansari
46cc1d2924 build(deps): bump frappeui to 0.1.201
(cherry picked from commit 171060df8ade73f6b9ef4b51945c76604c759299)
2025-09-25 16:22:33 +00:00
Shariq Ansari
ff4ca9fe66 fix: add validation for mandatory fields in useDocument
(cherry picked from commit dbcda4c548270f4b030d819857b1f393fdaadecb)
2025-09-25 16:18:30 +00:00
Shariq Ansari
4989dc0921
Merge pull request #1277 from shariquerik/backport-1125
fix: Bulk Delete "Reference Doctype must be set first" Error (backport #1125)
2025-09-22 16:12:16 +05:30
Shariq Ansari
1e613ebcd1 fix: Bulk Delete 'Reference Doctype must be set first' Error backport (#1125) 2025-09-22 16:04:53 +05:30
Frappe PR Bot
4f8f195d77 chore(release): Bumped to Version 1.53.0 2025-09-22 09:55:37 +00:00
Shariq Ansari
af64b86a04
Merge pull request #1276 from frappe/main-hotfix 2025-09-22 15:25:13 +05:30
Shariq Ansari
7e9bc0524e
Merge pull request #1275 from frappe/mergify/bp/main-hotfix/pr-1266 2025-09-22 15:18:12 +05:30
Shariq Ansari
9d0a0d1d32
Merge pull request #1273 from frappe/mergify/bp/main-hotfix/pr-1272 2025-09-22 15:17:51 +05:30
Shariq Ansari
9c84a8be7f
Merge pull request #1274 from frappe/mergify/bp/main-hotfix/pr-1262 2025-09-22 15:17:42 +05:30
frappe-pr-bot
d24537489e chore: update POT file
(cherry picked from commit 625e472303a4d759024a744bd93bc8d721537a0a)
2025-09-22 09:41:24 +00:00
Shariq Ansari
bd89b3b356 chore: Norwegian Bokmal translations
(cherry picked from commit ce632c69c1e634dc0feed03eec3f4c76370e7dcf)
2025-09-22 09:41:23 +00:00
Shariq Ansari
988fb90ddb chore: Norwegian Bokmal translations
(cherry picked from commit 93ed6fcdddac8a903e470ee35d1cbcca7e9ba5cf)
2025-09-22 09:41:23 +00:00
Shariq Ansari
8018b1766c chore: Norwegian Bokmal translations
(cherry picked from commit e3eff7f78de04f49bb78a3e1401048a42d4bf2eb)
2025-09-22 09:41:23 +00:00
Shariq Ansari
9c4c2a0aca chore: Norwegian Bokmal translations
(cherry picked from commit 394da5e0024dfce029f90346ff695dc7f7a67e5f)
2025-09-22 09:41:23 +00:00
Shariq Ansari
803e639961 build(deps): bump frappeui to 0.1.200
(cherry picked from commit 96c0c99939b30880ddf27b91f3f6b18c95ef3409)
2025-09-22 09:40:21 +00:00
Shariq Ansari
fabd362b2a
Merge pull request #1260 from frappe/mergify/bp/main-hotfix/pr-1256 2025-09-18 15:44:32 +05:30
Shariq Ansari
7dd98733f1
chore: resolved conflict 2025-09-18 15:38:44 +05:30
Shariq Ansari
ee4b7721b0
chore: resolved conflict 2025-09-18 15:37:11 +05:30
Shariq Ansari
95bc551254
chore: resolved conflict 2025-09-18 15:35:03 +05:30
Shariq Ansari
2546bdabb1 refactor: adjust padding and improve layout for Settings component
(cherry picked from commit d687a2eb56142c49b2b776dec55ad533556421f6)
2025-09-18 09:59:25 +00:00
Shariq Ansari
af248964c6 refactor: adjust padding and improve layout for Currency and Forecasting settings components
(cherry picked from commit 1044adc494b0b5b7f347aaf0080914a28be56eb9)
2025-09-18 09:59:25 +00:00
Shariq Ansari
8749f7bfd0 refactor: update styling and improve layout for assignment rules components
(cherry picked from commit 9f95a3a2b2132357708db03b9b4922f356150ff4)
2025-09-18 09:59:25 +00:00
Shariq Ansari
d18618b856 refactor: replace EmailMultiSelect with FormControl for inviting users by email
(cherry picked from commit 69f80903118965f500199f1b3368deb195882699)

# Conflicts:
#	frontend/src/components/Settings/InviteUserPage.vue
2025-09-18 09:59:24 +00:00
Shariq Ansari
ce4af4907a refactor: remove TemplateOption component usage and simplify dropdown options in multiple components
(cherry picked from commit ac34ac9b87b9c671be75401a545e09e0e83ac378)

# Conflicts:
#	frontend/src/components/Settings/Users.vue
2025-09-18 09:59:24 +00:00
Shariq Ansari
84d24a384b refactor: update Vite configuration to support dynamic loading of frappe-ui in development mode
(cherry picked from commit 129f8a00b66d87529c21ef085e66dc7864a3776e)
2025-09-18 09:59:24 +00:00
Shariq Ansari
29d86859d4 revert: create dynamic alias to use components from frontend vue apps
(cherry picked from commit 6328b6941bb620e47cebe82519df3f1453f355ae)
2025-09-18 09:59:23 +00:00
Shariq Ansari
be452fee58 refactor: reduce gap in Brand logo and Favicon sections for improved layout
(cherry picked from commit fbc9e37036d5720948381ca891714d78af001433)
2025-09-18 09:59:23 +00:00
Shariq Ansari
100eec0677 refactor: remove icon-left from Update button in multiple settings components
(cherry picked from commit 149901f6054f000d503ef8940037c13bd2e344f3)
2025-09-18 09:59:23 +00:00
Shariq Ansari
a4d3852c0e feat: Auto update expected deal value based on products value
(cherry picked from commit 7e21a5fee206723cf714f7a8732bb8929aaa57df)
2025-09-18 09:59:23 +00:00
Shariq Ansari
0399fc32be refactor: add ForecastingSettings component and remove GeneralSettingsPage component
(cherry picked from commit f4ff6bbdf306a89b78aab82df972bf55e1e0d82d)
2025-09-18 09:59:22 +00:00
Shariq Ansari
a79192ef4d fix: add auto-update expected deal value checkbox in FCRM settings
(cherry picked from commit 915023317310178d67306bb6d8b48e224e5644ae)
2025-09-18 09:59:22 +00:00
Shariq Ansari
e10ec543a7 refactor: CurrencySettings component
(cherry picked from commit 186584c1ac79170afa4ef8860bff9d3d3d91cadc)
2025-09-18 09:59:22 +00:00
Shariq Ansari
1962b9a103 refactor: update BrandSettings component to improve logo and favicon handling
(cherry picked from commit 3752c611576778f028ba78d6c1ae533a88c6040b)
2025-09-18 09:59:21 +00:00
Shariq Ansari
a3abaa57ec refactor: HomeActions component
(cherry picked from commit a6ecc5cfeda51781286d013cf3a738b8a3adfdc5)
2025-09-18 09:59:21 +00:00
Shariq Ansari
1bf3f7a38c refactor: BrandSettings component
(cherry picked from commit 84e0fe30a9667468ea9ddbb0bc768e3da62620d3)
2025-09-18 09:59:20 +00:00
Shariq Ansari
af81750388 refactor: enhance Settings component structure
(cherry picked from commit 03acea69b130cfdbdc00994c8d9930ad948cb64c)

# Conflicts:
#	frontend/src/components/Settings/Settings.vue
2025-09-18 09:59:20 +00:00
Shariq Ansari
7330a3c2a5 refactor: clean up ImageUploader component and improve label handling
(cherry picked from commit e19f75083147faaf988bcbef448e9d1ddbb9179a)
2025-09-18 09:59:19 +00:00
Shariq Ansari
9e8a4024a5
Merge pull request #1255 from frappe/mergify/bp/main-hotfix/pr-1252
fix: paddings and labels (backport #1252)
2025-09-18 15:27:27 +05:30
Shariq Ansari
7ef00965fa
Merge branch 'main-hotfix' into mergify/bp/main-hotfix/pr-1252 2025-09-18 15:21:52 +05:30
Shariq Ansari
97925aae15
Merge pull request #1259 from frappe/mergify/bp/main-hotfix/pr-1257 2025-09-18 15:17:19 +05:30
Pratik Badhe
ce66705e9c revert: yarn.lock file
(cherry picked from commit 41ef219d0abd0c0036d7697dc5a8b8ab78a81344)
2025-09-17 06:47:25 +00:00
Pratik Badhe
004923419c fix: paddings and labels
(cherry picked from commit db577afc568b56b49846773b16b638e0cf1444fa)

# Conflicts:
#	frontend/src/components/Settings/AssignmentRules/AssigneeRules.vue
#	frontend/src/components/Settings/AssignmentRules/AssignmentRuleView.vue
#	frontend/src/components/Settings/AssignmentRules/AssignmentRules.vue
2025-09-17 06:47:25 +00:00
Shariq Ansari
ff312d964b
Merge pull request #1251 from frappe/main-hotfix 2025-09-16 15:23:38 +05:30
Frappe PR Bot
17b7c6ecef chore(release): Bumped to Version 1.52.11 2025-09-08 14:20:41 +00:00
Shariq Ansari
8d7a155d78
Merge pull request #1239 from frappe/main-hotfix 2025-09-08 19:49:39 +05:30
67 changed files with 10687 additions and 9043 deletions

1
.gitignore vendored
View File

@ -7,6 +7,5 @@ dev-dist
tags
node_modules
crm/public/frontend
frontend/yarn.lock
crm/www/crm.html
build

View File

@ -1,4 +1,4 @@
__version__ = "1.52.10"
__version__ = "1.53.1"
__title__ = "Frappe CRM"

View File

@ -750,7 +750,11 @@ def getCounts(d, doctype):
@frappe.whitelist()
def get_linked_docs_of_document(doctype, docname):
doc = frappe.get_doc(doctype, docname)
try:
doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
return []
linked_docs = get_linked_docs(doc)
dynamic_linked_docs = get_dynamic_linked_docs(doc)
@ -759,7 +763,14 @@ def get_linked_docs_of_document(doctype, docname):
docs_data = []
for doc in linked_docs:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
if not doc.get("reference_doctype") or not doc.get("reference_docname"):
continue
try:
data = frappe.get_doc(doc["reference_doctype"], doc["reference_docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
continue
title = data.get("title")
if data.doctype == "CRM Call Log":
title = f"Call from {data.get('from')} to {data.get('to')}"
@ -767,6 +778,9 @@ def get_linked_docs_of_document(doctype, docname):
if data.doctype == "CRM Deal":
title = data.get("organization")
if data.doctype == "CRM Notification":
title = data.get("message")
docs_data.append(
{
"doc": data.doctype,
@ -779,25 +793,51 @@ def get_linked_docs_of_document(doctype, docname):
def remove_doc_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"reference_doctype": None,
"reference_docname": None,
}
)
linked_doc_data.save(ignore_permissions=True)
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
if doctype == "CRM Notification":
delete_notification_type = {
"notification_type_doctype": "",
"notification_type_doc": "",
}
delete_references = {
"reference_doctype": "",
"reference_name": "",
}
if linked_doc_data.get("notification_type_doctype") == linked_doc_data.get("reference_doctype"):
delete_references.update(delete_notification_type)
linked_doc_data.update(delete_references)
else:
linked_doc_data.update(
{
"reference_doctype": "",
"reference_docname": "",
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
def remove_contact_link(doctype, docname):
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
if not doctype or not docname:
return
try:
linked_doc_data = frappe.get_doc(doctype, docname)
linked_doc_data.update(
{
"contact": None,
"contacts": [],
}
)
linked_doc_data.save(ignore_permissions=True)
except (frappe.DoesNotExistError, frappe.ValidationError):
pass
@frappe.whitelist()
@ -806,13 +846,19 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
items = frappe.parse_json(items)
for item in items:
if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if not item.get("doctype") or not item.get("docname"):
continue
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
try:
if remove_contact:
remove_contact_link(item["doctype"], item["docname"])
else:
remove_doc_link(item["doctype"], item["docname"])
if delete:
frappe.delete_doc(item["doctype"], item["docname"])
except (frappe.DoesNotExistError, frappe.ValidationError):
# Skip if document doesn't exist or has validation errors
continue
return "success"
@ -821,19 +867,40 @@ def remove_linked_doc_reference(items, remove_contact=None, delete=False):
def delete_bulk_docs(doctype, items, delete_linked=False):
from frappe.desk.reportview import delete_bulk
if not doctype:
frappe.throw("Doctype is required")
if not items:
frappe.throw("Items are required")
items = frappe.parse_json(items)
if not isinstance(items, list):
frappe.throw("Items must be a list")
for doc in items:
linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs:
remove_linked_doc_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
],
remove_contact=doctype == "Contact",
delete=delete_linked,
try:
if not frappe.db.exists(doctype, doc):
frappe.log_error(f"Document {doctype} {doc} does not exist", "Bulk Delete Error")
continue
linked_docs = get_linked_docs_of_document(doctype, doc)
for linked_doc in linked_docs:
if not linked_doc.get("reference_doctype") or not linked_doc.get("reference_docname"):
continue
remove_linked_doc_reference(
[
{
"doctype": linked_doc["reference_doctype"],
"docname": linked_doc["reference_docname"],
}
],
remove_contact=doctype == "Contact",
delete=delete_linked,
)
except Exception as e:
frappe.log_error(
f"Error processing linked docs for {doctype} {doc}: {str(e)}", "Bulk Delete Error"
)
if len(items) > 10:

View File

@ -25,7 +25,7 @@ class CRMDeal(Document):
add_status_change_log(self)
if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
self.closed_date = frappe.utils.nowdate()
self.validate_forcasting_fields()
self.validate_forecasting_fields()
self.validate_lost_reason()
self.update_exchange_rate()
@ -151,9 +151,21 @@ class CRMDeal(Document):
if not self.probability or self.probability == 0:
self.probability = frappe.db.get_value("CRM Deal Status", self.status, "probability") or 0
def validate_forcasting_fields(self):
def update_expected_deal_value(self):
"""
Update the expected deal value based on the net total or total.
"""
if (
frappe.db.get_single_value("FCRM Settings", "auto_update_expected_deal_value")
and (self.net_total or self.total)
and self.expected_deal_value
):
self.expected_deal_value = self.net_total or self.total
def validate_forecasting_fields(self):
self.update_closed_date()
self.update_default_probability()
self.update_expected_deal_value()
if frappe.db.get_single_value("FCRM Settings", "enable_forecasting"):
if not self.expected_deal_value or self.expected_deal_value == 0:
frappe.throw(_("Expected Deal Value is required."), frappe.MandatoryError)

View File

@ -8,6 +8,7 @@
"defaults_tab",
"restore_defaults",
"enable_forecasting",
"auto_update_expected_deal_value",
"currency_tab",
"currency",
"exchange_rate_provider_section",
@ -105,12 +106,19 @@
{
"fieldname": "column_break_vqck",
"fieldtype": "Column Break"
},
{
"default": "1",
"description": "Automatically update \"Expected Deal Value\" based on the total value of associated products in a deal",
"fieldname": "auto_update_expected_deal_value",
"fieldtype": "Check",
"label": "Auto Update Expected Deal Value"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-07-29 11:26:50.420614",
"modified": "2025-09-16 17:33:26.406549",
"modified_by": "Administrator",
"module": "FCRM",
"name": "FCRM Settings",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit 02fc126fd5c49f0ecf6cce117585f89c4ea585c3
Subproject commit c9a0fc937cc897864857271b3708a0c675379015

View File

@ -33,7 +33,7 @@ declare module 'vue' {
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
@ -63,7 +63,7 @@ declare module 'vue' {
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
CurrencySettings: typeof import('./src/components/Settings/CurrencySettings.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
@ -127,11 +127,10 @@ declare module 'vue' {
FileVideoIcon: typeof import('./src/components/Icons/FileVideoIcon.vue')['default']
Filter: typeof import('./src/components/Filter.vue')['default']
FilterIcon: typeof import('./src/components/Icons/FilterIcon.vue')['default']
ForecastingSettings: typeof import('./src/components/Settings/ForecastingSettings.vue')['default']
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
@ -142,7 +141,7 @@ declare module 'vue' {
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
HomeActions: typeof import('./src/components/Settings/HomeActions.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default']
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
@ -229,6 +228,7 @@ declare module 'vue' {
SmileIcon: typeof import('./src/components/Icons/SmileIcon.vue')['default']
SortBy: typeof import('./src/components/SortBy.vue')['default']
SortIcon: typeof import('./src/components/Icons/SortIcon.vue')['default']
SparkleIcon: typeof import('./src/components/Icons/SparkleIcon.vue')['default']
SquareAsterisk: typeof import('./src/components/Icons/SquareAsterisk.vue')['default']
StepsIcon: typeof import('./src/components/Icons/StepsIcon.vue')['default']
SuccessIcon: typeof import('./src/components/Icons/SuccessIcon.vue')['default']

View File

@ -13,7 +13,7 @@
"@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0",
"frappe-ui": "^0.1.197",
"frappe-ui": "^0.1.201",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"mime": "^4.0.1",

View File

@ -13,7 +13,7 @@
</div>
</div>
<div>
<div class="text-ink-gray-5">
<div class="text-ink-gray-5 text-base">
{{
__('Are you sure you want to delete {0} items?', [
props.items?.length,
@ -53,7 +53,7 @@
</div>
</div>
<div>
<div class="text-ink-gray-5">
<div class="text-ink-gray-5 text-base">
{{
confirmDeleteInfo.delete
? __(

View File

@ -110,7 +110,6 @@
</template>
<script setup>
import { TemplateOption } from '@/utils'
import {
Autocomplete,
Button,
@ -191,31 +190,17 @@ const dropdownOptions = computed(() => {
options.push({
label: __('Remove'),
component: (props) =>
TemplateOption({
option: __('Remove'),
icon: 'trash-2',
active: props.active,
variant: 'danger',
onClick: () => {
emit('remove')
},
}),
icon: 'trash-2',
variant: 'red',
onClick: () => emit('remove'),
condition: () => !props.isGroup,
})
options.push({
label: __('Remove group'),
component: (props) =>
TemplateOption({
option: __('Remove group'),
icon: 'trash-2',
active: props.active,
variant: 'danger',
onClick: () => {
emit('remove')
},
}),
icon: 'trash-2',
variant: 'red',
onClick: () => emit('remove'),
condition: () => props.isGroup,
})

View File

@ -1,5 +1,5 @@
<template>
<div class="rounded-lg border border-gray-300 p-3 flex flex-col gap-4 w-full">
<div class="rounded-lg border border-outline-gray-2 p-3 flex flex-col gap-4 w-full">
<template v-for="(condition, i) in props.conditions" :key="condition.field">
<CFCondition
v-if="Array.isArray(condition)"

View File

@ -1,7 +1,6 @@
<template>
<FileUploader
:file-types="image_type"
class="text-base"
@success="
(file) => {
$emit('upload', file.file_url)
@ -10,21 +9,28 @@
>
<template v-slot="{ progress, uploading, openFileSelector }">
<div class="flex items-end space-x-1">
<Button @click="openFileSelector">
{{
<Button
@click="openFileSelector"
:iconLeft="uploading ? 'cloud-upload' : ImageUpIcon"
:label="
uploading
? `Uploading ${progress}%`
? __('Uploading {0}%', [progress])
: image_url
? 'Change'
: 'Upload'
}}
</Button>
<Button v-if="image_url" @click="$emit('remove')">Remove</Button>
? __('Change')
: __('Upload')
"
/>
<Button
v-if="image_url"
:label="__('Remove')"
@click="$emit('remove')"
/>
</div>
</template>
</FileUploader>
</template>
<script setup>
import ImageUpIcon from '~icons/lucide/image-up'
import { FileUploader, Button } from 'frappe-ui'
const prop = defineProps({
@ -33,10 +39,6 @@ const prop = defineProps({
type: String,
default: 'image/*',
},
label: {
type: String,
default: '',
},
})
const emit = defineEmits(['upload', 'remove'])
</script>

View File

@ -2,7 +2,7 @@
<Dialog v-model="show" :options="{ size: 'xl' }">
<template #body v-if="!confirmDeleteInfo.show">
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-4 flex items-center justify-between">
<div class="mb-6 flex items-center justify-between">
<div>
<h3 class="text-2xl leading-6 text-ink-gray-9 font-semibold">
{{
@ -32,11 +32,12 @@
{
label: 'Document',
key: 'title',
width: '19rem',
},
{
label: 'Master',
key: 'reference_doctype',
width: '30%',
width: '12rem',
},
]"
@selectionsChanged="

View File

@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.37543 1.93494L4.21632 2.21494C4.35232 2.26027 4.44388 2.38738 4.44388 2.53138C4.44388 2.67538 4.35143 2.80249 4.21543 2.84783L3.37454 3.12783L3.09365 3.9696C3.04921 4.10472 2.92121 4.19716 2.7781 4.19716C2.63499 4.19716 2.50787 4.1056 2.46254 3.9696L2.18165 3.12783L1.34076 2.84783C1.20476 2.80249 1.11232 2.67538 1.11232 2.53138C1.11232 2.38738 1.20476 2.26027 1.34076 2.21494L2.18165 1.93494L2.46254 1.09316C2.55321 0.82116 3.00387 0.82116 3.09454 1.09316L3.37543 1.93494ZM8.44852 1.33394C8.3643 1.16325 8.19046 1.05518 8.00012 1.05518C7.80978 1.05518 7.63595 1.16325 7.55173 1.33394L5.67697 5.13368L1.48388 5.74214C1.29552 5.76947 1.13901 5.90137 1.08017 6.08238C1.02133 6.26339 1.07036 6.46211 1.20665 6.59497L4.24065 9.55281L3.52421 13.7284C3.49203 13.916 3.56913 14.1056 3.7231 14.2174C3.87706 14.3293 4.08119 14.3441 4.24966 14.2555L8.11188 12.2253C8.35631 12.0968 8.4503 11.7945 8.32181 11.5501C8.19333 11.3057 7.89102 11.2117 7.64659 11.3402L4.68114 12.899L5.2707 9.46284C5.29853 9.30065 5.24477 9.13514 5.12693 9.02027L2.63025 6.58626L6.08082 6.08555C6.24373 6.06191 6.38457 5.95959 6.45741 5.81196L8.00012 2.6852L9.54284 5.81196C9.61568 5.95959 9.75652 6.06191 9.91943 6.08555L13.37 6.58625L11.6235 8.2887C11.4258 8.48146 11.4218 8.79802 11.6145 8.99575C11.8073 9.19349 12.1239 9.19752 12.3216 9.00476L14.7936 6.59498C14.9299 6.46212 14.9789 6.2634 14.9201 6.08239C14.8612 5.90138 14.7047 5.76947 14.5164 5.74214L10.3233 5.13368L8.44852 1.33394ZM13.4744 11.9911L12.3517 11.6168L11.9775 10.4942C11.8557 10.1315 11.2557 10.1315 11.1339 10.4942L10.7597 11.6168L9.63702 11.9911C9.45569 12.0515 9.33302 12.2213 9.33302 12.4124C9.33302 12.6035 9.45569 12.7733 9.63702 12.8337L10.7597 13.2079L11.1339 14.3306C11.1944 14.5119 11.365 14.6346 11.5561 14.6346C11.7472 14.6346 11.917 14.5119 11.9784 14.3306L12.3526 13.2079L13.4752 12.8337C13.6566 12.7733 13.7792 12.6035 13.7792 12.4124C13.7792 12.2213 13.6566 12.0515 13.4752 11.9911H13.4744ZM13.3333 2.88883C13.3333 3.25702 13.0349 3.5555 12.6667 3.5555C12.2985 3.5555 12 3.25702 12 2.88883C12 2.52064 12.2985 2.22217 12.6667 2.22217C13.0349 2.22217 13.3333 2.52064 13.3333 2.88883Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -26,13 +26,14 @@
<ListRowItem
:item="item"
@click="listViewRef.toggleRow(row['reference_docname'])"
class="!w-full"
>
<template #default="{ label }">
<div
v-if="column.key === 'title'"
class="truncate text-base flex gap-2"
class="truncate text-base flex gap-2 w-full"
>
<span>
<span class="max-w-[90%] truncate">
{{ label }}
</span>
<FeatherIcon
@ -102,6 +103,7 @@ const listViewRef = ref(null)
const viewLinkedDoc = (doc) => {
let page = ''
let id = ''
let openDesk = false
switch (doc.reference_doctype) {
case 'CRM Lead':
page = 'leads'
@ -123,6 +125,11 @@ const viewLinkedDoc = (doc) => {
page = 'organizations'
id = doc.reference_docname
break
case 'CRM Notification':
page = 'crm-notification'
id = doc.reference_docname
openDesk = true
break
case 'FCRM Note':
page = 'notes'
id = `view?open=${doc.reference_docname}`
@ -130,7 +137,11 @@ const viewLinkedDoc = (doc) => {
default:
break
}
window.open(`/crm/${page}/${id}`)
let base = '/crm'
if (openDesk) {
base = '/app'
}
window.open(`${base}/${page}/${id}`)
}
const getDoctypeName = (doctype) => {

View File

@ -6,20 +6,26 @@
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Define who receives the tickets and how theyre distributed among agents.',
)
__('Choose how {0} are assigned among salespeople.', [documentType])
}}
</span>
</div>
<div class="mt-8 flex items-center justify-between gap-2">
<div>
<div class="text-base font-medium text-ink-gray-8">
{{ __('Ticket Routing') }}
{{
__('{0} Routing', [
assignmentRuleData.documentType == 'CRM Lead'
? __('Lead')
: __('Deal'),
])
}}
</div>
<div class="text-p-sm text-ink-gray-6 mt-1">
{{
__('Choose how tickets are distributed among selected assignees.')
__('Choose how {0} are assigned among the selected assignees.', [
documentType,
])
}}
</div>
</div>
@ -27,12 +33,12 @@
<Popover placement="bottom-end">
<template #target="{ togglePopover }">
<div
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 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 w-full dark:[color-scheme:dark] select-none min-w-40"
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-outline-gray-2 bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 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 w-full dark:[color-scheme:dark] select-none min-w-40"
@click="togglePopover()"
>
<div>
{{
ticketRoutingOptions.find(
documentRoutingOptions.find(
(option) => option.value == assignmentRuleData.rule,
)?.label
}}
@ -45,7 +51,7 @@
class="p-1 text-ink-gray-7 mt-1 w-48 bg-white shadow-xl rounded"
>
<div
v-for="option in ticketRoutingOptions"
v-for="option in documentRoutingOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-gray-50 text-sm flex items-center justify-between rounded"
@click="
@ -75,7 +81,7 @@
{{ __('Assignees') }}
</div>
<div class="text-p-sm text-ink-gray-6 mt-1">
{{ __('Choose who receives the tickets.') }}
{{ __('Select the assignees for {0}.', [documentType]) }}
</div>
</div>
<AssigneeSearch @addAssignee="validateAssignmentRule('users')" />
@ -119,8 +125,13 @@ const { getUser } = usersStore()
const assignmentRuleData = inject('assignmentRuleData')
const assignmentRuleErrors = inject('assignmentRuleErrors')
const validateAssignmentRule = inject('validateAssignmentRule')
const documentType = computed(() =>
assignmentRuleData.value.documentType == 'CRM Lead'
? __('leads')
: __('deals'),
)
const ticketRoutingOptions = [
const documentRoutingOptions = [
{
label: 'Auto-rotate',
value: 'Round Robin',

View File

@ -1,25 +1,25 @@
<template>
<div
class="grid grid-cols-11 items-center gap-4 cursor-pointer hover:bg-gray-50 rounded"
class="flex p-3 items-center justify-between cursor-pointer hover:bg-surface-menu-bar rounded"
>
<div class="w-full py-3 pl-2 col-span-7" @click="updateStep('view', data)">
<div class="w-7/12" @click="updateStep('view', data)">
<div class="text-base text-ink-gray-7 font-medium">{{ data.name }}</div>
<div
v-if="data.description && data.description.length > 0"
class="text-sm w-full text-ink-gray-5 mt-1 whitespace-nowrap overflow-ellipsis overflow-hidden"
class="text-p-base w-full text-ink-gray-5 mt-0.5 whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{{ data.description }}
</div>
</div>
<div class="col-span-2">
<div class="w-3/12">
<Select
class="w-max bg-transparent -ml-2 border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
class="w-max -ml-2 bg-transparent border-0 text-ink-gray-6 focus-visible:!ring-0 bg-none"
:options="priorityOptions"
v-model="data.priority"
@update:modelValue="onPriorityChange"
/>
</div>
<div class="flex justify-between items-center w-full pr-2 col-span-2">
<div class="flex justify-between items-center w-2/12">
<Switch
size="sm"
:modelValue="!data.disabled"
@ -72,7 +72,6 @@ import {
toast,
} from 'frappe-ui'
import { inject, ref } from 'vue'
import { TemplateOption } from '@/utils'
const assignmentRulesList = inject('assignmentRulesList')
const updateStep = inject('updateStep')
@ -128,29 +127,19 @@ const dropdownOptions = [
},
{
label: __('Delete'),
component: (props) =>
TemplateOption({
option: __('Delete'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopImmediatePropagation()
isConfirmingDelete.value = true
},
}),
icon: 'trash-2',
onClick: (e) => {
e.preventDefault()
e.stopImmediatePropagation()
isConfirmingDelete.value = true
},
condition: () => !isConfirmingDelete.value,
},
{
label: __('Confirm Delete'),
component: (props) =>
TemplateOption({
option: __('Confirm Delete'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
onClick: () => deleteAssignmentRule(),
}),
icon: 'trash-2',
theme: 'red',
onClick: () => deleteAssignmentRule(),
condition: () => isConfirmingDelete.value,
},
]

View File

@ -1,15 +1,9 @@
<template>
<div
v-if="getAssignmentRuleData.loading"
class="flex items-center h-full justify-center"
>
<LoadingIndicator class="w-4" />
</div>
<div
v-if="!getAssignmentRuleData.loading"
class="sticky top-0 z-10 bg-white pb-6 px-10 py-8"
class="flex flex-col h-full gap-6 px-6 py-8 text-ink-gray-8"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center justify-between px-2 w-full">
<div class="flex items-center gap-2">
<Button
variant="ghost"
@ -47,274 +41,282 @@
/>
</div>
</div>
</div>
<div v-if="!getAssignmentRuleData.loading" class="overflow-y-auto px-10 pb-8">
<div class="grid grid-cols-2 gap-5">
<div>
<FormControl
:type="'text'"
size="sm"
variant="subtle"
:placeholder="__('Name')"
:label="__('Name')"
v-model="assignmentRuleData.assignmentRuleName"
required
maxlength="50"
@change="validateAssignmentRule('assignmentRuleName')"
/>
<ErrorMessage
:message="assignmentRuleErrors.assignmentRuleName"
class="mt-2"
/>
</div>
<div class="flex flex-col gap-1.5">
<FormLabel :label="__('Priority')" />
<Popover>
<template #target="{ togglePopover }">
<div
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-[--surface-gray-2] bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 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 w-full dark:[color-scheme:dark] cursor-default"
@click="togglePopover()"
>
<div>
{{
priorityOptions.find(
(option) => option.value == assignmentRuleData.priority,
)?.label
}}
</div>
<FeatherIcon name="chevron-down" class="size-4" />
</div>
</template>
<template #body="{ togglePopover }">
<div
class="p-1 text-ink-gray-6 top-1 absolute w-full bg-white shadow-2xl rounded"
>
<div
v-for="option in priorityOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-gray-50 text-base flex items-center justify-between rounded"
@click="
() => {
assignmentRuleData.priority = option.value
togglePopover()
}
"
>
{{ option.label }}
<FeatherIcon
v-if="assignmentRuleData.priority == option.value"
name="check"
class="size-4"
/>
</div>
</div>
</template>
</Popover>
</div>
<div>
<FormControl
:type="'textarea'"
size="sm"
variant="subtle"
:placeholder="__('Description')"
:label="__('Description')"
required
maxlength="250"
@change="validateAssignmentRule('description')"
v-model="assignmentRuleData.description"
/>
<ErrorMessage
:message="assignmentRuleErrors.description"
class="mt-2"
/>
</div>
<div class="flex flex-col gap-1.5">
<FormLabel :label="__('Apply on')" />
<Select
:options="[
{
label: 'Lead',
value: 'CRM Lead',
},
{
label: 'Deal',
value: 'CRM Deal',
},
]"
v-model="assignmentRuleData.documentType"
/>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Assignment condition')
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__('Choose which tickets are affected by this assignment rule.')
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/crm/assignment-rule"
target="_blank"
>{{ __('Learn about conditions') }}</a
>
</span>
<div v-if="isOldSla && step.data">
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
>
<span>{{ __('Old Condition') }}</span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.assignCondition }}</code>
</div>
</template>
</Popover>
</div>
</div>
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
v-if="!useNewUI && assignmentRuleData.assignCondition"
>
<span class="text-p-sm">
{{ __('Conditions for this rule were created from') }}
<a :href="deskUrl" target="_blank" class="underline">{{
__('desk')
}}</a>
{{
__(
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
)
}}
</span>
<Button
:label="__('I understand, add conditions')"
<div class="overflow-y-auto px-2">
<div class="grid grid-cols-2 gap-5">
<div>
<FormControl
:type="'text'"
size="sm"
variant="subtle"
theme="gray"
@click="useNewUI = true"
:placeholder="__('Name')"
:label="__('Name')"
v-model="assignmentRuleData.assignmentRuleName"
required
maxlength="50"
@change="validateAssignmentRule('assignmentRuleName')"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.assignConditionJson"
name="assignCondition"
:errors="assignmentRuleErrors.assignConditionError"
:doctype="assignmentRuleData.documentType"
v-else
/>
<div class="flex justify-end">
<ErrorMessage
:message="assignmentRuleErrors.assignCondition"
:message="assignmentRuleErrors.assignmentRuleName"
class="mt-2"
/>
</div>
<div class="flex flex-col gap-1.5">
<FormLabel :label="__('Priority')" />
<Popover>
<template #target="{ togglePopover }">
<div
class="flex items-center justify-between text-base rounded h-7 py-1.5 pl-2 pr-2 border border-outline-gray-2 bg-surface-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-modals hover:bg-surface-gray-3 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 w-full dark:[color-scheme:dark] cursor-default"
@click="togglePopover()"
>
<div>
{{
priorityOptions.find(
(option) => option.value == assignmentRuleData.priority,
)?.label
}}
</div>
<FeatherIcon name="chevron-down" class="size-4" />
</div>
</template>
<template #body="{ togglePopover }">
<div
class="p-1 text-ink-gray-6 top-1 absolute w-full bg-white shadow-2xl rounded"
>
<div
v-for="option in priorityOptions"
:key="option.value"
class="p-2 cursor-pointer hover:bg-gray-50 text-base flex items-center justify-between rounded"
@click="
() => {
assignmentRuleData.priority = option.value
togglePopover()
}
"
>
{{ option.label }}
<FeatherIcon
v-if="assignmentRuleData.priority == option.value"
name="check"
class="size-4"
/>
</div>
</div>
</template>
</Popover>
</div>
<div>
<FormControl
:type="'textarea'"
size="sm"
variant="subtle"
:placeholder="__('Description')"
:label="__('Description')"
required
maxlength="250"
@change="validateAssignmentRule('description')"
v-model="assignmentRuleData.description"
/>
<ErrorMessage
:message="assignmentRuleErrors.description"
class="mt-2"
/>
</div>
<div class="flex flex-col gap-1.5">
<FormLabel :label="__('Apply on')" />
<Select
:options="[
{
label: 'Lead',
value: 'CRM Lead',
},
{
label: 'Deal',
value: 'CRM Deal',
},
]"
v-model="assignmentRuleData.documentType"
/>
</div>
</div>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Unassignment condition')
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Choose which tickets are affected by this un-assignment rule.',
)
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/crm/assignment-rule"
target="_blank"
>{{ __('Learn about conditions') }}</a
>
</span>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Assignment condition')
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__('Choose which {0} are affected by this assignment rule.', [
documentType,
])
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/crm/assignment-rule"
target="_blank"
>{{ __('Learn about conditions') }}</a
>
</span>
<div v-if="isOldSla && step.data">
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
>
<span>{{ __('Old Condition') }}</span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.assignCondition }}</code>
</div>
</template>
</Popover>
</div>
</div>
</div>
<div class="mt-5">
<div
v-if="isOldSla && step.data && assignmentRuleData.unassignCondition"
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-md p-3 py-4"
v-if="!useNewUI && assignmentRuleData.assignCondition"
>
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
>
<span> {{ __('Old Condition') }} </span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.unassignCondition }}</code>
</div>
</template>
</Popover>
<span class="text-p-sm">
{{ __('Conditions for this rule were created from') }}
<a :href="deskUrl" target="_blank" class="underline">{{
__('desk')
}}</a>
{{
__(
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
)
}}
</span>
<Button
:label="__('I understand, add conditions')"
variant="subtle"
theme="gray"
@click="useNewUI = true"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.assignConditionJson"
name="assignCondition"
:errors="assignmentRuleErrors.assignConditionError"
:doctype="assignmentRuleData.documentType"
v-else
/>
<div class="flex justify-end">
<ErrorMessage
:message="assignmentRuleErrors.assignCondition"
class="mt-2"
/>
</div>
</div>
</div>
<div class="mt-5">
<div
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-gray-300 rounded-md p-3 py-4"
v-if="!useNewUI && assignmentRuleData.unassignCondition"
>
<span class="text-p-sm">
{{ __('Conditions for this rule were created from') }}
<a :href="deskUrl" target="_blank" class="underline">{{
__('desk')
}}</a>
{{
__(
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
)
}}
</span>
<Button
:label="__('I understand, add conditions')"
variant="subtle"
theme="gray"
@click="useNewUI = true"
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Unassignment condition')
}}</span>
<div class="flex items-center justify-between gap-6">
<span class="text-p-sm text-ink-gray-6">
{{
__(
'Choose which {0} are affected by this un-assignment rule.',
[documentType],
)
}}
<a
class="font-medium underline"
href="https://docs.frappe.io/crm/assignment-rule"
target="_blank"
>{{ __('Learn about conditions') }}</a
>
</span>
<div
v-if="
isOldSla && step.data && assignmentRuleData.unassignCondition
"
>
<Popover trigger="hover" :hoverDelay="0.25" placement="top-end">
<template #target>
<div
class="text-sm text-ink-gray-6 flex gap-1 cursor-default text-nowrap items-center"
>
<span> {{ __('Old Condition') }} </span>
<FeatherIcon name="info" class="size-4" />
</div>
</template>
<template #body-main>
<div
class="text-sm text-ink-gray-6 p-2 bg-white rounded-md max-w-96 text-wrap whitespace-pre-wrap leading-5"
>
<code>{{ assignmentRuleData.unassignCondition }}</code>
</div>
</template>
</Popover>
</div>
</div>
</div>
<div class="mt-5">
<div
v-if="!useNewUI && assignmentRuleData.unassignCondition"
class="flex flex-col gap-3 items-center text-center text-ink-gray-7 text-sm mb-2 border border-outline-gray-2 rounded-md p-3 py-4"
>
<span class="text-p-sm">
{{ __('Conditions for this rule were created from') }}
<a :href="deskUrl" target="_blank" class="underline">
{{ __('desk') }}
</a>
{{
__(
'which are not compatible with this UI, you will need to recreate the conditions here if you want to manage and add new conditions from this UI.',
)
}}
</span>
<Button
:label="__('I understand, add conditions')"
variant="subtle"
theme="gray"
@click="useNewUI = true"
/>
</div>
<AssignmentRulesSection
v-else
:conditions="assignmentRuleData.unassignConditionJson"
name="unassignCondition"
:errors="assignmentRuleErrors.unassignConditionError"
:doctype="assignmentRuleData.documentType"
/>
</div>
<AssignmentRulesSection
:conditions="assignmentRuleData.unassignConditionJson"
name="unassignCondition"
:errors="assignmentRuleErrors.unassignConditionError"
:doctype="assignmentRuleData.documentType"
v-else
/>
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Assignment Schedule')
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__('Choose the days of the week when this rule should be active.')
}}
</span>
</div>
<div class="mt-6">
<AssignmentSchedule />
</div>
</div>
<hr class="my-8" />
<AssigneeRules />
</div>
<hr class="my-8" />
<div>
<div class="flex flex-col gap-1">
<span class="text-lg font-semibold text-ink-gray-8">{{
__('Assignment Schedule')
}}</span>
<span class="text-p-sm text-ink-gray-6">
{{
__('Choose the days of the week when this rule should be active.')
}}
</span>
</div>
<div class="mt-6">
<AssignmentSchedule />
</div>
</div>
<hr class="my-8" />
<AssigneeRules />
</div>
<div v-else class="flex items-center h-full justify-center">
<LoadingIndicator class="w-4" />
</div>
<ConfirmDialog
v-model="showConfirmDialog.show"
@ -340,7 +342,15 @@ import {
Switch,
toast,
} from 'frappe-ui'
import { onMounted, onUnmounted, ref, inject, watch, provide } from 'vue'
import {
onMounted,
onUnmounted,
ref,
inject,
watch,
provide,
computed,
} from 'vue'
import AssignmentRulesSection from './AssignmentRulesSection.vue'
import AssignmentSchedule from './AssignmentSchedule.vue'
import AssigneeRules from './AssigneeRules.vue'
@ -364,6 +374,11 @@ const showConfirmDialog = ref({
})
const useNewUI = ref(true)
const isOldSla = ref(false)
const documentType = computed(() =>
assignmentRuleData.value.documentType == 'CRM Lead'
? __('leads')
: __('deals'),
)
const deskUrl = `${window.location.origin}/app/assignment-rule/${step.value.data?.name}`
const defaultAssignmentDays = [

View File

@ -1,35 +1,39 @@
<template>
<div class="px-10 py-8 sticky top-0">
<div class="flex items-start justify-between">
<div class="flex flex-col gap-1">
<h1 class="text-lg font-semibold text-ink-gray-8">
<div class="flex h-full flex-col gap-6 p-6 text-ink-gray-8">
<!-- Header -->
<div class="flex justify-between px-2 pt-2">
<div class="flex flex-col gap-1 w-9/12">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Assignment rules') }}
</h1>
<p class="text-p-sm text-ink-gray-6 max-w-md">
</h2>
<p class="text-p-base text-ink-gray-6">
{{
__(
'Assignment Rules automatically route tickets to the right team members based on predefined conditions.',
'Assignment rules automatically assign lead/deal to the right sales user based on predefined conditions',
)
}}
</p>
</div>
<Button
:label="__('Create new')"
theme="gray"
variant="solid"
@click="goToNew()"
icon-left="plus"
/>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('New')"
icon-left="plus"
variant="solid"
@click="goToNew()"
/>
</div>
</div>
<!-- Assignment rules list -->
<div class="overflow-y-auto">
<AssignmentRulesList />
</div>
</div>
<div class="overflow-y-auto px-10 pb-8">
<AssignmentRulesList />
</div>
</template>
<script setup>
import { createResource } from 'frappe-ui'
import AssignmentRulesList from './AssignmentRulesList.vue'
import { createResource } from 'frappe-ui'
import { inject, provide } from 'vue'
const updateStep = inject('updateStep')

View File

@ -8,25 +8,27 @@
<div v-else>
<div
v-if="assignmentRulesList.data?.length === 0"
class="flex items-center justify-center rounded-md border border-gray-200 p-4"
class="flex items-center justify-center rounded-md border border-outline-gray-2 p-4"
>
<div class="text-sm text-ink-gray-7">
{{ __('No items in the list') }}
</div>
</div>
<div v-else>
<div class="grid grid-cols-11 items-center gap-4 text-sm text-gray-600">
<div class="col-span-7 ml-2">{{ __('Assignment rule') }}</div>
<div class="col-span-2">{{ __('Priority') }}</div>
<div class="col-span-2">{{ __('Enabled') }}</div>
<div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
<div class="w-7/12">{{ __('Assignment rule') }}</div>
<div class="w-3/12">{{ __('Priority') }}</div>
<div class="w-2/12">{{ __('Enabled') }}</div>
</div>
<hr class="mt-2 mx-2" />
<div
v-for="assignmentRule in assignmentRulesList.data"
:key="assignmentRule.name"
>
<AssignmentRuleListItem :data="assignmentRule" />
<hr class="mx-2" />
<div class="h-px border-t mx-4 border-outline-gray-modals" />
<div class="overflow-y-auto px-2">
<template
v-for="(assignmentRule, i) in assignmentRulesList.data"
:key="assignmentRule.name"
>
<AssignmentRuleListItem :data="assignmentRule" />
<hr v-if="assignmentRulesList.data.length !== i + 1" class="mx-2" />
</template>
</div>
</div>
</div>

View File

@ -8,7 +8,7 @@
/>
<div
v-if="props.conditions.length == 0"
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-gray-300 text-gray-600 rounded-md"
class="flex p-4 items-center cursor-pointer justify-center gap-2 text-sm border border-outline-gray-2 text-gray-600 rounded-md"
@click="
() => {
props.conditions.push(['', '', ''])

View File

@ -1,5 +1,5 @@
<template>
<div class="rounded-md border px-2 border-gray-300 text-sm">
<div class="rounded-md border px-2 border-outline-gray-2 text-sm">
<div
class="grid p-2 px-4 items-center"
style="grid-template-columns: 3fr 1fr"
@ -9,7 +9,7 @@
:key="column.key"
class="text-gray-600 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ column.label }}
{{ __(column.label) }}
</div>
</div>
<hr />
@ -24,10 +24,9 @@
</template>
<script setup>
import { ErrorMessage } from 'frappe-ui'
import { onMounted, ref } from 'vue'
import AssignmentScheduleItem from './AssignmentScheduleItem.vue'
import { inject } from 'vue'
import { ErrorMessage } from 'frappe-ui'
import { onMounted, ref, inject } from 'vue'
const assignmentRuleData = inject('assignmentRuleData')
const assignmentRuleErrors = inject('assignmentRuleErrors')

View File

@ -3,7 +3,7 @@
class="grid py-3.5 px-4 items-center"
style="grid-template-columns: 3fr 1fr"
>
<div class="text-ink-gray-7 font-medium">{{ data.day }}</div>
<div class="text-ink-gray-7 font-medium">{{ __(data.day) }}</div>
<div class="flex justify-start">
<Switch v-model="data.active" @update:model-value="toggleDay" />
</div>

View File

@ -1,27 +1,18 @@
<template>
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
<!-- Header -->
<div class="flex px-2 justify-between">
<div class="flex items-center gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Brand settings')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
class="text-xl !h-7 font-semibold hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5"
/>
<Badge
v-if="settings.isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<div class="flex justify-between px-2 text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Brand settings') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{ __('Configure your brand name, logo, and favicon') }}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!settings.isDirty"
:loading="settings.loading"
@ -36,35 +27,30 @@
<FormControl
type="text"
class="w-1/2"
size="md"
v-model="settings.doc.brand_name"
:label="__('Brand name')"
:placeholder="__('Enter brand name')"
/>
</div>
<!-- logo -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-8">
{{ __('Logo') }}
</span>
<div class="flex flex-1 gap-5">
<div class="flex items-center flex-1 gap-5">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
class="flex items-center justify-center rounded border border-outline-gray-modals size-20"
>
<img
:src="settings.doc?.brand_logo || '/assets/crm/images/logo.png'"
v-if="settings.doc?.brand_logo"
:src="settings.doc?.brand_logo"
alt="Logo"
class="size-8 rounded"
/>
<ImageIcon v-else class="size-5 text-ink-gray-4" />
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
image_type="image/ico"
:image_url="settings.doc?.brand_logo"
@upload="(url) => (settings.doc.brand_logo = url)"
@remove="() => (settings.doc.brand_logo = '')"
/>
<span class="text-p-sm text-ink-gray-6">
<div class="flex flex-1 flex-col gap-1">
<span class="text-base font-medium">{{ __('Brand logo') }}</span>
<span class="text-p-base text-ink-gray-6">
{{
__(
'Appears in the left sidebar. Recommended size is 32x32 px in PNG or SVG',
@ -72,33 +58,34 @@
}}
</span>
</div>
<div>
<ImageUploader
image_type="image/ico"
:image_url="settings.doc?.brand_logo"
@upload="(url) => (settings.doc.brand_logo = url)"
@remove="() => (settings.doc.brand_logo = '')"
/>
</div>
</div>
</div>
<!-- favicon -->
<div class="flex flex-col justify-between gap-4">
<span class="text-base font-semibold text-ink-gray-8">
{{ __('Favicon') }}
</span>
<div class="flex flex-1 gap-5">
<div class="flex items-center flex-1 gap-5">
<div
class="flex items-center justify-center rounded border border-outline-gray-modals px-10 py-2"
class="flex items-center justify-center rounded border border-outline-gray-modals size-20"
>
<img
:src="settings.doc?.favicon || '/assets/crm/images/logo.png'"
v-if="settings.doc?.favicon"
:src="settings.doc?.favicon"
alt="Favicon"
class="size-8 rounded"
/>
<ImageIcon v-else class="size-5 text-ink-gray-4" />
</div>
<div class="flex flex-1 flex-col gap-2">
<ImageUploader
label="Favicon"
image_type="image/ico"
:image_url="settings.doc?.favicon"
@upload="(url) => (settings.doc.favicon = url)"
@remove="() => (settings.doc.favicon = '')"
/>
<span class="text-p-sm text-ink-gray-6">
<div class="flex flex-1 flex-col gap-1">
<span class="text-base font-medium">{{ __('Favicon') }}</span>
<span class="text-p-base text-ink-gray-6">
{{
__(
'Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO',
@ -106,20 +93,25 @@
}}
</span>
</div>
<div>
<ImageUploader
image_type="image/ico"
:image_url="settings.doc?.favicon"
@upload="(url) => (settings.doc.favicon = url)"
@remove="() => (settings.doc.favicon = '')"
/>
</div>
</div>
</div>
</div>
<div v-if="errorMessage">
<ErrorMessage :message="__(errorMessage)" />
</div>
</div>
</template>
<script setup>
import ImageIcon from '~icons/lucide/image'
import ImageUploader from '@/components/Controls/ImageUploader.vue'
import { FormControl, ErrorMessage } from 'frappe-ui'
import { FormControl } from 'frappe-ui'
import { getSettings } from '@/stores/settings'
import { showSettings } from '@/composables/settings'
import { ref } from 'vue'
const { _settings: settings, setupBrand } = getSettings()
@ -131,7 +123,4 @@ function updateSettings() {
},
})
}
const emit = defineEmits(['updateStep'])
const errorMessage = ref('')
</script>

View File

@ -1,27 +1,20 @@
<template>
<div class="flex h-full flex-col gap-6 px-6 py-8 text-ink-gray-8">
<!-- Header -->
<div class="flex px-2 justify-between">
<div class="flex items-center gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Currency & Exchange rate provider')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
class="text-xl !h-7 font-semibold hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5"
/>
<Badge
v-if="settings.isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<div class="flex justify-between px-2 text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Currency & Exchange rate provider') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{
__('Configure the currency and exchange rate provider for your CRM')
}}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!settings.isDirty"
:loading="settings.loading"
@ -32,7 +25,7 @@
<!-- Fields -->
<div class="flex flex-1 flex-col overflow-y-auto">
<div class="flex items-center justify-between gap-8 p-3">
<div class="flex items-center justify-between gap-8 py-3 px-2">
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Currency') }}
@ -61,7 +54,7 @@
</div>
</div>
<div class="h-px border-t mx-2 border-outline-gray-modals" />
<div class="flex items-center justify-between gap-8 p-3">
<div class="flex items-center justify-between gap-8 py-3 px-2">
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Exchange rate provider') }}
@ -131,17 +124,15 @@
</div>
</template>
<script setup>
import { ErrorMessage, toast } from 'frappe-ui'
import { ErrorMessage, FormControl, toast } from 'frappe-ui'
import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global'
import { showSettings } from '@/composables/settings'
import { ref } from 'vue'
import FormControl from 'frappe-ui/src/components/FormControl/FormControl.vue'
const { _settings: settings } = getSettings()
const { $dialog } = globalStore()
const emit = defineEmits(['updateStep'])
const errorMessage = ref('')
function updateSettings() {

View File

@ -9,10 +9,14 @@
:label="__(template.name)"
size="md"
@click="() => emit('updateStep', 'template-list')"
class="text-xl !h-7 font-semibold hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5"
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
/>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<div class="flex item-center space-x-4 w-3/12 justify-end">
<div class="flex items-center space-x-2">
<Switch size="sm" v-model="template.enabled" />
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
</div>
<Button
:label="__('Update')"
icon-left="plus"
@ -26,13 +30,6 @@
<!-- Fields -->
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
<div
class="flex justify-between items-center cursor-pointer border-b py-3"
@click="() => (template.enabled = !template.enabled)"
>
<div class="text-base text-ink-gray-7">{{ __('Enabled') }}</div>
<Switch v-model="template.enabled" @click.stop />
</div>
<div class="flex sm:flex-row flex-col gap-4">
<div class="flex-1">
<FormControl

View File

@ -148,7 +148,6 @@
</div>
</template>
<script setup>
import { TemplateOption } from '@/utils'
import {
TextInput,
FormControl,
@ -223,43 +222,28 @@ function getDropdownOptions(template) {
let options = [
{
label: __('Duplicate'),
component: (props) =>
TemplateOption({
option: __('Duplicate'),
icon: 'copy',
active: props.active,
onClick: () => emit('updateStep', 'new-template', { ...template }),
}),
icon: 'copy',
onClick: () => emit('updateStep', 'new-template', { ...template }),
},
{
label: __('Delete'),
component: (props) =>
TemplateOption({
option: __('Delete'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmDelete.value = true
},
}),
icon: 'trash-2',
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmDelete.value = true
},
condition: () => !confirmDelete.value,
},
{
label: __('Confirm Delete'),
component: (props) =>
TemplateOption({
option: __('Confirm Delete'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
onClick: () => deleteTemplate(template),
}),
icon: 'trash-2',
theme: 'red',
onClick: () => deleteTemplate(template),
condition: () => confirmDelete.value,
},
]
return options.filter((option) => option.condition?.() || true)
return options
}
</script>

View File

@ -11,10 +11,14 @@
"
size="md"
@click="() => emit('updateStep', 'template-list')"
class="text-xl !h-7 font-semibold hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5"
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
/>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<div class="flex item-center space-x-4 w-3/12 justify-end">
<div class="flex items-center space-x-2">
<Switch size="sm" v-model="template.enabled" />
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
</div>
<Button
:label="templateData?.name ? __('Duplicate') : __('Create')"
icon-left="plus"
@ -26,13 +30,6 @@
<!-- Fields -->
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
<div
class="flex justify-between items-center cursor-pointer border-b py-3"
@click="() => (template.enabled = !template.enabled)"
>
<div class="text-base text-ink-gray-7">{{ __('Enabled') }}</div>
<Switch v-model="template.enabled" @click.stop />
</div>
<div class="flex sm:flex-row flex-col gap-4">
<div class="flex-1">
<FormControl

View File

@ -0,0 +1,93 @@
<template>
<div class="flex h-full flex-col gap-6 py-8 px-6 text-ink-gray-8">
<div class="flex flex-col gap-1 px-2">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Forecasting') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{
__(
'Configure forecasting feature to help predict sales performance and growth',
)
}}
</p>
</div>
<div class="flex-1 flex flex-col overflow-y-auto">
<div class="flex items-center justify-between py-3 px-2">
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Enable forecasting') }}
</div>
<div class="text-p-sm text-ink-gray-5 truncate">
{{
__(
'Makes "Expected Closure Date" and "Expected Deal Value" mandatory for deal value forecasting',
)
}}
</div>
</div>
<div>
<Switch
size="sm"
v-model="settings.doc.enable_forecasting"
@click.stop="toggleForecasting"
/>
</div>
</div>
<div class="h-px border-t mx-2 border-outline-gray-modals" />
<div class="flex items-center justify-between py-3 px-2">
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Auto update expected deal value') }}
</div>
<div class="text-p-sm text-ink-gray-5 truncate">
{{
__(
'Automatically update "Expected Deal Value" based on the total value of associated products in a deal',
)
}}
</div>
</div>
<div>
<Switch
size="sm"
v-model="settings.doc.auto_update_expected_deal_value"
@click.stop="autoUpdateExpectedDealValue"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { getSettings } from '@/stores/settings'
import { Switch, toast } from 'frappe-ui'
const { _settings: settings } = getSettings()
function toggleForecasting() {
settings.save.submit(null, {
onSuccess: () => {
toast.success(
settings.doc.enable_forecasting
? __('Forecasting enabled successfully')
: __('Forecasting disabled successfully'),
)
},
})
}
function autoUpdateExpectedDealValue() {
settings.save.submit(null, {
onSuccess: () => {
toast.success(
settings.doc.auto_update_expected_deal_value
? __('Auto update of expected deal value enabled')
: __('Auto update of expected deal value disabled'),
)
},
})
}
</script>

View File

@ -1,105 +0,0 @@
<template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('General') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{ __('Configure general settings for your CRM') }}
</p>
</div>
<div class="flex-1 flex flex-col overflow-y-auto">
<div
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
@click="toggleForecasting()"
>
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Enable forecasting') }}
</div>
<div class="text-p-sm text-ink-gray-5 truncate">
{{
__(
'Makes "Expected Closure Date" and "Expected Deal Value" mandatory for deal value forecasting',
)
}}
</div>
</div>
<div>
<Switch
size="sm"
v-model="settings.doc.enable_forecasting"
@click.stop="toggleForecasting(settings.doc.enable_forecasting)"
/>
</div>
</div>
<div class="h-px border-t mx-2 border-outline-gray-modals" />
<template v-for="(setting, i) in settingsList" :key="setting.name">
<li
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
@click="() => emit('updateStep', setting.name)"
>
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __(setting.label) }}
</div>
<div class="text-p-sm text-ink-gray-5 truncate">
{{ __(setting.description) }}
</div>
</div>
<div>
<FeatherIcon name="chevron-right" class="text-ink-gray-7 size-4" />
</div>
</li>
<div
v-if="settingsList.length !== i + 1"
class="h-px border-t mx-2 border-outline-gray-modals"
/>
</template>
</div>
</div>
</template>
<script setup>
import { getSettings } from '@/stores/settings'
import { Switch, toast } from 'frappe-ui'
const emit = defineEmits(['updateStep'])
const { _settings: settings } = getSettings()
const settingsList = [
{
name: 'currency-settings',
label: 'Currency & Exchange rate provider',
description:
'Configure the currency and exchange rate provider for your CRM',
},
{
name: 'brand-settings',
label: 'Brand settings',
description: 'Configure your brand name, logo and favicon',
},
{
name: 'home-actions',
label: 'Home actions',
description: 'Configure actions that appear on the home dropdown',
},
]
function toggleForecasting(value) {
settings.doc.enable_forecasting =
value !== undefined ? value : !settings.doc.enable_forecasting
settings.save.submit(null, {
onSuccess: () => {
toast.success(
settings.doc.enable_forecasting
? __('Forecasting enabled successfully')
: __('Forecasting disabled successfully'),
)
},
})
}
</script>

View File

@ -1,34 +0,0 @@
<template>
<component :is="getComponent(step)" :data="data" @updateStep="updateStep" />
</template>
<script setup>
import GeneralSettings from './GeneralSettings.vue'
import CurrencySettings from './CurrencySettings.vue'
import BrandSettings from './BrandSettings.vue'
import HomeActions from './HomeActions.vue'
import { ref } from 'vue'
const step = ref('general-settings')
const data = ref(null)
function updateStep(newStep, _data) {
step.value = newStep
data.value = _data
}
function getComponent(step) {
switch (step) {
case 'general-settings':
return GeneralSettings
case 'currency-settings':
return CurrencySettings
case 'brand-settings':
return BrandSettings
case 'home-actions':
return HomeActions
default:
return null
}
}
</script>

View File

@ -1,21 +1,18 @@
<template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
<!-- Header -->
<div class="flex justify-between">
<div class="flex gap-1 -ml-4 w-9/12">
<Button
variant="ghost"
icon-left="chevron-left"
:label="__('Home actions')"
size="md"
@click="() => emit('updateStep', 'general-settings')"
class="text-xl !h-7 font-semibold hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5"
/>
<div class="flex justify-between text-ink-gray-8">
<div class="flex flex-col gap-1">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Home actions') }}
</h2>
<p class="text-p-base text-ink-gray-6">
{{ __('Configure actions that appear on the home dropdown') }}
</p>
</div>
<div class="flex item-center space-x-2 w-3/12 justify-end">
<Button
:label="__('Update')"
icon-left="plus"
variant="solid"
:disabled="!document.isDirty"
:loading="document.loading"
@ -25,7 +22,7 @@
</div>
<!-- Fields -->
<div class="flex flex-1 flex-col gap-4 overflow-y-auto">
<div class="flex flex-1 flex-col overflow-y-auto">
<Grid
v-model="document.doc.dropdown_items"
doctype="CRM Dropdown Item"

View File

@ -1,6 +1,6 @@
<template>
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
<div class="flex justify-between">
<div class="flex h-full flex-col gap-6 py-8 px-6 text-ink-gray-8">
<div class="flex px-2 justify-between">
<div class="flex flex-col gap-1 w-9/12">
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
{{ __('Send invites to') }}
@ -23,26 +23,21 @@
/>
</div>
</div>
<div class="flex-1 flex flex-col gap-8 overflow-y-auto">
<div class="flex-1 flex flex-col px-2 gap-8 overflow-y-auto">
<div>
<label class="block text-xs text-ink-gray-5 mb-1.5">
{{ __('Invite by email') }}
</label>
<div
class="p-2 group bg-surface-gray-2 hover:bg-surface-gray-3 rounded"
>
<MultiSelectUserInput
class="flex-1"
inputClass="!bg-surface-gray-2 hover:!bg-surface-gray-3 group-hover:!bg-surface-gray-3"
:placeholder="__('john@doe.com')"
v-model="invitees"
:validate="validateEmail"
:error-message="
(value) => __('{0} is an invalid email address', [value])
"
:fetchUsers="false"
/>
</div>
<FormControl
type="textarea"
label="Invite by email"
placeholder="user1@example.com, user2@example.com, ..."
@input="updateInvitees($event.target.value)"
:debounce="100"
:disabled="inviteByEmail.loading"
:description="
__(
'You can invite multiple users by comma separating their email addresses',
)
"
/>
<div
v-if="userExistMessage || inviteeExistMessage"
class="text-xs text-ink-red-3 mt-1.5"
@ -100,15 +95,9 @@
</div>
</template>
<script setup>
import MultiSelectUserInput from '@/components/Controls/MultiSelectUserInput.vue'
import { validateEmail, convertArrayToString } from '@/utils'
import { usersStore } from '@/stores/users'
import {
createListResource,
createResource,
FormControl,
Tooltip,
} from 'frappe-ui'
import { createListResource, createResource, FormControl } from 'frappe-ui'
import { useOnboarding } from 'frappe-ui/frappe'
import { ref, computed } from 'vue'
@ -208,6 +197,15 @@ const pendingInvitations = createListResource({
doctype: 'CRM Invitation',
filters: { status: 'Pending' },
fields: ['name', 'email', 'role'],
pageLength: 999,
auto: true,
})
function updateInvitees(value) {
const emails = value
.split(',')
.map((email) => email.trim())
.filter((email) => validateEmail(email))
invitees.value = emails
}
</script>

View File

@ -7,31 +7,33 @@
>
<template #body>
<div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex flex-col p-2 w-52 shrink-0 bg-surface-gray-2">
<h1 class="px-2 pt-2 mb-3 text-lg font-semibold text-ink-gray-8">
<div class="flex flex-col p-1 w-52 shrink-0 bg-surface-gray-2">
<h1 class="px-3 pt-3 pb-2 text-lg font-semibold text-ink-gray-8">
{{ __('Settings') }}
</h1>
<div v-for="tab in tabs">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
<nav class="space-y-1">
<SidebarLink
v-for="i in tab.items"
:icon="i.icon"
:label="__(i.label)"
class="w-full"
:class="
activeTab?.label == i.label
? 'bg-surface-selected shadow-sm hover:bg-surface-selected'
: 'hover:bg-surface-gray-3'
"
@click="activeSettingsPage = i.label"
/>
</nav>
<div class="flex flex-col overflow-y-auto">
<template v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
class="py-[7px] px-2 my-1 flex cursor-pointer gap-1.5 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
<nav class="space-y-1 px-1">
<SidebarLink
v-for="i in tab.items"
:icon="i.icon"
:label="__(i.label)"
class="w-full"
:class="
activeTab?.label == i.label
? 'bg-surface-selected shadow-sm hover:bg-surface-selected'
: 'hover:bg-surface-gray-3'
"
@click="activeSettingsPage = i.label"
/>
</nav>
</template>
</div>
</div>
<div class="flex flex-col flex-1 overflow-y-auto bg-surface-modal">
@ -42,6 +44,9 @@
</Dialog>
</template>
<script setup>
import CircleDollarSignIcon from '~icons/lucide/circle-dollar-sign'
import TrendingUpDownIcon from '~icons/lucide/trending-up-down'
import SparkleIcon from '@/components/Icons/SparkleIcon.vue'
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
@ -49,11 +54,14 @@ import Email2Icon from '@/components/Icons/Email2Icon.vue'
import EmailTemplateIcon from '@/components/Icons/EmailTemplateIcon.vue'
import SettingsIcon2 from '@/components/Icons/SettingsIcon2.vue'
import Users from '@/components/Settings/Users.vue'
import GeneralSettingsPage from '@/components/Settings/General/GeneralSettingsPage.vue'
import InviteUserPage from '@/components/Settings/InviteUserPage.vue'
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
import BrandSettings from '@/components/Settings/BrandSettings.vue'
import HomeActions from '@/components/Settings/HomeActions.vue'
import ForecastingSettings from '@/components/Settings/ForecastingSettings.vue'
import CurrencySettings from '@/components/Settings/CurrencySettings.vue'
import EmailTemplatePage from '@/components/Settings/EmailTemplate/EmailTemplatePage.vue'
import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
import EmailConfig from '@/components/Settings/EmailConfig.vue'
@ -76,7 +84,7 @@ const user = computed(() => getUser() || {})
const tabs = computed(() => {
let _tabs = [
{
label: __('Settings'),
label: __('Personal Settings'),
hideLabel: true,
items: [
{
@ -89,12 +97,32 @@ const tabs = computed(() => {
}),
component: markRaw(ProfileSettings),
},
],
},
{
label: __('System Configuration'),
items: [
{
label: __('General'),
icon: 'settings',
component: markRaw(GeneralSettingsPage),
condition: () => isManager(),
label: __('Forecasting'),
component: markRaw(ForecastingSettings),
icon: TrendingUpDownIcon,
},
{
label: __('Currency & Exchange Rate'),
icon: CircleDollarSignIcon,
component: markRaw(CurrencySettings),
},
{
label: __('Brand Settings'),
icon: SparkleIcon,
component: markRaw(BrandSettings),
},
],
condition: () => isManager(),
},
{
label: __('User Management'),
items: [
{
label: __('Users'),
icon: 'user',
@ -107,6 +135,12 @@ const tabs = computed(() => {
component: markRaw(InviteUserPage),
condition: () => isManager(),
},
],
condition: () => isManager(),
},
{
label: __('Email Settings'),
items: [
{
label: __('Email Accounts'),
icon: Email2Icon,
@ -118,6 +152,11 @@ const tabs = computed(() => {
icon: EmailTemplateIcon,
component: markRaw(EmailTemplatePage),
},
],
},
{
label: __('Automation & Rules'),
items: [
{
label: __('Assignment rules'),
icon: markRaw(h(SettingsIcon2, { class: 'rotate-90' })),
@ -125,6 +164,17 @@ const tabs = computed(() => {
},
],
},
{
label: __('Customization'),
items: [
{
label: __('Home Actions'),
component: markRaw(HomeActions),
icon: 'home',
},
],
condition: () => isManager(),
},
{
label: __('Integrations', null, 'FCRM'),
items: [

View File

@ -169,8 +169,16 @@
import AddExistingUserModal from '@/components/Modals/AddExistingUserModal.vue'
import { activeSettingsPage } from '@/composables/settings'
import { usersStore } from '@/stores/users'
import { TemplateOption, DropdownOption } from '@/utils'
import { Avatar, TextInput, toast, call, FeatherIcon, Tooltip } from 'frappe-ui'
import { DropdownOption } from '@/utils'
import {
Dropdown,
Avatar,
TextInput,
toast,
call,
FeatherIcon,
Tooltip,
} from 'frappe-ui'
import { ref, computed, onMounted } from 'vue'
const { users, isAdmin, isManager } = usersStore()
@ -208,29 +216,19 @@ function getMoreOptions(user) {
let options = [
{
label: __('Remove'),
component: (props) =>
TemplateOption({
option: __('Remove'),
icon: 'trash-2',
active: props.active,
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmRemove.value = true
},
}),
icon: 'trash-2',
onClick: (e) => {
e.preventDefault()
e.stopPropagation()
confirmRemove.value = true
},
condition: () => !confirmRemove.value,
},
{
label: __('Confirm Remove'),
component: (props) =>
TemplateOption({
option: __('Confirm Remove'),
icon: 'trash-2',
active: props.active,
theme: 'danger',
onClick: () => removeUser(user, true),
}),
icon: 'trash-2',
theme: 'red',
onClick: () => removeUser(user, true),
condition: () => confirmRemove.value,
},
]
@ -242,38 +240,35 @@ function getDropdownOptions(user) {
let options = [
{
label: __('Admin'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Admin'),
icon: 'shield',
active: props.active,
selected: user.role === 'System Manager',
onClick: () => updateRole(user, 'System Manager'),
}),
onClick: () => updateRole(user, 'System Manager'),
condition: () => isAdmin(),
},
{
label: __('Manager'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Manager'),
icon: 'briefcase',
active: props.active,
selected: user.role === 'Sales Manager',
onClick: () => updateRole(user, 'Sales Manager'),
}),
onClick: () => updateRole(user, 'Sales Manager'),
condition: () => isManager(),
},
{
label: __('Sales User'),
component: (props) =>
component: () =>
DropdownOption({
option: __('Sales User'),
icon: 'user-check',
active: props.active,
selected: user.role === 'Sales User',
onClick: () => updateRole(user, 'Sales User'),
}),
onClick: () => updateRole(user, 'Sales User'),
},
]

View File

@ -1,7 +1,8 @@
import { getScript } from '@/data/script'
import { globalStore } from '@/stores/global'
import { getMeta } from '@/stores/meta'
import { showSettings, activeSettingsPage } from '@/composables/settings'
import { runSequentially, parseAssignees } from '@/utils'
import { runSequentially, parseAssignees, evaluateExpression } from '@/utils'
import { createDocumentResource, createResource, toast } from 'frappe-ui'
import { ref, reactive } from 'vue'
@ -11,6 +12,7 @@ const assigneesCache = {}
export function useDocument(doctype, docname) {
const { setupScript, scripts } = getScript(doctype)
const meta = getMeta(doctype)
documentsCache[doctype] = documentsCache[doctype] || {}
@ -37,6 +39,7 @@ export function useDocument(doctype, docname) {
}
},
setValue: {
validate,
onSuccess: () => {
triggerOnSave()
toast.success(__('Document updated successfully'))
@ -152,6 +155,42 @@ export function useDocument(doctype, docname) {
return []
}
function validate(d) {
checkMandatory(d.doc || d.fieldname)
}
function checkMandatory(doc) {
let fields = meta?.getFields() || []
if (!fields || fields.length === 0) return
let missingFields = []
fields.forEach((df) => {
let parent = meta?.doctypeMeta?.[df.parent] || null
if (evaluateExpression(df.mandatory_depends_on, doc, parent)) {
const value = doc[df.fieldname]
if (
value === undefined ||
value === null ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0)
) {
missingFields.push(df.label || df.fieldname)
}
}
})
if (missingFields.length > 0) {
toast.error(
__('Mandatory fields required: {0}', [missingFields.join(', ')]),
)
throw new Error(
__('Mandatory fields required: {0}', [missingFields.join(', ')]),
)
}
}
async function triggerOnLoad() {
const handler = async function () {
await (this.onLoad?.() || this.on_load?.() || this.onload?.())
@ -280,6 +319,7 @@ export function useDocument(doctype, docname) {
assignees: assigneesCache[doctype][docname || ''],
scripts,
error,
validate,
getControllers,
triggerOnLoad,
triggerOnBeforeCreate,

View File

@ -421,6 +421,36 @@ export function evaluateDependsOnValue(expression, doc) {
return out
}
export function evaluateExpression(expression, doc, parent) {
if (!expression) return false
if (!doc) return false
let out = null
if (typeof expression === 'boolean') {
out = expression
} else if (typeof expression === 'function') {
out = expression(doc)
} else if (expression.substr(0, 5) == 'eval:') {
try {
out = _eval(expression.substr(5), { doc, parent })
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true
}
} catch (e) {
out = true
}
} else {
let value = doc[expression]
if (Array.isArray(value)) {
out = !!value.length
} else {
out = !!value
}
}
return out
}
export function convertSize(size) {
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let unitIndex = 0
@ -462,23 +492,12 @@ export function runSequentially(functions) {
}, Promise.resolve())
}
export function DropdownOption({
active,
option,
theme,
icon,
onClick,
selected,
}) {
export function DropdownOption({ option, icon, selected }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2' : 'text-ink-gray-8',
'group flex w-full justify-between items-center rounded-md px-2 py-2 text-sm',
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
],
onClick: !selected ? onClick : null,
class:
'group flex w-full text-ink-gray-8 justify-between items-center rounded-md px-2 py-2 text-sm hover:bg-surface-gray-2',
},
[
h('div', { class: 'flex gap-2' }, [
@ -501,30 +520,6 @@ export function DropdownOption({
)
}
export function TemplateOption({ active, option, theme, icon, onClick }) {
return h(
'button',
{
class: [
active ? 'bg-surface-gray-2 text-ink-gray-8' : 'text-ink-gray-7',
'group flex w-full gap-2 items-center rounded-md px-2 py-2 text-sm',
theme == 'danger' ? 'text-ink-red-3 hover:bg-ink-red-1' : '',
],
onClick: onClick,
},
[
icon
? h(FeatherIcon, {
name: icon,
class: ['h-4 w-4 shrink-0'],
'aria-hidden': true,
})
: null,
h('span', { class: 'whitespace-nowrap' }, option),
],
)
}
export function copy(obj) {
if (!obj) return obj
return JSON.parse(JSON.stringify(obj))

View File

@ -2,137 +2,122 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import path from 'path'
import fs from 'fs'
import frappeui from 'frappe-ui/vite'
import { VitePWA } from 'vite-plugin-pwa'
function appPath(app) {
const root = path.resolve(__dirname, '../..') // points to apps
const frontendPaths = [
// Standard frontend structure: appname/frontend/src
path.join(root, app, 'frontend', 'src'),
// Desk-based apps: appname/desk/src
path.join(root, app, 'desk', 'src'),
// Alternative frontend structures
path.join(root, app, 'client', 'src'),
path.join(root, app, 'ui', 'src'),
// Direct src structure: appname/src
path.join(root, app, 'src'),
]
return frontendPaths.find((srcPath) => fs.existsSync(srcPath)) || null
}
function hasApp(app) {
return fs.existsSync(appPath(app))
}
// List of frontend apps used in this project
let apps = []
const alias = [
// Default "@" for this app
{
find: '@',
replacement: path.resolve(__dirname, 'src'),
},
// App-specific aliases like @helpdesk, @hrms, etc.
...apps.map((app) =>
hasApp(app)
? { find: `@${app}`, replacement: appPath(app) }
: { find: `@${app}`, replacement: `virtual:${app}` },
),
]
const defineFlags = Object.fromEntries(
apps.map((app) => [
`__HAS_${app.toUpperCase()}__`,
JSON.stringify(hasApp(app)),
]),
)
const virtualStubPlugin = {
name: 'virtual-empty-modules',
resolveId(id) {
if (id.startsWith('virtual:')) return '\0' + id
},
load(id) {
if (id.startsWith('\0virtual:')) {
return 'export default {}; export const missing = true;'
}
},
}
console.log('Generated app aliases:', alias)
// https://vitejs.dev/config/
export default defineConfig({
define: defineFlags,
plugins: [
frappeui({
frappeProxy: true,
lucideIcons: true,
jinjaBootData: true,
buildConfig: {
indexHtmlPath: '../crm/www/crm.html',
emptyOutDir: true,
sourcemap: true,
},
}),
vue(),
vueJsx(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
manifest: {
display: 'standalone',
name: 'Frappe CRM',
short_name: 'Frappe CRM',
start_url: '/crm',
description:
'Modern & 100% Open-source CRM tool to supercharge your sales operations',
icons: [
{
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable',
},
{
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
}),
virtualStubPlugin,
],
resolve: { alias },
optimizeDeps: {
include: [
'feather-icons',
'showdown',
'tailwind.config.js',
'prosemirror-state',
'prosemirror-view',
'lowlight',
'interactjs'
export default defineConfig(async ({ mode }) => {
const isDev = mode === 'development'
const frappeui = await importFrappeUIPlugin(isDev)
const config = {
plugins: [
frappeui({
frappeProxy: true,
lucideIcons: true,
jinjaBootData: true,
buildConfig: {
indexHtmlPath: '../crm/www/crm.html',
emptyOutDir: true,
sourcemap: true,
},
}),
vue(),
vueJsx(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
},
manifest: {
display: 'standalone',
name: 'Frappe CRM',
short_name: 'Frappe CRM',
start_url: '/crm',
description:
'Modern & 100% Open-source CRM tool to supercharge your sales operations',
icons: [
{
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable',
},
{
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/assets/crm/manifest/manifest-icon-512.maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
}),
],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
optimizeDeps: {
include: [
'feather-icons',
'showdown',
'tailwind.config.js',
'prosemirror-state',
'prosemirror-view',
'lowlight',
'interactjs',
],
},
}
// Add local frappe-ui alias only in development if the local frappe-ui exists
if (isDev) {
try {
// Check if the local frappe-ui directory exists
const fs = await import('node:fs')
const localFrappeUIPath = path.resolve(__dirname, '../frappe-ui')
if (fs.existsSync(localFrappeUIPath)) {
config.resolve.alias['frappe-ui'] = localFrappeUIPath
} else {
console.warn('Local frappe-ui directory not found, using npm package')
}
} catch (error) {
console.warn(
'Error checking for local frappe-ui, using npm package:',
error.message,
)
}
}
return config
})
async function importFrappeUIPlugin(isDev) {
if (isDev) {
try {
const module = await import('../frappe-ui/vite')
return module.default
} catch (error) {
console.warn(
'Local frappe-ui not found, falling back to npm package:',
error.message,
)
}
}
// Fall back to npm package if local import fails
const module = await import('frappe-ui/vite')
return module.default
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,10 @@
{
"private": true,
"type": "module",
"workspaces": ["frontend", "frappe-ui"],
"scripts": {
"postinstall": "cd frontend && yarn install",
"dev": "cd frontend && yarn dev",
"build": "cd frontend && yarn build",
"disable-workspaces": "sed -i '' 's/\"workspaces\"/\"aworkspaces\"/g' package.json",
"enable-workspaces": "sed -i '' 's/\"aworkspaces\"/\"workspaces\"/g' package.json && rm -rf node_modules ./frontend/node_modules/ frappe-ui/node_modules/ && yarn install",
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd ..",
"disable-workspaces-and-upgrade-frappeui": "yarn disable-workspaces && yarn upgrade-frappeui"
"upgrade-frappeui": "cd frontend && yarn add frappe-ui@latest && cd .."
}
}