diff --git a/desk/src/components/CommunicationArea.vue b/desk/src/components/CommunicationArea.vue index 0ef730e5e6..6dd4c2f971 100644 --- a/desk/src/components/CommunicationArea.vue +++ b/desk/src/components/CommunicationArea.vue @@ -245,4 +245,4 @@ onClickOutside( width: 100vw; } } - + \ No newline at end of file diff --git a/desk/src/components/EmailEditor.vue b/desk/src/components/EmailEditor.vue index c154e74f03..0b27bdf393 100644 --- a/desk/src/components/EmailEditor.vue +++ b/desk/src/components/EmailEditor.vue @@ -169,6 +169,8 @@ + \ No newline at end of file diff --git a/desk/src/components/Settings/Profile/Profile.vue b/desk/src/components/Settings/Profile/Profile.vue index dd3280f348..31f440101b 100644 --- a/desk/src/components/Settings/Profile/Profile.vue +++ b/desk/src/components/Settings/Profile/Profile.vue @@ -114,6 +114,36 @@ maxlength="40" v-model="profile.lastName" /> + + + + + +
+
+ {{ __("Signature") }} +
+ + +
+ + +
@@ -182,6 +212,7 @@ import { Dropdown, FileUploader, LoadingIndicator, + TextEditor, toast, } from "frappe-ui"; import { Autocomplete } from "@/components"; @@ -194,13 +225,17 @@ import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue"; import Link from "@/components/frappe-ui/Link.vue"; import { HDAgent } from "@/types/doctypes"; + const auth = useAuthStore(); const profile = ref({ fullName: auth.userName, userImage: auth.userImage, firstName: auth.userFirstName, lastName: auth.userLastName, + availabilityStatus: "Available", + signature: "", }); + const showChangePasswordModal = ref(false); const language = ref(auth.language); const timezone = ref(auth.timezone); @@ -215,11 +250,15 @@ const isTimezoneChanged = computed(() => { }); const isAccountInfoDirty = computed(() => { - const agentName = agentData.data?.agent_name?.split(" "); - if (!agentName) return false; + const data = agentData.data; + if (!data) return false; + const agentName = data.agent_name?.split(" ") || []; + const isDirty = - profile.value.firstName !== agentName[0] || - profile.value.lastName !== (agentName[1] || ""); + profile.value.firstName !== (agentName[0] || "") || + profile.value.lastName !== (agentName[1] || "") || + profile.value.availabilityStatus !== agentData.data?.availability_status || + (profile.value.signature?.trim() || "") !== (data.signature?.trim() || "") if (isDirty) { disableSettingModalOutsideClick.value = true; } else { @@ -244,7 +283,10 @@ const agentData = createResource({ firstName: fullName[0], lastName: fullName[1] || "", userImage: data.user_image, + availabilityStatus: data.availability_status, + signature: data.signature || "", }; + }, }); @@ -273,6 +315,8 @@ const setAgent = createResource({ fieldname: { agent_name: `${profile.value.firstName} ${profile.value.lastName}`, user_image: profile.value.userImage, + availability_status: profile.value.availabilityStatus, + signature: profile.value.signature || "", }, }; }, diff --git a/desk/src/stores/auth.ts b/desk/src/stores/auth.ts index df69620331..fa6bc71e6d 100644 --- a/desk/src/stores/auth.ts +++ b/desk/src/stores/auth.ts @@ -47,6 +47,11 @@ export const useAuthStore = defineStore("auth", () => { ); const language: ComputedRef = computed(() => user__.value.language); + const userSignature: ComputedRef = computed(() => { + return user__.value.signature || ""; +}); + + function sessionUser() { const cookies = new URLSearchParams(document.cookie.split("; ").join("&")); let _sessionUser = cookies.get("user_id"); @@ -93,6 +98,7 @@ export const useAuthStore = defineStore("auth", () => { timezone, userTeams, language, + userSignature, user, logout, }; diff --git a/desk/src/types/doctypes.ts b/desk/src/types/doctypes.ts index e8127f420e..06576e05cd 100644 --- a/desk/src/types/doctypes.ts +++ b/desk/src/types/doctypes.ts @@ -29,7 +29,6 @@ export interface HDTicketStatus extends DocType { order?: number; /** Enabled: Check */ enabled: 0 | 1; - parsed_color?: string; } // Last updated: 2026-01-20 15:18:57.195606 @@ -246,6 +245,7 @@ export interface HDServiceLevelAgreement extends DocType { default_ticket_status?: string; } +// Last updated: 2026-02-09 22:08:22.055831 // Last updated: 2026-02-02 11:15:47.402850 export interface HDAgent extends DocType { /** User: Link (User) */ @@ -254,6 +254,10 @@ export interface HDAgent extends DocType { agent_name: string; /** Is Active: Check */ is_active: 0 | 1; + /** Availability Status: Select */ + availability_status: 'Available' | 'Away'; + /** Signature: Small Text */ + signature?: string; /** Image: Attach Image */ user_image?: string; } diff --git a/helpdesk/api/auth.py b/helpdesk/api/auth.py index 45a31cc0b6..6959e091cf 100644 --- a/helpdesk/api/auth.py +++ b/helpdesk/api/auth.py @@ -23,7 +23,7 @@ def get_user(): fieldname=fields, as_dict=True, ) - + is_agent = _is_agent() is_admin = ("System Manager" or "Admistrator") in frappe.get_roles(current_user) has_desk_access = is_agent or is_admin @@ -38,7 +38,13 @@ def get_user(): language = user.language or frappe.db.get_single_value( "System Settings", "language" ) - + signature = frappe.db.get_value( + "HD Agent", + {"user": current_user}, + "signature" + ) or "" + frappe.logger().debug(f"Signature for {current_user}: {signature}") # 👈 add this + print(f"Signature for {current_user}: {signature}") # 👈 add this return { "has_desk_access": has_desk_access, "is_admin": is_admin, @@ -52,4 +58,7 @@ def get_user(): "time_zone": user.time_zone, "user_teams": user_team_names, "language": language, + "signature":signature , + } + diff --git a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json index b51d039761..971830417d 100644 --- a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json +++ b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json @@ -9,7 +9,9 @@ "user", "agent_name", "user_image", - "is_active" + "is_active", + "availability_status", + "signature" ], "fields": [ { @@ -34,6 +36,21 @@ "in_list_view": 1, "label": "Is Active" }, + { + "default": "Available", + "fieldname": "availability_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Availability Status", + "options": "Available\nAway", + "reqd": 1 + }, + { + "description": "automatically added in reply section", + "fieldname": "signature", + "fieldtype": "Text Editor", + "label": "Signature" + }, { "fieldname": "user_image", "fieldtype": "Attach Image", @@ -42,7 +59,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2026-02-02 11:15:47.402850", + "modified": "2026-02-09 22:08:22.055831", "modified_by": "Administrator", "module": "Helpdesk", "name": "HD Agent", diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.js b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.js index 13f1b14eeb..8893a19627 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.js +++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.js @@ -6,4 +6,4 @@ frappe.ui.form.on("HD Ticket", { if (frm.is_new()) return; frm.call("mark_seen"); }, -}); +}); \ No newline at end of file diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py index 23d7375a4c..65f3abc5c3 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py +++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py @@ -448,6 +448,8 @@ def assign_agent(self, agent: str): if frappe.session.user != agent: self.notify_agent(agent, "Assignment") + + def get_assigned_agents(self): assignees = get_assignees({"doctype": "HD Ticket", "name": self.name}) if len(assignees) > 0: @@ -576,6 +578,8 @@ def reply_via_agent( medium = "" if skip_email_workflow else "Email" subject = f"Re: {self.subject}" sender = frappe.session.user + + recipients = to or self.raised_by sender_email = None if skip_email_workflow else self.sender_email() diff --git a/helpdesk/hooks.py b/helpdesk/hooks.py index 2a45582aa3..3251e4cb64 100644 --- a/helpdesk/hooks.py +++ b/helpdesk/hooks.py @@ -66,8 +66,19 @@ "on_trash": "helpdesk.extends.assignment_rule.on_assignment_rule_trash", "validate": "helpdesk.extends.assignment_rule.on_assignment_rule_validate", }, + "ToDo": { + "before_insert": "helpdesk.overrides.assign.validate_agent_availability", + }, + "HD Ticket": { + "validate": "helpdesk.overrides.assign.validate_hd_ticket_agent", + }, + "ToDo": { + "before_insert": "helpdesk.overrides.assign.validate_agent_availability", + }, + } + has_permission = { "HD Ticket": "helpdesk.helpdesk.doctype.hd_ticket.hd_ticket.has_permission", "HD Saved Reply": "helpdesk.helpdesk.doctype.hd_saved_reply.hd_saved_reply.has_permission", @@ -78,6 +89,8 @@ "HD Saved Reply": "helpdesk.helpdesk.doctype.hd_saved_reply.hd_saved_reply.permission_query", } + + # DocType Class # --------------- # Override standard doctype classes @@ -101,3 +114,4 @@ before_tests = "helpdesk.test_utils.before_tests" auth_hooks = ["helpdesk.auth.authenticate"] + diff --git a/helpdesk/overrides/__init__.py b/helpdesk/overrides/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/helpdesk/overrides/assign.py b/helpdesk/overrides/assign.py new file mode 100644 index 0000000000..656a481d46 --- /dev/null +++ b/helpdesk/overrides/assign.py @@ -0,0 +1,46 @@ +import frappe +from frappe import _ + + +def validate_agent_availability(doc, method=None): + + if doc.reference_type != "HD Ticket": + return + + assigned_to = doc.allocated_to or doc.owner + + if not assigned_to: + return + + status = frappe.db.get_value( + "HD Agent", + {"user": assigned_to}, + "availability_status", + ) + + if status == "Away": + frappe.throw( + msg=_("This agent is marked as Away and cannot be assigned tickets."), + title=_("Agent Unavailable"), + exc=frappe.ValidationError, + ) + + +def validate_hd_ticket_agent(doc, method=None): + allocated_to = getattr(doc, "allocated_to", None) + + if not allocated_to: + return + + status = frappe.db.get_value( + "HD Agent", + {"user": doc.allocated_to}, + "availability_status", + ) + + if status == "Away": + frappe.throw( + msg=_("This agent is marked as Away and cannot be assigned tickets."), + title=_("Agent Unavailable"), + exc=frappe.ValidationError, + ) \ No newline at end of file