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 @@ + + + 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" > + 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" > 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 @@