Skip to content
69 changes: 61 additions & 8 deletions desk/src/components/EmailEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@
</template>

<script setup lang="ts">


import {
AttachmentItem,
MultiSelectInput,
Expand Down Expand Up @@ -190,7 +192,8 @@ import {
toast,
} from "frappe-ui";
import { useOnboarding } from "frappe-ui/frappe";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";

import SavedReplyIcon from "./icons/SavedReplyIcon.vue";

const editorRef = ref(null);
Expand Down Expand Up @@ -242,8 +245,15 @@ const newEmail = useStorage<null | string>(
null
);
const { updateOnboardingStep } = useOnboarding("helpdesk");
const { isManager } = useAuthStore();

const auth = useAuthStore();
const getSignature = createResource({
url: "frappe.client.get_value",
params: {
doctype: "HD Agent",
filters: { user: auth.userId },
fieldname: "signature",
},
});
// Initialize typing composable
const { onUserType, cleanup } = useTyping(props.ticketId);

Expand Down Expand Up @@ -301,10 +311,6 @@ const sendMail = createResource({
onSuccess: () => {
resetState();
emit("submit");

if (isManager) {
updateOnboardingStep("reply_on_ticket");
}
},
debounce: 300,
});
Expand Down Expand Up @@ -345,7 +351,7 @@ async function removeAttachment(attachment) {
await removeAttachmentFromServer(attachment.name);
}

function addToReply(
async function addToReply(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be Async

body: string,
toEmails: string[],
ccEmails: string[],
Expand All @@ -354,6 +360,24 @@ function addToReply(
toEmailsClone.value = toEmails;
ccEmailsClone.value = ccEmails;
bccEmailsClone.value = bccEmails;

let signature = "";

try {
const res = await getSignature.fetch();
signature = res?.signature || "";
} catch (err) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to do this, we already fetch some user details, get it from there.


}

const content = signature
? `${body}<p><br/></p><p>${signature}</p>`
: body;

editorRef.value.editor
.chain()
.clearContent()
.insertContent(content)
const repliedMessage = `<p class="reply-to-content"><p><blockquote>${body}</blockquote>`;
editorRef.value.editor
.chain()
Expand Down Expand Up @@ -390,6 +414,35 @@ const editor = computed(() => {
return editorRef.value.editor;
});

watch(
() => props.editable,
async (isEditable) => {
if (isEditable) {
try {
const res = await getSignature.fetch();
const signature = String(res.signature || "").trim();


// Always set signature if box is empty or null
if (signature && (!newEmail.value || newEmail.value.trim() === "")) {
newEmail.value = `<p><br/></p><p>${signature.replace(/\n/g, "<br/>")}</p>`;

} else {
// Force clear localStorage and set signature
newEmail.value = null;
setTimeout(() => {
newEmail.value = `<p><br/></p><p>${signature.replace(/\n/g, "<br/>")}</p>`;

}, 100);
}
} catch (err) {

}
}
},
{ immediate: true }
);

defineExpose({
addToReply,
editor,
Expand Down
24 changes: 24 additions & 0 deletions desk/src/components/Settings/Profile/Profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@
maxlength="40"
v-model="profile.lastName"
/>


<FormControl
class="w-full"
type="select"
:label="__('Availability Status')"
:options="['Available', 'Away']"
v-model="profile.availabilityStatus"
/>
<FormControl
class="w-full md:col-span-2"
type="textarea"
:label="__('Signature')"
v-model="profile.signature"
/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be a TextEditor component




</div>
<div class="flex items-center justify-between mt-6">
<div class="flex flex-col gap-1">
Expand Down Expand Up @@ -176,6 +194,8 @@ const profile = ref({
userImage: auth.userImage,
firstName: auth.userFirstName,
lastName: auth.userLastName,
availabilityStatus: "Available",
signature: "",
});
const showChangePasswordModal = ref(false);
const language = ref(auth.language);
Expand Down Expand Up @@ -214,6 +234,8 @@ const agentData = createResource({
firstName: fullName[0],
lastName: fullName[1] || "",
userImage: data.user_image,
availabilityStatus: data.availability_status,
signature: data.signature,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is half baked

};
},
});
Expand All @@ -232,6 +254,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,
},
};
},
Expand Down
26 changes: 8 additions & 18 deletions desk/src/types/doctypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,22 @@ interface DocType {
idx?: number;
}

// Last updated: 2025-08-25 12:29:02.646874
// Last updated: 2026-02-02 17:22:11.094897
export interface HDTicketStatus extends DocType {
/** Color: Select */
color?:
| "Black"
| "Gray"
| "Blue"
| "Green"
| "Red"
| "Pink"
| "Orange"
| "Amber"
| "Yellow"
| "Cyan"
| "Teal"
| "Violet"
| "purple";
color?: 'Black' | 'Gray' | 'Blue' | 'Green' | 'Red' | 'Pink' | 'Orange' | 'Amber' | 'Yellow' | 'Cyan' | 'Teal' | 'Violet' | 'purple';
/** Label: Data */
label_agent: string;
/** Show end users a different view: Check */
different_view: 0 | 1;
/** Label (customer view): Data */
label_customer?: string;
/** Category: Select */
category: "Open" | "Paused" | "Resolved";
category: 'Open' | 'Paused' | 'Resolved';
/** Order: Int */
order?: number;
/** Enabled: Check */
enabled: 0 | 1;
parsed_color?: string;
}

// Last updated: 2026-01-20 15:18:57.195606
Expand Down Expand Up @@ -259,14 +245,18 @@ export interface HDServiceLevelAgreement extends DocType {
default_ticket_status?: string;
}

// Last updated: 2026-01-19 23:22:29.075052
// Last updated: 2026-02-09 22:08:22.055831
export interface HDAgent extends DocType {
/** User: Link (User) */
user: string;
/** Agent Name: Data */
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;
}
21 changes: 19 additions & 2 deletions helpdesk/helpdesk/doctype/hd_agent/hd_agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"user",
"agent_name",
"user_image",
"is_active"
"is_active",
"availability_status",
"signature"
],
"fields": [
{
Expand All @@ -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": "Small Text",
"label": "Signature"
Comment on lines 12 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that these are also shown in the "Profile" tab shown in "Settings Modal" both signature and status field

Copy link
Author

@Shindhu-Ramaswamy Shindhu-Ramaswamy Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Fixed in commit 910edf4 — added both fields to Profile.vue"

},
{
"fieldname": "user_image",
"fieldtype": "Attach Image",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ frappe.ui.form.on("HD Ticket", {
if (frm.is_new()) return;
frm.call("mark_seen");
},
});
});
4 changes: 4 additions & 0 deletions helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand Down
8 changes: 8 additions & 0 deletions helpdesk/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@
"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"
},

}


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",
Expand All @@ -78,6 +83,8 @@
"HD Saved Reply": "helpdesk.helpdesk.doctype.hd_saved_reply.hd_saved_reply.permission_query",
}



# DocType Class
# ---------------
# Override standard doctype classes
Expand All @@ -101,3 +108,4 @@

before_tests = "helpdesk.test_utils.before_tests"
auth_hooks = ["helpdesk.auth.authenticate"]

Empty file added helpdesk/overrides/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions helpdesk/overrides/assign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import frappe
from frappe import _


def validate_agent_availability(doc, method=None):
if not doc.allocated_to:
return

status = frappe.db.get_value(
"HD Agent",
{"user": doc.allocated_to},
"availability_status",
)

if status == "Away":
frappe.msgprint(
msg=_("This agent is marked as Away "),
title=_("Agent Unavailable"),
)
frappe.log_error("Ticket assigned to Away agent", "Agent Unavailable")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

I was still able to assign it to the the Agent who is unavailable,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check how assignment rule is being applied for reference





# removed user_query_condition