/);
+ const closing = new RegExp(/<\/div><\/div>$/);
+
+ return content.replace(opening, "").replace(closing, "");
+}
+
+/**
+ * Check if the content is empty. This is done by replacing all
+ * with empty strings.
+ */
+export function isEmpty(content: string) {
+ return isEmpty_(strip(content).trim());
+}
diff --git a/desk/src/utils/index.js b/desk/src/utils/index.js
index 27be1fae8..c1e9b829a 100644
--- a/desk/src/utils/index.js
+++ b/desk/src/utils/index.js
@@ -1,117 +1,103 @@
-import _dayjs from "dayjs"
-import relativeTime from "dayjs/esm/plugin/relativeTime"
-import updateLocale from "dayjs/esm/plugin/updateLocale"
+import _dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import updateLocale from "dayjs/plugin/updateLocale";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
-_dayjs.extend(relativeTime)
-_dayjs.extend(updateLocale)
-
-_dayjs.updateLocale("en", {
- relativeTime: {
- future: "%s",
- past: "%s ago",
- s: "now",
- m: "1 minute",
- mm: "%d minutes",
- h: "1 hour",
- hh: "%d hours",
- d: "1 day",
- dd: "%d days",
- M: "1 month",
- MM: "%d months",
- y: "1 year",
- yy: "%d years",
- },
-})
+_dayjs.extend(relativeTime);
+_dayjs.extend(updateLocale);
+_dayjs.extend(utc);
+_dayjs.extend(timezone);
_dayjs.longFormating = (s) => {
if (s === "now" || s === "now ago") {
- return "just now"
+ return "just now";
}
- return s
-}
+ return s;
+};
_dayjs.shortFormating = (s, ago = false) => {
if (s === "now" || s === "now ago") {
- return "now"
+ return "now";
}
- const prefix = s.split(" ")[0]
- const posfix = s.split(" ")[1]
- const isPast = s.includes("ago")
- let newPostfix = ""
+ const prefix = s.split(" ")[0];
+ const posfix = s.split(" ")[1];
+ const isPast = s.includes("ago");
+ let newPostfix = "";
switch (posfix) {
case "minute":
- newPostfix = "m"
- break
+ newPostfix = "m";
+ break;
case "minutes":
- newPostfix = "m"
- break
+ newPostfix = "m";
+ break;
case "hour":
- newPostfix = "h"
- break
+ newPostfix = "h";
+ break;
case "hours":
- newPostfix = "h"
- break
+ newPostfix = "h";
+ break;
case "day":
- newPostfix = "d"
- break
+ newPostfix = "d";
+ break;
case "days":
- newPostfix = "d"
- break
+ newPostfix = "d";
+ break;
case "month":
- newPostfix = "M"
- break
+ newPostfix = "M";
+ break;
case "months":
- newPostfix = "M"
- break
+ newPostfix = "M";
+ break;
case "year":
- newPostfix = "Y"
- break
+ newPostfix = "Y";
+ break;
case "years":
- newPostfix = "Y"
- break
+ newPostfix = "Y";
+ break;
}
- return `${prefix}${newPostfix}${isPast ? (ago ? " ago" : "") : ""}`
-}
+ return `${prefix}${newPostfix}${isPast ? (ago ? " ago" : "") : ""}`;
+};
export function remove_script_and_style(txt) {
- const evil_tags = ["script", "noscript", "title", "meta", "base", "head"]
+ const evil_tags = ["script", "noscript", "title", "meta", "base", "head"];
const regex = new RegExp(
evil_tags.map((tag) => `<${tag}>.*<\\/${tag}>`).join("|"),
"s"
- )
+ );
if (!regex.test(txt)) {
// no evil tags found, skip the DOM method entirely!
- return txt
+ return txt;
}
- var div = document.createElement("div")
- div.innerHTML = txt
- var found = false
+ var div = document.createElement("div");
+ div.innerHTML = txt;
+ var found = false;
evil_tags.forEach(function (e) {
- var elements = div.getElementsByTagName(e)
- i = elements.length
+ var elements = div.getElementsByTagName(e);
+ i = elements.length;
while (i--) {
- found = true
- elements[i].parentNode.removeChild(elements[i])
+ found = true;
+ elements[i].parentNode.removeChild(elements[i]);
}
- })
+ });
// remove links with rel="stylesheet"
- var elements = div.getElementsByTagName("link")
- var i = elements.length
+ var elements = div.getElementsByTagName("link");
+ var i = elements.length;
while (i--) {
if (elements[i].getAttribute("rel") == "stylesheet") {
- found = true
- elements[i].parentNode.removeChild(elements[i])
+ found = true;
+ elements[i].parentNode.removeChild(elements[i]);
}
}
if (found) {
- return div.innerHTML
+ return div.innerHTML;
} else {
// don't disturb
- return txt
+ return txt;
}
}
-export let dayjs = _dayjs
+export let dayjs = _dayjs;
diff --git a/desk/tailwind.config.js b/desk/tailwind.config.js
index 73fe0eec0..2f7fcb477 100644
--- a/desk/tailwind.config.js
+++ b/desk/tailwind.config.js
@@ -9,15 +9,27 @@ module.exports = {
],
theme: {
extend: {
- zIndex: {
- 5: "5",
- },
fontSize: {
xs: "12px",
sm: "13px",
base: "14px",
+ lg: "16px",
+ xl: "18px",
"2xl": "20px",
},
+ height: {
+ 18: "68px",
+ },
+ margin: {
+ 3.5: "14px",
+ },
+ padding: {
+ 2.5: "10px",
+ 3.5: "14px",
+ },
+ zIndex: {
+ 5: "5",
+ },
},
},
plugins: [
diff --git a/desk/tsconfig.json b/desk/tsconfig.json
index dc5b0b207..a1688f098 100644
--- a/desk/tsconfig.json
+++ b/desk/tsconfig.json
@@ -1,5 +1,7 @@
{
"compilerOptions": {
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
"target": "es2021",
"moduleResolution":"node",
"paths": {
diff --git a/desk/vite.config.js b/desk/vite.config.js
index b8624b8a1..40488ace1 100644
--- a/desk/vite.config.js
+++ b/desk/vite.config.js
@@ -24,6 +24,11 @@ export default defineConfig({
return r.toMinifiedString();
}),
+ logos: FileSystemIconLoader("./src/assets/logos", async (svg) => {
+ const r = new SVG(svg);
+ await cleanupSVG(r);
+ return r.toMinifiedString();
+ }),
},
}),
],
diff --git a/helpdesk/__init__.py b/helpdesk/__init__.py
index 732155f8d..fa3ddd8c5 100644
--- a/helpdesk/__init__.py
+++ b/helpdesk/__init__.py
@@ -1 +1 @@
-__version__ = "0.8.3"
+__version__ = "0.8.4"
diff --git a/helpdesk/api/auth.py b/helpdesk/api/auth.py
index c115a7536..f89b20f0d 100644
--- a/helpdesk/api/auth.py
+++ b/helpdesk/api/auth.py
@@ -9,6 +9,7 @@ def get_user():
is_admin = user.username == "administrator"
has_desk_access = is_agent or is_admin
user_image = user.user_image
+ user_first_name = user.first_name
user_name = user.full_name
user_id = user.name
username = user.username
@@ -19,6 +20,7 @@ def get_user():
"is_agent": is_agent,
"user_id": user_id,
"user_image": user_image,
+ "user_first_name": user_first_name,
"user_name": user_name,
"username": username,
}
diff --git a/helpdesk/api/config.py b/helpdesk/api/config.py
index 060773ccd..f71a28a57 100644
--- a/helpdesk/api/config.py
+++ b/helpdesk/api/config.py
@@ -1,14 +1,18 @@
import frappe
-@frappe.whitelist()
+@frappe.whitelist(allow_guest=True)
def get_config():
+ brand_logo = frappe.db.get_single_value("HD Settings", "brand_logo")
+ brand_favicon = frappe.db.get_single_value("HD Settings", "brand_favicon")
helpdesk_name = frappe.db.get_single_value("HD Settings", "helpdesk_name")
suppress_default_email_toast = frappe.db.get_single_value(
"HD Settings", "suppress_default_email_toast"
)
return {
+ "brand_logo": brand_logo,
+ "brand_favicon": brand_favicon,
"helpdesk_name": helpdesk_name,
"suppress_default_email_toast": suppress_default_email_toast,
}
diff --git a/helpdesk/api/dashboard.py b/helpdesk/api/dashboard.py
index 79a997c44..47faadf1b 100644
--- a/helpdesk/api/dashboard.py
+++ b/helpdesk/api/dashboard.py
@@ -1,124 +1,177 @@
+from datetime import datetime, timedelta
+
import frappe
-import datetime
@frappe.whitelist()
-def get_ticket_count(filters=None):
- ticket_count = frappe.db.get_list(
+def get_all():
+ return [
+ ticket_statuses(),
+ avg_first_response_time(),
+ ticket_types(),
+ new_tickets(),
+ resolution_within_sla(),
+ ticket_activity(),
+ ticket_priority(),
+ ]
+
+
+def ticket_statuses():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {"creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")]}
+
+ res = frappe.db.get_list(
"HD Ticket",
+ fields=["count(name) as value", "status as name"],
filters=filters,
- fields=["count(name) as count", "creation"],
- group_by="creation",
- order_by="creation asc",
+ group_by="status",
)
- return ticket_count
-
+ return {
+ "title": "Status",
+ "is_chart": True,
+ "chart_type": "Pie",
+ "data": res,
+ }
-@frappe.whitelist()
-def ticket_summary(startDate=None, endDate=None):
- if startDate and endDate:
- startDate = datetime.datetime.strptime(startDate, "%Y-%m-%d").strftime("%d-%m")
- endDate = datetime.datetime.strptime(endDate, "%Y-%m-%d").strftime("%d-%m")
- ticket_count = frappe.db.sql(
- """SELECT TO_CHAR(creation,'DD/MM') AS date,
- COUNT(Case when status="Open" then status end) as "Open",
- COUNT(Case when status="Closed" then status end) as "Closed",
- COUNT(Case when status="Replied" then status end) as "Replied"
- FROM `tabHD Ticket` WHERE TO_CHAR(creation,'DD/MM') BETWEEN %s AND %s group by
- TO_CHAR(creation,'DD/MM')
- ORDER BY TO_CHAR(creation,'MM/DD') ASC""",
- (startDate, endDate),
- )
- return ticket_count
+def avg_first_response_time():
+ average_resolution_time = float(0.0)
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {
+ "creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")],
+ "resolution_time": ["not like", ""],
+ }
-@frappe.whitelist()
-def ticket_status():
- ticket_count_by_status = frappe.db.get_list(
+ ticket_list = frappe.get_list(
"HD Ticket",
- fields=["count(name) as count", "status", "creation", "resolution_by"],
- group_by="status",
+ fields=["name", "resolution_time"],
+ filters=filters,
)
- return ticket_count_by_status
+ for ticket in ticket_list:
+ average_resolution_time += ticket.resolution_time
+
+ res = "Not enough data"
+ if ticket_list:
+ h = round((((average_resolution_time) / len(ticket_list)) / 3600), 1)
+ res = f"{h} Hours"
-@frappe.whitelist()
-def ticket_type(filters=None):
- ticket_count_by_type = frappe.db.get_list(
+ return {
+ "title": "Avg First Response Time",
+ "is_chart": False,
+ "data": res,
+ }
+
+
+def ticket_types():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {"creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")]}
+
+ res = frappe.db.get_list(
"HD Ticket",
+ fields=["count(name) as value", "ticket_type as name"],
filters=filters,
- fields=["count(name) as count", "ticket_type"],
group_by="ticket_type",
)
- return ticket_count_by_type
+ return {
+ "title": "Type",
+ "is_chart": True,
+ "chart_type": "Pie",
+ "data": res,
+ }
-@frappe.whitelist()
-def average_first_response_time():
- average_response_time = float(0.0)
- ticket_list = frappe.get_list(
+def new_tickets():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {"creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")]}
+
+ res = frappe.db.get_list(
"HD Ticket",
- fields=["name", "first_response_time"],
- filters={"first_response_time": ["not like", ""]},
+ fields=["COUNT(name) as value", "DATE_FORMAT(creation, '%d/%m/%Y') as name"],
+ filters=filters,
+ group_by="DATE(creation)",
+ order_by="DATE(creation)",
)
- if len(ticket_list) == 0:
- return 0.0
-
- for ticket in ticket_list:
- average_response_time += ticket.first_response_time
+ return {
+ "title": "New Tickets",
+ "is_chart": True,
+ "chart_type": "Line",
+ "data": res,
+ }
- return round((((average_response_time) / len(ticket_list)) / 3600), 1)
+def resolution_within_sla():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {
+ "creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")],
+ "status": "Closed",
+ }
-@frappe.whitelist()
-def average_resolution_time():
- average_resolution_time = float(0.0)
ticket_list = frappe.get_list(
"HD Ticket",
- fields=["name", "resolution_time"],
- filters={"resolution_time": ["not like", ""]},
+ filters=filters,
+ fields=["name", "agreement_status", "sla"],
)
- if len(ticket_list) == 0:
- return 0.0
+ count = 0
for ticket in ticket_list:
- average_resolution_time += ticket.resolution_time
+ if ticket.agreement_status == "Fulfilled":
+ count = count + 1
- return round((((average_resolution_time) / len(ticket_list)) / 3600), 1)
+ res = "0%"
+ if count:
+ resolution_within_sla_percentage = (count / len(ticket_list)) * 100
+ resolution_within_sla_percentage = round(resolution_within_sla_percentage, 1)
-@frappe.whitelist()
-def resolution_within_sla():
- ticket_list = frappe.get_list(
- "HD Ticket", filters={"status": "Closed"}, fields=["name", "agreement_status", "sla"],
- )
- count = 0
+ res = str(resolution_within_sla_percentage) + "%"
- if len(ticket_list) == 0:
- return 0.0
+ return {
+ "title": "Resolution Within SLA",
+ "is_chart": False,
+ "data": res,
+ }
- for ticket in ticket_list:
- if ticket.agreement_status == "Fulfilled":
- count = count + 1
- resolution_within_sla_percentage = (count / len(ticket_list)) * 100
- resolution_within_sla_percentage = round(resolution_within_sla_percentage, 1)
+def ticket_activity():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {"creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")]}
- return str(resolution_within_sla_percentage) + "%"
+ res = frappe.db.get_list(
+ "HD Ticket Activity",
+ fields=["COUNT(name) as value", "DATE_FORMAT(creation, '%d/%m/%Y') as name"],
+ filters=filters,
+ group_by="DATE(creation)",
+ order_by="DATE(creation)",
+ )
+ return {
+ "title": "Activity",
+ "is_chart": True,
+ "chart_type": "Line",
+ "data": res,
+ }
-@frappe.whitelist()
-def feedback_status(dateFilter=None):
- feedback_status_count = frappe.db.get_list(
+
+def ticket_priority():
+ thirty_days_ago = datetime.now() - timedelta(days=30)
+ filters = {"creation": [">=", thirty_days_ago.strftime("%Y-%m-%d")]}
+
+ res = frappe.db.get_list(
"HD Ticket",
- filters=[{"feedback_status": ["not like", ""]}, dateFilter],
- fields=["count(name) as value", "feedback_status as name"],
- group_by="feedback_status",
+ fields=["count(name) as value", "priority as name"],
+ filters=filters,
+ group_by="priority",
)
- return feedback_status_count
+ return {
+ "title": "Priority",
+ "is_chart": True,
+ "chart_type": "Pie",
+ "data": res,
+ }
diff --git a/helpdesk/api/ticket.py b/helpdesk/api/ticket.py
index 8ad1d8f59..15bb3186b 100644
--- a/helpdesk/api/ticket.py
+++ b/helpdesk/api/ticket.py
@@ -496,7 +496,7 @@ def activities(name):
activities = frappe.db.sql(
"""
SELECT action, creation, owner
- FROM `tabTicket Activity`
+ FROM `tabHD Ticket Activity`
WHERE ticket = %(ticket)s
ORDER BY creation DESC
""",
diff --git a/helpdesk/api/website.py b/helpdesk/api/website.py
deleted file mode 100644
index eaf0aa8f5..000000000
--- a/helpdesk/api/website.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import frappe
-
-
-@frappe.whitelist(allow_guest=True)
-def brand_html():
- settings_doc = frappe.get_doc("Website Settings")
- if settings_doc.brand_html:
- return settings_doc.brand_html
- else:
- if settings_doc.banner_image:
- return (
- f"
![]({settings_doc.banner_image})
"
- )
- else:
- return ""
-
-
-@frappe.whitelist(allow_guest=True)
-def navbar_items():
- return frappe.get_doc("Website Settings").top_bar_items
-
-
-@frappe.whitelist(allow_guest=True)
-def helpdesk_name():
- name = frappe.get_doc("HD Settings").helpdesk_name
- return name if name else ""
diff --git a/helpdesk/extends/client.py b/helpdesk/extends/client.py
index 147a869b0..67ce57a1c 100644
--- a/helpdesk/extends/client.py
+++ b/helpdesk/extends/client.py
@@ -56,10 +56,11 @@ def get_list_meta(
):
check_permissions(doctype, parent)
- query: Query = get_query(
+ query = get_query(
table=doctype,
filters=filters,
group_by=group_by,
+ fields=["name"],
)
query = apply_custom_filters(doctype, query)
diff --git a/helpdesk/extends/data_import.py b/helpdesk/extends/data_import.py
new file mode 100644
index 000000000..84734886a
--- /dev/null
+++ b/helpdesk/extends/data_import.py
@@ -0,0 +1,36 @@
+import frappe
+
+from frappe.handler import upload_file
+from frappe.model.document import Document
+
+
+@frappe.whitelist()
+def bulk_insert(
+ target_doctype: str, import_type: str = "Insert New Records"
+) -> Document:
+ """
+ Upload a file and initiate an import against a DocType. File can be of any
+ type supported by data import tool. File should be in a form with key `file`.
+
+ Caveats
+ - `doctype` can not be used as argument, since it is already used by `upload_file`
+ - `file` is not an explicit argument, but is required by `upload_file`
+
+ :param target_doctype: DocType against which import should be performed
+ :param import_type: An import type supported by data import tool
+ :return: Newly created `Data Import` document
+ """
+ file = upload_file()
+ data_import_doc = frappe.get_doc(
+ {
+ "doctype": "Data Import",
+ "reference_doctype": target_doctype,
+ "import_type": import_type,
+ "import_file": file.file_url,
+ }
+ )
+
+ data_import_doc.save()
+ data_import_doc.start_import()
+
+ return data_import_doc
diff --git a/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json b/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json
index 1fe6682db..536a455f2 100644
--- a/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json
+++ b/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json
@@ -26,6 +26,11 @@
"skip_email_workflow",
"instantly_send_email",
"column_break_aomm",
+ "branding_tab",
+ "images_column",
+ "brand_logo",
+ "brand_favicon",
+ "column_break_fsjn",
"misc_tab",
"toasts_column",
"suppress_default_email_toast",
@@ -192,11 +197,37 @@
"fieldname": "instantly_send_email",
"fieldtype": "Check",
"label": "Instantly send e-mail"
+ },
+ {
+ "fieldname": "branding_tab",
+ "fieldtype": "Tab Break",
+ "label": "Branding"
+ },
+ {
+ "fieldname": "images_column",
+ "fieldtype": "Column Break",
+ "label": "Images"
+ },
+ {
+ "description": "Image to be used in various places, including Login and Signup pages. An image with transparent background and 160 x 32 is preferred",
+ "fieldname": "brand_logo",
+ "fieldtype": "Attach Image",
+ "label": "Logo"
+ },
+ {
+ "fieldname": "column_break_fsjn",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Image to be used as website ",
+ "fieldname": "brand_favicon",
+ "fieldtype": "Attach Image",
+ "label": "Favicon"
}
],
"issingle": 1,
"links": [],
- "modified": "2023-04-14 04:03:58.330687",
+ "modified": "2023-05-14 00:34:20.077662",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Settings",
diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
index 0344c3fec..b541be565 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
@@ -16,6 +16,7 @@
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case, DocType, Order
from frappe.query_builder.functions import Count
+from frappe.realtime import get_website_room
from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds
from frappe.utils.user import is_website_user
@@ -156,6 +157,11 @@ def handle_ticket_activity_update(self):
self.name, f"{field_maps[field]} set to {self.as_dict()[field]}"
)
+ event = "helpdesk:ticket-update"
+ room = get_website_room()
+ data = {"name": self.name}
+ frappe.publish_realtime(event, message=data, room=room, after_commit=True)
+
def remove_assignment_if_not_in_team(self):
"""
Removes the assignment if the agent is not in the team.
@@ -278,6 +284,7 @@ def reset_ticket_metrics(self):
self.db_set("resolution_time", None)
self.db_set("user_resolution_time", None)
+ @frappe.whitelist()
def assign_agent(self, agent):
if self._assign:
assignees = json.loads(self._assign)
@@ -290,11 +297,11 @@ def assign_agent(self, agent):
assign({"assign_to": [agent], "doctype": "HD Ticket", "name": self.name})
agent_name = frappe.get_value("HD Agent", agent, "agent_name")
log_ticket_activity(self.name, f"assigned to {agent_name}")
- frappe.publish_realtime(
- "helpdesk:update-ticket-assignee",
- {"ticket_id": self.name},
- after_commit=True,
- )
+
+ event = "helpdesk:ticket-assignee-update"
+ data = {"name": self.name}
+ room = get_website_room()
+ frappe.publish_realtime(event, message=data, room=room, after_commit=True)
def get_assigned_agent(self):
if self._assign:
@@ -335,7 +342,7 @@ def instantly_send_email(self):
return bool(int(check))
@frappe.whitelist()
- def last_communication(self):
+ def get_last_communication(self):
filters = {"reference_doctype": "HD Ticket", "reference_name": ["=", self.name]}
try:
@@ -349,7 +356,7 @@ def last_communication(self):
pass
def last_communication_email(self):
- if not (communication := self.last_communication()):
+ if not (communication := self.get_last_communication()):
return
if not communication.email_account:
@@ -393,15 +400,15 @@ def reply_via_agent(
):
skip_email_workflow = self.skip_email_workflow()
medium = "" if skip_email_workflow else "Email"
- subject = f"Re: {self.subject} {self.name}"
+ subject = f"Re: {self.subject} (#{self.name})"
sender = frappe.session.user
recipients = self.raised_by
sender_email = None if skip_email_workflow else self.sender_email()
- last_communication = self.last_communication()
+ last_communication = self.get_last_communication()
if last_communication:
- bcc = last_communication.bcc or bcc
- cc = last_communication.cc or cc
+ cc = cc or last_communication.cc
+ bcc = bcc or last_communication.bcc
if recipients == "Administrator":
admin_email = frappe.get_value("User", "Administrator", "email")
@@ -445,7 +452,11 @@ def reply_via_agent(
frappe.throw(_("Can not send email. No sender email set up!"))
reply_to_email = sender_email.email_id
- template = "new_reply_on_customer_portal_notification"
+ template = (
+ "new_reply_on_customer_portal_notification"
+ if self.via_customer_portal
+ else None
+ )
args = {
"message": message,
"portal_link": self.portal_uri,
@@ -476,7 +487,7 @@ def reply_via_agent(
sender=reply_to_email,
subject=subject,
template=template,
- with_container=True,
+ with_container=False,
)
except Exception as e:
frappe.throw(_(e))
@@ -536,13 +547,81 @@ def get_assignees(self):
res = (
frappe.qb.from_(QBUser)
- .select(QBUser.full_name, QBUser.user_image)
+ .select(QBUser.name, QBUser.full_name, QBUser.user_image)
.where(Case.any(condition))
.run(as_dict=True)
)
return res
+ @frappe.whitelist()
+ def get_communications(self):
+ conversations = frappe.db.get_all(
+ "Communication",
+ filters={
+ "reference_doctype": ["=", "HD Ticket"],
+ "reference_name": ["=", self.name],
+ },
+ order_by="creation asc",
+ fields=[
+ "name",
+ "content",
+ "creation",
+ "sent_or_received",
+ "sender",
+ "cc",
+ "bcc",
+ ],
+ )
+
+ for conversation in conversations:
+ if frappe.db.exists("HD Agent", conversation.sender):
+ # user User details instead of Contact if the sender is an agent
+ sender = frappe.get_doc("User", conversation.sender).__dict__
+ sender["image"] = sender["user_image"]
+ else:
+ contacts = frappe.get_all(
+ "Contact Email",
+ filters=[["email_id", "like", "%{0}".format(conversation.sender)]],
+ fields=["parent"],
+ limit=1,
+ )
+ if len(contacts) > 0:
+ sender = frappe.get_doc("Contact", contacts[0].parent)
+ else:
+ sender = frappe.get_last_doc(
+ "User", filters={"email": conversation.sender}
+ )
+
+ conversation.sender = sender
+
+ attachments = frappe.get_all(
+ "File",
+ ["file_name", "file_url"],
+ {
+ "attached_to_name": conversation.name,
+ "attached_to_doctype": "Communication",
+ },
+ )
+
+ conversation.attachments = attachments
+
+ return conversations
+
+ @frappe.whitelist()
+ def get_comments(self):
+ filters = {
+ "reference_ticket": self.name,
+ }
+ fields = ["name", "commented_by", "content", "creation"]
+
+ l = frappe.get_list("HD Ticket Comment", filters=filters, fields=fields)
+
+ for i in l:
+ i["sender"] = frappe.get_doc("User", i.commented_by)
+
+ return l
+
def set_descritption_from_communication(doc, type):
if doc.reference_doctype == "HD Ticket":
diff --git a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
index cdd0c74f2..5b48fa493 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
@@ -37,3 +37,10 @@ def after_insert(self):
room = get_website_room()
frappe.publish_realtime(event, message=data, room=room, after_commit=True)
+
+ def after_delete(self):
+ event = "helpdesk:delete-ticket-comment"
+ data = {"ticket_id": self.reference_ticket}
+ room = get_website_room()
+
+ frappe.publish_realtime(event, message=data, room=room, after_commit=True)
diff --git a/helpdesk/helpdesk/report/ticket_summary/ticket_summary.py b/helpdesk/helpdesk/report/ticket_summary/ticket_summary.py
index 21f555bc7..de234c523 100644
--- a/helpdesk/helpdesk/report/ticket_summary/ticket_summary.py
+++ b/helpdesk/helpdesk/report/ticket_summary/ticket_summary.py
@@ -291,7 +291,7 @@ def get_metrics_data(self):
AVG(total_hold_time) as avg_hold_time,
AVG(resolution_time) as avg_resolution_time,
AVG(user_resolution_time) as avg_user_resolution_time
- FROM `tabTicket`
+ FROM `tabHD Ticket`
WHERE
name IN %(ticket)s
GROUP BY {0}
diff --git a/helpdesk/patches.txt b/helpdesk/patches.txt
index e7d42eb0f..bf27357d6 100644
--- a/helpdesk/patches.txt
+++ b/helpdesk/patches.txt
@@ -2,7 +2,7 @@
helpdesk.patches.change_app_name_to_helpdesk
helpdesk.patches.rename_doctypes_prefix_with_hd
helpdesk.patches.rename_frappedesk_module_references
-helpdesk.patches.ticket_naming_rule_to_autoincrement
+helpdesk.patches.naming_autoincrement
[post_model_sync]
execute:frappe.delete_doc("Workspace", "Frappe Desk", force=True)
diff --git a/helpdesk/patches/naming_autoincrement.py b/helpdesk/patches/naming_autoincrement.py
new file mode 100644
index 000000000..ed22a82ba
--- /dev/null
+++ b/helpdesk/patches/naming_autoincrement.py
@@ -0,0 +1,50 @@
+import frappe
+from frappe.utils import get_table_name
+
+from helpdesk.utils import alphanumeric_to_int
+
+DOCTYPES = [
+ "HD Preset Filter Item",
+ "HD Team Item",
+ "HD Team Member",
+ "HD Ticket",
+ "HD Ticket Custom Field Item",
+]
+MODULE = "Helpdesk"
+SEQ_SUFFIX = "_id_seq"
+
+
+def execute():
+ modify_table()
+ create_sequence()
+
+
+def modify_table():
+ for doctype in DOCTYPES:
+ table = f"`{get_table_name(doctype)}`"
+ frappe.db.sql_ddl(f"ALTER TABLE {table} MODIFY COLUMN name BIGINT(20)")
+ frappe.reload_doc(MODULE, "DocType", doctype)
+
+
+def create_sequence():
+ for doctype in DOCTYPES:
+ sequence_name = frappe.scrub(doctype + SEQ_SUFFIX)
+ frappe.db.sql_ddl(f"DROP SEQUENCE IF EXISTS {sequence_name}")
+ start_value = sequence_start(doctype)
+ frappe.db.create_sequence(
+ doctype, check_not_exists=False, start_value=start_value
+ )
+
+
+def sequence_start(doctype: str):
+ try:
+ last_doc = frappe.get_last_doc(doctype, order_by="name desc")
+ last_id = last_doc.name
+
+ if isinstance(last_id, int):
+ return last_id
+
+ last_id = alphanumeric_to_int(last_id) or 0
+ return last_id + 1
+ except:
+ return 1
diff --git a/helpdesk/patches/ticket_naming_rule_to_autoincrement.py b/helpdesk/patches/ticket_naming_rule_to_autoincrement.py
deleted file mode 100644
index aa669ba8c..000000000
--- a/helpdesk/patches/ticket_naming_rule_to_autoincrement.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import frappe
-from frappe.utils import get_table_name
-
-from helpdesk.utils import alphanumeric_to_int
-
-DOCTYPE = "HD Ticket"
-MODULE = "Helpdesk"
-SEQ_SUFFIX = "_id_seq"
-
-
-def execute():
- create_sequence()
- modify_table()
-
-
-def modify_table():
- table = f"`{get_table_name(DOCTYPE)}`"
- frappe.db.sql_ddl(f"ALTER TABLE {table} MODIFY COLUMN name BIGINT(20)")
- frappe.reload_doc(MODULE, "DocType", DOCTYPE)
-
-
-def create_sequence():
- sequence_name = frappe.scrub(DOCTYPE + SEQ_SUFFIX)
- frappe.db.sql_ddl(f"DROP SEQUENCE IF EXISTS {sequence_name}")
- start_value = sequence_start()
- frappe.db.create_sequence(DOCTYPE, check_not_exists=False, start_value=start_value)
-
-
-def sequence_start():
- try:
- last_doc = frappe.get_last_doc(DOCTYPE)
- last_id = alphanumeric_to_int(last_doc.name) or 0
- return last_id + 1
- except:
- return 1
diff --git a/helpdesk/setup/demo_data.py b/helpdesk/setup/demo_data.py
deleted file mode 100644
index 35a6cd99b..000000000
--- a/helpdesk/setup/demo_data.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import frappe
-from helpdesk.helpdesk.doctype.hd_ticket.hd_ticket import (
- create_communication_via_contact,
-)
-
-AUTHOR_EMAIl = "hello@frappedesk.com"
-AUTHOR_NAME = "Frappe Helpdesk Team"
-DEMO_TICKET_CONTENT = """
-
Hi 👋
-
-
We're glad you decided to try Helpdesk! We're working hard to build a better way for teams to communicate and serve customers well. I'm excited to get started.
-
-
You can get started right away by setting up a support email. This will help you see what your support will look like with Frappe Helpdesk!
-
-
Best,
-
Helpdesk Team | Frappe.
-"""
-
-
-def create_demo_data():
- create_team_contact()
- create_demo_ticket()
-
-
-def create_demo_ticket():
- if frappe.db.count("HD Ticket"):
- return
-
- d = frappe.new_doc("HD Ticket")
- d.subject = "Welcome to Helpdesk"
- d.description = DEMO_TICKET_CONTENT
- d.raised_by = AUTHOR_EMAIl
- d.contact = AUTHOR_NAME
- d.via_customer_portal = True
- d.insert()
-
- create_communication_via_contact(d.name, d.description)
-
-
-def create_team_contact():
- frappe.get_doc(
- {
- "doctype": "Contact",
- "first_name": AUTHOR_NAME,
- "email_ids": [{"email_id": AUTHOR_EMAIl, "is_primary": 1}],
- }
- ).insert()
diff --git a/helpdesk/setup/install.py b/helpdesk/setup/install.py
index 70a567a49..4ea204e1b 100644
--- a/helpdesk/setup/install.py
+++ b/helpdesk/setup/install.py
@@ -3,7 +3,7 @@
import frappe
from frappe.permissions import add_permission
-from .demo_data import create_demo_data
+from .welcome_ticket import create_welcome_ticket
def before_install():
@@ -21,7 +21,7 @@ def after_install():
update_agent_role_permissions()
add_default_assignment_rule()
add_system_preset_filters()
- create_demo_data()
+ create_welcome_ticket()
def add_support_redirect_to_tickets():
diff --git a/helpdesk/setup/welcome_ticket.py b/helpdesk/setup/welcome_ticket.py
new file mode 100644
index 000000000..69a8c021d
--- /dev/null
+++ b/helpdesk/setup/welcome_ticket.py
@@ -0,0 +1,62 @@
+import frappe
+from frappe.desk.form.assign_to import add as add_assign
+from helpdesk.helpdesk.doctype.hd_ticket.hd_ticket import (
+ create_communication_via_contact,
+)
+
+AUTHOR_EMAIl = "sabu@frappe.io"
+AUTHOR_NAME = "Sabu Siyad"
+CONTENT = """
+
+Hi 👋
+
+I'm glad you decided to try Helpdesk! We're working hard to build a better way for teams
+to communicate and serve customers well.
+
+You can get started right away by setting up a support email. This will help you see what
+your support will look like with Helpdesk!
+
+If you face any issues, please reach out to us via
+https://frappedesk.com/helpdesk
+
+Best,
+
+Sabu Siyad | Frappe Helpdesk.
+"""
+
+
+def create_welcome_ticket():
+ create_contact()
+ create_ticket()
+
+
+def create_ticket():
+ if frappe.db.count("HD Ticket"):
+ return
+
+ d = frappe.new_doc("HD Ticket")
+ d.subject = "Welcome to Helpdesk"
+ d.description = CONTENT
+ d.raised_by = AUTHOR_EMAIl
+ d.contact = AUTHOR_NAME
+ d.via_customer_portal = True
+ d.insert()
+
+ create_communication_via_contact(d.name, d.description)
+ add_assign(
+ {
+ "doctype": "HD Ticket",
+ "name": d.name,
+ "assign_to": ["Administrator"],
+ }
+ )
+
+
+def create_contact():
+ frappe.get_doc(
+ {
+ "doctype": "Contact",
+ "first_name": AUTHOR_NAME,
+ "email_ids": [{"email_id": AUTHOR_EMAIl, "is_primary": 1}],
+ }
+ ).insert()