diff --git a/crm/api/__init__.py b/crm/api/__init__.py
index e7f86e1b..e0b98221 100644
--- a/crm/api/__init__.py
+++ b/crm/api/__init__.py
@@ -70,7 +70,7 @@ def check_app_permission():
roles = frappe.get_roles()
if any(
- role in ["System Manager", "Sales User", "Sales Manager", "Sales Master Manager"] for role in roles
+ role in ["System Manager", "Sales User", "Sales Manager"] for role in roles
):
return True
@@ -98,7 +98,11 @@ def accept_invitation(key: str | None = None):
@frappe.whitelist()
def invite_by_email(emails: str, role: str):
- frappe.only_for("Sales Manager")
+ frappe.only_for(["Sales Manager", "System Manager"])
+
+ if role not in ["System Manager", "Sales Manager", "Sales User"]:
+ frappe.throw("Cannot invite for this role")
+
if not emails:
return
email_string = validate_email_address(emails, throw=False)
@@ -108,7 +112,10 @@ def invite_by_email(emails: str, role: str):
existing_members = frappe.db.get_all("User", filters={"email": ["in", email_list]}, pluck="email")
existing_invites = frappe.db.get_all(
"CRM Invitation",
- filters={"email": ["in", email_list], "role": ["in", ["Sales Manager", "Sales User"]]},
+ filters={
+ "email": ["in", email_list],
+ "role": ["in", ["System Manager", "Sales Manager", "Sales User"]],
+ },
pluck="email",
)
diff --git a/crm/api/session.py b/crm/api/session.py
index 3c12947c..add14c37 100644
--- a/crm/api/session.py
+++ b/crm/api/session.py
@@ -23,11 +23,35 @@ def get_users():
if frappe.session.user == user.name:
user.session_user = True
- user.is_manager = "Sales Manager" in frappe.get_roles(user.name) or user.name == "Administrator"
+ user.is_manager = "Sales Manager" in frappe.get_roles(user.name)
+ user.is_admin = user.name == "Administrator"
+
+ user.roles = frappe.get_roles(user.name)
+
+ user.role = ""
+
+ if "System Manager" in user.roles:
+ user.role = "System Manager"
+ elif "Sales Manager" in user.roles:
+ user.role = "Sales Manager"
+ elif "Sales User" in user.roles:
+ user.role = "Sales User"
+ elif "Guest" in user.roles:
+ user.role = "Guest"
+
+ if frappe.session.user == user.name:
+ user.session_user = True
user.is_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
- return users
+ crm_users = []
+
+ # crm users are users with role Sales User or Sales Manager
+ for user in users:
+ if "Sales User" in user.roles or "Sales Manager" in user.roles:
+ crm_users.append(user)
+
+ return users, crm_users
@frappe.whitelist()
diff --git a/crm/api/user.py b/crm/api/user.py
new file mode 100644
index 00000000..caae05f3
--- /dev/null
+++ b/crm/api/user.py
@@ -0,0 +1,84 @@
+import frappe
+
+
+@frappe.whitelist()
+def add_existing_users(users, role="Sales User"):
+ """
+ Add existing users to the CRM by assigning them a role (Sales User or Sales Manager).
+ :param users: List of user names to be added
+ """
+ frappe.only_for(["System Manager", "Sales Manager"])
+ users = frappe.parse_json(users)
+
+ for user in users:
+ add_user(user, role)
+
+
+@frappe.whitelist()
+def update_user_role(user, new_role):
+ """
+ Update the role of the user to Sales Manager, Sales User, or System Manager.
+ :param user: The name of the user
+ :param new_role: The new role to assign (Sales Manager or Sales User)
+ """
+
+ frappe.only_for(["System Manager", "Sales Manager"])
+
+ if new_role not in ["System Manager", "Sales Manager", "Sales User"]:
+ frappe.throw("Cannot assign this role")
+
+ user_doc = frappe.get_doc("User", user)
+
+ if new_role == "System Manager":
+ user_doc.append_roles("System Manager", "Sales Manager", "Sales User")
+ user_doc.set("block_modules", [])
+ if new_role == "Sales Manager":
+ user_doc.append_roles("Sales Manager", "Sales User")
+ user_doc.remove_roles("System Manager")
+ if new_role == "Sales User":
+ user_doc.append_roles("Sales User")
+ user_doc.remove_roles("Sales Manager", "System Manager")
+ update_module_in_user(user_doc, "FCRM")
+
+ user_doc.save(ignore_permissions=True)
+
+
+@frappe.whitelist()
+def add_user(user, role):
+ """
+ Add a user means adding role (Sales User or/and Sales Manager) to the user.
+ :param user: The name of the user to be added
+ :param role: The role to be assigned (Sales User or Sales Manager)
+ """
+ update_user_role(user, role)
+
+
+@frappe.whitelist()
+def remove_user(user):
+ """
+ Remove a user means removing Sales User & Sales Manager roles from the user.
+ :param user: The name of the user to be removed
+ """
+ frappe.only_for(["System Manager", "Sales Manager"])
+
+ user_doc = frappe.get_doc("User", user)
+ roles = [d.role for d in user_doc.roles]
+
+ if "Sales User" in roles:
+ user_doc.remove_roles("Sales User")
+ if "Sales Manager" in roles:
+ user_doc.remove_roles("Sales Manager")
+
+ user_doc.save(ignore_permissions=True)
+ frappe.msgprint(f"User {user} has been removed from CRM roles.")
+
+
+def update_module_in_user(user, module):
+ block_modules = frappe.get_all(
+ "Module Def",
+ fields=["name as module"],
+ filters={"name": ["!=", module]},
+ )
+
+ if block_modules:
+ user.set("block_modules", block_modules)
diff --git a/crm/fcrm/doctype/crm_invitation/crm_invitation.json b/crm/fcrm/doctype/crm_invitation/crm_invitation.json
index f5902d6e..826a2b33 100644
--- a/crm/fcrm/doctype/crm_invitation/crm_invitation.json
+++ b/crm/fcrm/doctype/crm_invitation/crm_invitation.json
@@ -27,7 +27,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Role",
- "options": "\nSales User\nSales Manager",
+ "options": "\nSales User\nSales Manager\nSystem Manager",
"reqd": 1
},
{
@@ -66,7 +66,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-09-03 14:59:29.450018",
+ "modified": "2025-06-17 17:20:18.935395",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Invitation",
@@ -106,7 +106,8 @@
"share": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/crm/fcrm/doctype/crm_invitation/crm_invitation.py b/crm/fcrm/doctype/crm_invitation/crm_invitation.py
index 625cd882..965bf4d7 100644
--- a/crm/fcrm/doctype/crm_invitation/crm_invitation.py
+++ b/crm/fcrm/doctype/crm_invitation/crm_invitation.py
@@ -35,7 +35,7 @@ class CRMInvitation(Document):
@frappe.whitelist()
def accept_invitation(self):
- frappe.only_for("System Manager")
+ frappe.only_for(["System Manager", "Sales Manager"])
self.accept()
def accept(self):
diff --git a/frappe-ui b/frappe-ui
index 8b615c0e..883bb643 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit 8b615c0e899d75b99c7d36ec6df97b5d0386b2ca
+Subproject commit 883bb643d1e662d6467925927e347dd28376960f
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index d7d6057c..6a8cbbaf 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -12,6 +12,7 @@ declare module 'vue' {
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.vue')['default']
+ AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
AllModals: typeof import('./src/components/Activities/AllModals.vue')['default']
@@ -38,6 +39,7 @@ declare module 'vue' {
CallUI: typeof import('./src/components/Telephony/CallUI.vue')['default']
CameraIcon: typeof import('./src/components/Icons/CameraIcon.vue')['default']
CertificateIcon: typeof import('./src/components/Icons/CertificateIcon.vue')['default']
+ ChangePasswordModal: typeof import('./src/components/Modals/ChangePasswordModal.vue')['default']
CheckCircleIcon: typeof import('./src/components/Icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/Icons/CheckIcon.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
@@ -136,10 +138,11 @@ declare module 'vue' {
InboxIcon: typeof import('./src/components/Icons/InboxIcon.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
- InviteMemberPage: typeof import('./src/components/Settings/InviteMemberPage.vue')['default']
+ InviteUserPage: typeof import('./src/components/Settings/InviteUserPage.vue')['default']
KanbanIcon: typeof import('./src/components/Icons/KanbanIcon.vue')['default']
KanbanSettings: typeof import('./src/components/Kanban/KanbanSettings.vue')['default']
KanbanView: typeof import('./src/components/Kanban/KanbanView.vue')['default']
+ KeyboardShortcut: typeof import('./src/components/KeyboardShortcut.vue')['default']
LayoutHeader: typeof import('./src/components/LayoutHeader.vue')['default']
LeadModal: typeof import('./src/components/Modals/LeadModal.vue')['default']
LeadsIcon: typeof import('./src/components/Icons/LeadsIcon.vue')['default']
@@ -151,7 +154,9 @@ declare module 'vue' {
ListIcon: typeof import('./src/components/Icons/ListIcon.vue')['default']
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
+ LucideInfo: typeof import('~icons/lucide/info')['default']
LucidePlus: typeof import('~icons/lucide/plus')['default']
+ LucideSearch: typeof import('~icons/lucide/search')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@@ -164,6 +169,7 @@ declare module 'vue' {
MultiActionButton: typeof import('./src/components/MultiActionButton.vue')['default']
MultipleAvatar: typeof import('./src/components/MultipleAvatar.vue')['default']
MultiSelectEmailInput: typeof import('./src/components/Controls/MultiSelectEmailInput.vue')['default']
+ MultiSelectUserInput: typeof import('./src/components/Controls/MultiSelectUserInput.vue')['default']
MuteIcon: typeof import('./src/components/Icons/MuteIcon.vue')['default']
NestedPopover: typeof import('./src/components/NestedPopover.vue')['default']
NoteArea: typeof import('./src/components/Activities/NoteArea.vue')['default']
@@ -228,6 +234,7 @@ declare module 'vue' {
UnpinIcon: typeof import('./src/components/Icons/UnpinIcon.vue')['default']
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
+ Users: typeof import('./src/components/Settings/Users.vue')['default']
ViewBreadcrumbs: typeof import('./src/components/ViewBreadcrumbs.vue')['default']
ViewControls: typeof import('./src/components/ViewControls.vue')['default']
ViewModal: typeof import('./src/components/Modals/ViewModal.vue')['default']
diff --git a/frontend/src/components/Activities/DataFields.vue b/frontend/src/components/Activities/DataFields.vue
index 5b97e80a..88119f80 100644
--- a/frontend/src/components/Activities/DataFields.vue
+++ b/frontend/src/components/Activities/DataFields.vue
@@ -31,7 +31,7 @@
{{ __('Loading...') }}
diff --git a/frontend/src/components/CommentBox.vue b/frontend/src/components/CommentBox.vue
index 3fcfc82c..86074750 100644
--- a/frontend/src/components/CommentBox.vue
+++ b/frontend/src/components/CommentBox.vue
@@ -149,7 +149,7 @@ function removeAttachment(attachment) {
const users = computed(() => {
return (
- usersList.data
+ usersList.data?.crmUsers
?.filter((user) => user.enabled)
.map((user) => ({
label: user.full_name.trimEnd(),
diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue
index dcfa2846..cce8b245 100644
--- a/frontend/src/components/Controls/Grid.vue
+++ b/frontend/src/components/Controls/Grid.vue
@@ -380,9 +380,9 @@ const props = defineProps({
},
})
-const triggerOnChange = inject('triggerOnChange')
-const triggerOnRowAdd = inject('triggerOnRowAdd')
-const triggerOnRowRemove = inject('triggerOnRowRemove')
+const triggerOnChange = inject('triggerOnChange', () => {})
+const triggerOnRowAdd = inject('triggerOnRowAdd', () => {})
+const triggerOnRowRemove = inject('triggerOnRowRemove', () => {})
const {
getGridViewSettings,
@@ -393,7 +393,7 @@ const {
getGridSettings,
} = getMeta(props.doctype)
getMeta(props.parentDoctype)
-const { getUser } = usersStore()
+const { users, getUser } = usersStore()
const rows = defineModel()
const parentDoc = defineModel('parent')
@@ -438,6 +438,14 @@ function getFieldObj(field) {
}
}
+ if (field.fieldtype === 'Link' && field.options === 'User') {
+ field.fieldtype = 'User'
+ field.link_filters = JSON.stringify({
+ ...(field.link_filters ? JSON.parse(field.link_filters) : {}),
+ name: ['in', users.data.crmUsers?.map((user) => user.name)],
+ })
+ }
+
return {
...field,
filters: field.link_filters && JSON.parse(field.link_filters),
diff --git a/frontend/src/components/Controls/MultiSelectEmailInput.vue b/frontend/src/components/Controls/MultiSelectEmailInput.vue
index 182ed938..133bfea4 100644
--- a/frontend/src/components/Controls/MultiSelectEmailInput.vue
+++ b/frontend/src/components/Controls/MultiSelectEmailInput.vue
@@ -70,7 +70,7 @@
{{
fetchContacts
? __('No results found')
- : __('Type an email address to add')
+ : __('Type an email address to invite')
}}
[],
+ },
})
const values = defineModel()
@@ -205,6 +209,14 @@ const filterOptions = createResource({
value: email,
}
})
+
+ // Filter out existing emails
+ if (props.existingEmails?.length) {
+ allData = allData.filter((option) => {
+ return !props.existingEmails.includes(option.value)
+ })
+ }
+
return allData
},
})
diff --git a/frontend/src/components/Controls/MultiSelectUserInput.vue b/frontend/src/components/Controls/MultiSelectUserInput.vue
new file mode 100644
index 00000000..3a554c2e
--- /dev/null
+++ b/frontend/src/components/Controls/MultiSelectUserInput.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+ {
+ query = e.target.value
+ showOptions = true
+ }
+ "
+ autocomplete="off"
+ @focus="() => togglePopover()"
+ @keydown.delete.capture.stop="removeLastValue"
+ />
+
+
+
+
+
+
+
+ {{
+ fetchUsers
+ ? __('No results found')
+ : __('Type an email address to invite')
+ }}
+
+
+
+
+
+
+ {{ option.label }}
+
+
+ {{ option.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ info }}
+
+
+
+
+
diff --git a/frontend/src/components/Controls/Password.vue b/frontend/src/components/Controls/Password.vue
index 6a636039..2346fc93 100644
--- a/frontend/src/components/Controls/Password.vue
+++ b/frontend/src/components/Controls/Password.vue
@@ -3,17 +3,47 @@
:type="show ? 'text' : 'password'"
:value="modelValue || value"
v-bind="$attrs"
+ @keydown.meta.i.prevent="show = !show"
+ @keydown.ctrl.i.prevent="show = !show"
>
+
+
+
-
+
+
+
+
+ {{ show ? __('Hide Password') : __('Show Password') }}
+
+ +I
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Layouts/AppSidebar.vue b/frontend/src/components/Layouts/AppSidebar.vue
index f7e87c4f..629ceb4f 100644
--- a/frontend/src/components/Layouts/AppSidebar.vue
+++ b/frontend/src/components/Layouts/AppSidebar.vue
@@ -172,6 +172,7 @@ import {
import { usersStore } from '@/stores/users'
import { sessionStore } from '@/stores/session'
import { showSettings, activeSettingsPage } from '@/composables/settings'
+import { showChangePasswordModal } from '@/composables/modals'
import { FeatherIcon, call } from 'frappe-ui'
import {
SignupBanner,
@@ -329,8 +330,7 @@ const steps = reactive([
completed: false,
onClick: () => {
minimize.value = true
- showSettings.value = true
- activeSettingsPage.value = 'Profile'
+ showChangePasswordModal.value = true
},
},
{
@@ -351,7 +351,7 @@ const steps = reactive([
onClick: () => {
minimize.value = true
showSettings.value = true
- activeSettingsPage.value = 'Invite Members'
+ activeSettingsPage.value = 'Invite User'
},
condition: () => isManager(),
},
@@ -529,7 +529,7 @@ const articles = ref([
{ name: 'profile', title: __('Profile') },
{ name: 'custom-branding', title: __('Custom branding') },
{ name: 'home-actions', title: __('Home actions') },
- { name: 'invite-members', title: __('Invite members') },
+ { name: 'invite-users', title: __('Invite users') },
],
},
{
diff --git a/frontend/src/components/Modals/AddExistingUserModal.vue b/frontend/src/components/Modals/AddExistingUserModal.vue
new file mode 100644
index 00000000..d11c2f61
--- /dev/null
+++ b/frontend/src/components/Modals/AddExistingUserModal.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
diff --git a/frontend/src/components/Modals/AssignmentModal.vue b/frontend/src/components/Modals/AssignmentModal.vue
index 05cea454..c6fc6dfd 100644
--- a/frontend/src/components/Modals/AssignmentModal.vue
+++ b/frontend/src/components/Modals/AssignmentModal.vue
@@ -33,6 +33,7 @@
doctype="User"
@change="(option) => addValue(option) && ($refs.input.value = '')"
:placeholder="__('John Doe')"
+ :filters="{ name: ['in', users.data.crmUsers?.map((user) => user.name)] }"
:hideMe="true"
>
@@ -105,7 +106,7 @@ const oldAssignees = ref([])
const error = ref('')
-const { getUser } = usersStore()
+const { users, getUser } = usersStore()
const removeValue = (value) => {
assignees.value = assignees.value.filter(
diff --git a/frontend/src/components/Modals/ChangePasswordModal.vue b/frontend/src/components/Modals/ChangePasswordModal.vue
new file mode 100644
index 00000000..d6c03399
--- /dev/null
+++ b/frontend/src/components/Modals/ChangePasswordModal.vue
@@ -0,0 +1,129 @@
+
+
+
+
diff --git a/frontend/src/components/Modals/GlobalModals.vue b/frontend/src/components/Modals/GlobalModals.vue
index 310b3d5d..d4328e6c 100644
--- a/frontend/src/components/Modals/GlobalModals.vue
+++ b/frontend/src/components/Modals/GlobalModals.vue
@@ -16,9 +16,14 @@
v-model="showAddressModal"
v-bind="addressProps"
/>
+
diff --git a/frontend/src/components/Modals/TaskModal.vue b/frontend/src/components/Modals/TaskModal.vue
index a172b51e..98c2cd71 100644
--- a/frontend/src/components/Modals/TaskModal.vue
+++ b/frontend/src/components/Modals/TaskModal.vue
@@ -1,23 +1,32 @@
-