diff --git a/desk/public/article-2.png b/desk/public/article-2.png new file mode 100644 index 000000000..e5421f4b7 Binary files /dev/null and b/desk/public/article-2.png differ diff --git a/desk/public/article.png b/desk/public/article.png new file mode 100644 index 000000000..a50a04e9b Binary files /dev/null and b/desk/public/article.png differ diff --git a/desk/src/components/ColumnSettings.vue b/desk/src/components/ColumnSettings.vue index 1410f70ca..dd857e542 100644 --- a/desk/src/components/ColumnSettings.vue +++ b/desk/src/components/ColumnSettings.vue @@ -154,6 +154,10 @@ let props = defineProps({ type: Boolean, default: false, }, + isCustomerPortal: { + type: Boolean, + default: false, + }, }); function resetToDefault(close) { diff --git a/desk/src/components/Filter.vue b/desk/src/components/Filter.vue index 8f24361fc..83fe3e65b 100644 --- a/desk/src/components/Filter.vue +++ b/desk/src/components/Filter.vue @@ -213,7 +213,7 @@ function setfilter(data) { function updateFilter( index: number, field = null, - value: string = null, + value: string = "", operator: string = null ) { let filter = JSON.parse(JSON.stringify(props.filters[index])); diff --git a/desk/src/components/Settings/EmailAdd.vue b/desk/src/components/Settings/EmailAdd.vue index 9f7d175e0..3d15e0e28 100644 --- a/desk/src/components/Settings/EmailAdd.vue +++ b/desk/src/components/Settings/EmailAdd.vue @@ -68,7 +68,7 @@ :name="field.name" :type="field.type" /> -
{{ field.description }}
+{{ field.description }}
{{ field.description }}
+{{ field.description }}
(), {
isActive: false,
onClick: () => () => true,
to: "",
+ bgColor: "bg-white",
+ hvColor: "hover:bg-gray-100",
});
const router = useRouter();
diff --git a/desk/src/components/UserAvatar.vue b/desk/src/components/UserAvatar.vue
index a37f3bf03..64b415a49 100644
--- a/desk/src/components/UserAvatar.vue
+++ b/desk/src/components/UserAvatar.vue
@@ -1,6 +1,11 @@
(), {
});
const route = useRoute();
const ticket = inject(ITicket);
-const data = computed(() => ticket.data || {});
-const communications = computed(() => data.value.communications || []);
-const comments = computed(() => data.value.comments || []);
-const conversation = computed(() =>
- orderBy([...communications.value, ...comments.value], (c) =>
- dayjs(c.creation)
- )
-);
+const communications = computed(() => {
+ const _communications = ticket.data.communications || [];
+ return orderBy(_communications, (c) => dayjs(c.creation));
+});
function scroll(id: string) {
const e = document.getElementById(id);
@@ -69,7 +76,7 @@ watch(
);
nextTick(() => {
const hash = route.hash.slice(1);
- const id = hash || conversation.value.slice(-1).pop()?.name;
+ const id = hash || communications.value.slice(-1).pop()?.name;
if (id) setTimeout(() => scroll(id), 1000);
});
diff --git a/desk/src/pages/ticket/TicketCustomerTemplateFields.vue b/desk/src/pages/ticket/TicketCustomerTemplateFields.vue
index 6b949bd33..094aefaed 100644
--- a/desk/src/pages/ticket/TicketCustomerTemplateFields.vue
+++ b/desk/src/pages/ticket/TicketCustomerTemplateFields.vue
@@ -29,9 +29,7 @@
+ {{ article.title }}
+
+
+ {{ category.categoryName }}
+
+ Sub-categories
+
+ {{ subCategory?.category_name }}
+
+ {{ subCategory.articles.length }} articles
+
+
+ {{ showAllArticles ? "All Articles" : "Articles" }}
+
+
+
+
diff --git a/desk/src/router/index.js b/desk/src/router/index.ts
similarity index 73%
rename from desk/src/router/index.js
rename to desk/src/router/index.ts
index 14fc8bf62..82b23dd4f 100644
--- a/desk/src/router/index.js
+++ b/desk/src/router/index.ts
@@ -30,77 +30,89 @@ export const KB_PUBLIC_CATEGORY = "KBCategoryPublic";
export const CUSTOMER_PORTAL_LANDING = "TicketsCustomer";
export const AGENT_PORTAL_LANDING = AGENT_PORTAL_TICKET_LIST;
export const REDIRECT_PAGE = "/login?redirect-to=/helpdesk";
+
+export const CUSTOMER_PORTAL_ROUTES = [
+ "TicketsCustomer",
+ "TicketNew",
+ "TicketCustomer",
+];
+
+// type the meta fields
+declare module "vue-router" {
+ interface RouteMeta {
+ auth?: boolean;
+ agent?: boolean;
+ admin?: boolean;
+ public?: boolean;
+ onSuccessRoute?: string;
+ parent?: string;
+ }
+}
+
const routes = [
{
path: "",
component: () => import("@/pages/HRoot.vue"),
},
+ // Customer portal routing
{
- path: "/knowledge-base",
- component: () => import("@/pages/KnowledgeBasePublic.vue"),
- children: [
- {
- path: "",
- name: "KBHome",
- component: () => import("@/pages/KnowledgeBasePublicHome.vue"),
- },
- {
- path: ":categoryId",
- name: "KBCategoryPublic",
- component: () => import("@/pages/KnowledgeBasePublicCategory.vue"),
- props: true,
- },
- {
- path: "articles/:articleId",
- name: "KBArticlePublic",
- component: () => import("@/pages/KnowledgeBaseArticle.vue"),
- meta: {
- public: true,
- },
- props: true,
- },
- ],
- },
- {
- path: "/my-tickets",
- component: () => import("@/pages/CLayout.vue"),
+ path: "",
+ name: "CustomerRoot",
+ // component: () => import("@/pages/CLayout.vue"), // old customer portal
+ component: () => import("@/pages/CustomerPortalRoot.vue"),
meta: {
auth: true,
+ public: true,
},
children: [
+ // handle tickets routing
{
- path: "",
- name: "TicketsCustomer",
- component: () => import("@/pages/TicketsCustomer.vue"),
- },
- {
- path: "new/:templateId?",
- name: "TicketNew",
- component: () => import("@/pages/TicketNew.vue"),
- props: true,
- meta: {
- onSuccessRoute: "TicketCustomer",
- parent: "TicketsCustomer",
- },
+ path: "my-tickets",
+ children: [
+ {
+ path: "",
+ name: "TicketsCustomer",
+ component: () => import("@/pages/Tickets.vue"),
+ },
+ {
+ path: "new/:templateId?",
+ name: "TicketNew",
+ component: () => import("@/pages/TicketNew.vue"),
+ props: true,
+ meta: {
+ onSuccessRoute: "TicketCustomer",
+ parent: "TicketsCustomer",
+ },
+ },
+ {
+ path: ":ticketId",
+ name: "TicketCustomer",
+ component: () => import("@/pages/TicketCustomer.vue"),
+ props: true,
+ },
+ ],
},
+ // handle knowledge base routing
{
- path: ":ticketId",
- name: "TicketCustomer",
- component: () => import("@/pages/TicketCustomer.vue"),
- props: true,
+ path: "knowledge-base-public",
+ children: [
+ {
+ path: "",
+ name: "KnowledgeBasePublicNew",
+ component: () =>
+ import("@/pages/knowledge-base-v2/KnowledgeBasePublic.vue"),
+ },
+ {
+ path: "articles/:articleId?",
+ name: "KBArticlePublicNew",
+ component: () => import("@/pages/KnowledgeBaseArticle.vue"),
+ props: true,
+ },
+ ],
},
],
},
- {
- path: "/onboarding",
- name: ONBOARDING_PAGE,
- component: () => import("@/pages/onboarding/SimpleOnboarding.vue"),
- },
- {
- path: "/:invalidpath",
- name: "Invalid Page",
- component: () => import("@/pages/InvalidPage.vue"),
- },
+ // Agent Portal Routing
{
path: "",
name: "AgentRoot",
@@ -114,7 +126,7 @@ const routes = [
{
path: "tickets",
name: AGENT_PORTAL_TICKET_LIST,
- component: () => import("@/pages/TicketsAgent.vue"),
+ component: () => import("@/pages/Tickets.vue"),
},
{
path: "notifications",
@@ -204,6 +216,17 @@ const routes = [
},
],
},
+ // Additonal routes
+ {
+ path: "/onboarding",
+ name: ONBOARDING_PAGE,
+ component: () => import("@/pages/onboarding/SimpleOnboarding.vue"),
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ name: "Invalid Page",
+ component: () => import("@/pages/InvalidPage.vue"),
+ },
];
const handleMobileView = (componentName) => {
diff --git a/desk/src/stores/ticketStatus.ts b/desk/src/stores/ticketStatus.ts
index 7b98c83a5..b99ce11ec 100644
--- a/desk/src/stores/ticketStatus.ts
+++ b/desk/src/stores/ticketStatus.ts
@@ -18,6 +18,7 @@ export const useTicketStatusStore = defineStore("ticketStatus", () => {
const textColorMap = {
Open: "text-red-600",
Replied: "text-blue-600",
+ "Awaiting Response": "text-blue-600",
Resolved: "text-green-700",
Closed: "text-gray-700",
};
diff --git a/desk/src/types.ts b/desk/src/types.ts
index 2fa61e131..a4d9feb13 100644
--- a/desk/src/types.ts
+++ b/desk/src/types.ts
@@ -79,6 +79,8 @@ export interface Ticket {
subject: string;
ticket_type: string;
via_customer_portal: string;
+ agreement_status: string;
+ creation: string;
feedback_rating?: number;
feedback_text?: string;
feedback_extra?: string;
@@ -204,3 +206,42 @@ export interface TabObject {
icon: Component;
condition?: () => boolean;
}
+
+export interface RootCategory {
+ category_id: string;
+ category_name: string;
+}
+
+export interface Article {
+ name: string;
+ title: string;
+ category: string;
+ published_on: string;
+ author: string;
+ subtitle: string;
+ article_image: string | null;
+ _user_tags: string | null;
+}
+
+export interface SubCategory {
+ name: string;
+ category_name: string;
+ icon: string | null;
+ articles: Article[];
+}
+
+export interface Author {
+ name: string;
+ image: string | null;
+ email?: string;
+}
+
+export interface Category {
+ categoryName: string;
+ subCategories: SubCategory[];
+ articles: Article[];
+ authors?: {
+ [key: string]: Author;
+ };
+ children?: (Article | SubCategory)[];
+}
diff --git a/desk/tsconfig.json b/desk/tsconfig.json
index 5ed9653ce..193b9ffb6 100644
--- a/desk/tsconfig.json
+++ b/desk/tsconfig.json
@@ -11,6 +11,8 @@
"types": [
"unplugin-icons/types/vue",
"vite/client"
- ]
- }
+ ],
+ },
+ "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
+ "exclude": ["node_modules"]
}
diff --git a/frappe-ui b/frappe-ui
index a51471841..9f927012a 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit a5147184122d7d8fd2d71af7caef2395708758f5
+Subproject commit 9f927012a3ef54e934fa01bea07b57f8ec06617c
diff --git a/helpdesk/__init__.py b/helpdesk/__init__.py
index 61fb31cae..5becc17c0 100644
--- a/helpdesk/__init__.py
+++ b/helpdesk/__init__.py
@@ -1 +1 @@
-__version__ = "0.10.0"
+__version__ = "1.0.0"
diff --git a/helpdesk/api/article.py b/helpdesk/api/article.py
index 7fbccf13e..d7794e4ac 100644
--- a/helpdesk/api/article.py
+++ b/helpdesk/api/article.py
@@ -2,6 +2,7 @@
from textblob import TextBlob
from textblob.exceptions import MissingCorpusError
+from helpdesk.search import NUM_RESULTS
from helpdesk.search import search as hd_search
@@ -19,24 +20,36 @@ def get_noun_phrases(blob: TextBlob):
return []
+def search_with_enough_results(prev_res: list, query: str) -> tuple[list, bool]:
+ out = hd_search(query, only_articles=True)
+ if not out:
+ return prev_res, len(prev_res) == NUM_RESULTS
+ items = prev_res + out[0].get("items", [])
+ items = list({v["id"]: v for v in items}.values())[:NUM_RESULTS] # unique results
+ return items, len(items) == NUM_RESULTS
+
+
@frappe.whitelist()
-def search(query: str):
+def search(query: str) -> list:
query = query.strip().lower()
- out = hd_search(query, only_articles=True)
- if not out: # fallback
- blob = TextBlob(query)
- if noun_phrases := get_noun_phrases(blob):
- and_query = " ".join(noun_phrases)
- or_query = "|".join(noun_phrases)
- out = hd_search(and_query, only_articles=True) or hd_search(
- or_query, only_articles=True
- )
- if not out and (nouns := get_nouns(blob)):
- and_query = " ".join(nouns)
- or_query = "|".join(nouns)
- out = hd_search(and_query, only_articles=True) or hd_search(
- or_query, only_articles=True
- )
- if not out:
- return []
- return out[0].get("items", [])
+ ret, enough = search_with_enough_results([], query)
+ if enough:
+ return ret
+ blob = TextBlob(query) # fallback
+ if noun_phrases := get_noun_phrases(blob):
+ and_query = " ".join(noun_phrases)
+ ret, enough = search_with_enough_results(ret, and_query)
+ if enough:
+ return ret
+ or_query = "|".join(noun_phrases)
+ ret, enough = search_with_enough_results(ret, or_query)
+ if enough:
+ return ret
+ if nouns := get_nouns(blob):
+ and_query = " ".join(nouns)
+ ret, enough = search_with_enough_results(ret, and_query)
+ if enough:
+ return ret
+ or_query = "|".join(nouns)
+ ret, enough = search_with_enough_results(ret, or_query)
+ return ret
diff --git a/helpdesk/api/doc.py b/helpdesk/api/doc.py
index f4dcb90d5..9369dd91c 100644
--- a/helpdesk/api/doc.py
+++ b/helpdesk/api/doc.py
@@ -9,7 +9,7 @@
@frappe.whitelist()
@redis_cache()
-def get_filterable_fields(doctype):
+def get_filterable_fields(doctype: str, show_customer_portal_fields=False):
check_permissions(doctype, None)
QBDocField = frappe.qb.DocType("DocField")
QBCustomField = frappe.qb.DocType("Custom Field")
@@ -26,6 +26,17 @@ def get_filterable_fields(doctype):
"Text",
]
+ visible_custom_fields = get_visible_custom_fields()
+ customer_portal_fields = [
+ "name",
+ "subject",
+ "status",
+ "priority",
+ "response_by",
+ "resolution_by",
+ "creation",
+ ]
+
from_doc_fields = (
frappe.qb.from_(QBDocField)
.select(
@@ -38,7 +49,6 @@ def get_filterable_fields(doctype):
.where(QBDocField.parent == doctype)
.where(QBDocField.hidden == False)
.where(Criterion.any([QBDocField.fieldtype == i for i in allowed_fieldtypes]))
- .run(as_dict=True)
)
from_custom_fields = (
@@ -55,29 +65,47 @@ def get_filterable_fields(doctype):
.where(
Criterion.any([QBCustomField.fieldtype == i for i in allowed_fieldtypes])
)
- .run(as_dict=True)
)
+ # for customer portal show only fields present in customer_portal_fields
+
+ if show_customer_portal_fields:
+ from_doc_fields = from_doc_fields.where(
+ QBDocField.fieldname.isin(customer_portal_fields)
+ )
+ from_custom_fields = from_custom_fields.where(
+ QBCustomField.fieldname.isin(visible_custom_fields)
+ )
+
+ from_doc_fields = from_doc_fields.run(as_dict=True)
+ from_custom_fields = from_custom_fields.run(as_dict=True)
+
+ # from hd ticket template get children with fieldname and hidden_from_customer
+
res = []
res.extend(from_doc_fields)
+ # TODO: Ritvik => till a better way we have for custom fields, just show custom fields
res.extend(from_custom_fields)
- res.extend(
- [
+ if not show_customer_portal_fields:
+ res.append(
{
"fieldname": "_assign",
"fieldtype": "Link",
"label": "Assigned to",
"name": "_assign",
"options": "HD Agent",
- },
- {
- "fieldname": "name",
- "fieldtype": "Data",
- "label": "ID",
- "name": "name",
- },
- ]
+ }
+ )
+
+ res.append(
+ {
+ "fieldname": "name",
+ "fieldtype": "Data",
+ "label": "ID",
+ "name": "name",
+ },
)
+
return res
@@ -90,6 +118,7 @@ def get_list_data(
page_length=20,
columns=None,
rows=None,
+ show_customer_portal_fields=False,
):
is_default = True
@@ -123,7 +152,7 @@ def get_list_data(
# flake8: noqa
if is_default:
if hasattr(list, "default_list_data"):
- columns = list.default_list_data().get("columns")
+ columns = list.default_list_data(show_customer_portal_fields).get("columns")
rows = list.default_list_data().get("rows")
# check if rows has all keys from columns if not add them
@@ -176,6 +205,9 @@ def get_list_data(
if field not in fields:
fields.append(field)
+ if show_customer_portal_fields:
+ fields = get_customer_portal_fields(doctype, fields)
+
return {
"data": data,
"columns": columns,
@@ -187,7 +219,7 @@ def get_list_data(
@frappe.whitelist()
-def sort_options(doctype: str):
+def sort_options(doctype: str, show_customer_portal_fields=False):
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldtype not in no_value_fields]
fields = [
@@ -199,6 +231,9 @@ def sort_options(doctype: str):
if field.label and field.fieldname
]
+ if show_customer_portal_fields:
+ fields = get_customer_portal_fields(doctype, fields)
+
standard_fields = [
{"label": "Name", "value": "name"},
{"label": "Created On", "value": "creation"},
@@ -207,7 +242,30 @@ def sort_options(doctype: str):
{"label": "Owner", "value": "owner"},
]
- for field in standard_fields:
- fields.append(field)
+ fields.extend(standard_fields)
return fields
+
+
+def get_customer_portal_fields(doctype, fields):
+ visible_custom_fields = get_visible_custom_fields()
+ customer_portal_fields = [
+ "name",
+ "subject",
+ "status",
+ "priority",
+ "response_by",
+ "resolution_by",
+ "creation",
+ *visible_custom_fields,
+ ]
+ fields = [field for field in fields if field.get("value") in customer_portal_fields]
+ return fields
+
+
+def get_visible_custom_fields():
+ return frappe.db.get_all(
+ "HD Ticket Template Field",
+ {"parent": "Default", "hide_from_customer": 0},
+ pluck="fieldname",
+ )
diff --git a/helpdesk/api/kbase.py b/helpdesk/api/kbase.py
new file mode 100644
index 000000000..65929085b
--- /dev/null
+++ b/helpdesk/api/kbase.py
@@ -0,0 +1,81 @@
+import frappe
+from frappe.utils import get_user_info_for_avatar
+
+# from frappe.core.utils import html2text
+
+
+@frappe.whitelist()
+def get_sub_categories_and_articles(category):
+ category_title = frappe.get_value("HD Article Category", category, "category_name")
+ sub_categories = frappe.get_all(
+ "HD Article Category",
+ filters={"parent_category": category},
+ fields=["name", "category_name", "icon", "parent_category"],
+ order_by="creation",
+ )
+ sub_categories_names = [sub_category["name"] for sub_category in sub_categories]
+
+ article_fields = [
+ "name",
+ "title",
+ "category",
+ "published_on",
+ "author",
+ "subtitle",
+ "article_image",
+ "_user_tags",
+ ]
+
+ direct_articles = frappe.get_all(
+ "HD Article",
+ filters={"category": category, "status": "Published"},
+ fields=article_fields,
+ )
+
+ nested_articles = frappe.get_all(
+ "HD Article",
+ filters={"category": ["in", sub_categories_names], "status": "Published"},
+ fields=article_fields,
+ )
+
+ category_tree = {
+ "root_category": {"category_id": category, "category_name": category_title},
+ "sub_categories": [],
+ "all_articles": direct_articles,
+ "authors": {},
+ "children": [],
+ }
+ # Create a dictionary to store sub-categories by their name
+ sub_categories_dict = {sub_cat["name"]: sub_cat for sub_cat in sub_categories}
+
+ # Add nested articles to their respective sub-categories
+ for article in nested_articles:
+ sub_cat = sub_categories_dict[article["category"]]
+ if "articles" not in sub_cat:
+ sub_cat["articles"] = []
+ sub_cat["articles"].append(article)
+
+ # Add sub-categories to the main tree
+ for sub_cat in sub_categories:
+ sub_cat_tree = {
+ "name": sub_cat["name"],
+ "category_name": sub_cat["category_name"],
+ "icon": sub_cat["icon"],
+ "articles": sub_cat.get("articles", []),
+ }
+ category_tree["sub_categories"].append(sub_cat_tree)
+
+ # add all articles to the main tree
+
+ for sub_cat in category_tree["sub_categories"]:
+ category_tree["all_articles"].extend(sub_cat["articles"])
+
+ # get author details
+ for article in category_tree["all_articles"]:
+ author = article["author"]
+ if author not in category_tree["authors"]:
+ category_tree["authors"][author] = get_user_info_for_avatar(author)
+
+ category_tree["children"] = direct_articles + category_tree["sub_categories"]
+
+ return category_tree
diff --git a/helpdesk/helpdesk/doctype/hd_article/api.py b/helpdesk/helpdesk/doctype/hd_article/api.py
index 445eb07c1..84be868b1 100644
--- a/helpdesk/helpdesk/doctype/hd_article/api.py
+++ b/helpdesk/helpdesk/doctype/hd_article/api.py
@@ -14,7 +14,7 @@ def get_article(name: str):
author = frappe.get_cached_doc("User", article["author"])
sub_category = frappe.get_cached_doc("HD Article Category", article["category"])
category = frappe.get_cached_doc(
- "HD Article Category", sub_category.parent_category
+ "HD Article Category", sub_category.parent_category or article["category"]
)
return {
diff --git a/helpdesk/helpdesk/doctype/hd_article/hd_article.json b/helpdesk/helpdesk/doctype/hd_article/hd_article.json
index cf90e2c88..d8b2db488 100644
--- a/helpdesk/helpdesk/doctype/hd_article/hd_article.json
+++ b/helpdesk/helpdesk/doctype/hd_article/hd_article.json
@@ -14,6 +14,8 @@
"column_break_7",
"author",
"idx",
+ "subtitle",
+ "article_image",
"content_section",
"content"
],
@@ -85,11 +87,21 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Title Slug"
+ },
+ {
+ "fieldname": "article_image",
+ "fieldtype": "Attach Image",
+ "label": "Article Image"
+ },
+ {
+ "fieldname": "subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle"
}
],
"links": [],
"make_attachments_public": 1,
- "modified": "2024-06-25 11:58:31.475327",
+ "modified": "2024-10-01 21:39:58.189965",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Article",
diff --git a/helpdesk/helpdesk/doctype/hd_notification/hd_notification.json b/helpdesk/helpdesk/doctype/hd_notification/hd_notification.json
index 7456d08a6..6ade339ba 100644
--- a/helpdesk/helpdesk/doctype/hd_notification/hd_notification.json
+++ b/helpdesk/helpdesk/doctype/hd_notification/hd_notification.json
@@ -82,7 +82,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-12-19 15:31:37.669662",
+ "modified": "2024-09-30 02:16:06.186898",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Notification",
@@ -110,6 +110,10 @@
"role": "Agent",
"share": 1,
"write": 1
+ },
+ {
+ "read": 1,
+ "role": "Guest"
}
],
"sort_field": "modified",
diff --git a/helpdesk/helpdesk/doctype/hd_service_level_agreement/hd_service_level_agreement.py b/helpdesk/helpdesk/doctype/hd_service_level_agreement/hd_service_level_agreement.py
index 523a2e2f4..2f1f1a171 100644
--- a/helpdesk/helpdesk/doctype/hd_service_level_agreement/hd_service_level_agreement.py
+++ b/helpdesk/helpdesk/doctype/hd_service_level_agreement/hd_service_level_agreement.py
@@ -326,12 +326,11 @@ def calc_elapsed_time(self, start_time, end_time) -> float:
or not_in_working_day_list
or not self.is_working_time(current_time, working_hours)
):
- current_time += timedelta(seconds=1)
+ current_time += timedelta(minutes=1)
continue
total_seconds += 1
- current_time += timedelta(seconds=1)
-
- return total_seconds
+ current_time += timedelta(minutes=1)
+ return total_seconds * 60
def get_holidays(self):
res = []
diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
index e61398f95..c01797dc3 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
@@ -705,8 +705,7 @@ def on_communication_update(self, c):
self.save()
@staticmethod
- def default_list_data():
-
+ def default_list_data(show_customer_portal_fields=False):
columns = [
{
"label": "ID",
@@ -801,6 +800,65 @@ def default_list_data():
"width": "8rem",
},
]
+ customer_portal_columns = [
+ {
+ "label": "ID",
+ "type": "Int",
+ "key": "name",
+ "width": "5rem",
+ },
+ {
+ "label": "Subject",
+ "type": "Data",
+ "key": "subject",
+ "width": "22rem",
+ },
+ {
+ "label": "Status",
+ "type": "Select",
+ "key": "status",
+ "width": "11rem",
+ },
+ {
+ "label": "Priority",
+ "type": "Link",
+ "options": "HD Ticket Priority",
+ "key": "priority",
+ "width": "10rem",
+ },
+ {
+ "label": "First response",
+ "type": "Datetime",
+ "key": "response_by",
+ "width": "8rem",
+ },
+ {
+ "label": "Resolution",
+ "type": "Datetime",
+ "key": "resolution_by",
+ "width": "8rem",
+ },
+ {
+ "label": "Team",
+ "type": "Link",
+ "options": "HD Team",
+ "key": "agent_group",
+ "width": "10rem",
+ },
+ # {
+ # "label": "Assigned To",
+ # "type": "Text",
+ # "key": "_assign",
+ # "width": "10rem",
+ # },
+ {
+ "label": "Created",
+ "type": "Datetime",
+ "key": "creation",
+ "options": "Contact",
+ "width": "8rem",
+ },
+ ]
rows = [
"name",
"subject",
@@ -819,7 +877,12 @@ def default_list_data():
"_assign",
"resolution_date",
]
- return {"columns": columns, "rows": rows}
+ return {
+ "columns": customer_portal_columns
+ if show_customer_portal_fields
+ else columns,
+ "rows": rows,
+ }
# Check if `user` has access to this specific ticket (`doc`). This implements extra
diff --git a/helpdesk/helpdesk/doctype/hd_ticket_priority/hd_ticket_priority.json b/helpdesk/helpdesk/doctype/hd_ticket_priority/hd_ticket_priority.json
index 71742437e..fbf3f142e 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket_priority/hd_ticket_priority.json
+++ b/helpdesk/helpdesk/doctype/hd_ticket_priority/hd_ticket_priority.json
@@ -30,7 +30,7 @@
}
],
"links": [],
- "modified": "2023-04-11 20:48:07.721515",
+ "modified": "2024-09-29 22:30:34.206381",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Ticket Priority",
@@ -72,6 +72,15 @@
"role": "Agent",
"share": 1,
"write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"quick_entry": 1,
@@ -79,4 +88,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/helpdesk/search.py b/helpdesk/search.py
index f6a8ca9bf..9d50bfa57 100644
--- a/helpdesk/search.py
+++ b/helpdesk/search.py
@@ -25,6 +25,8 @@
if TYPE_CHECKING:
from helpdesk.helpdesk.doctype.hd_settings.hd_settings import HDSettings
+NUM_RESULTS = 5
+
STOPWORDS = [
"a",
"is",
@@ -148,7 +150,7 @@ def search(
self,
query,
start=0,
- page_length=5,
+ page_length=NUM_RESULTS,
highlight=False,
):
query = self.clean_query(query)
@@ -208,7 +210,6 @@ def index_exists(self):
class HelpdeskSearch(Search):
-
DOCTYPE_FIELDS = {
"HD Ticket": [
"name",
@@ -342,7 +343,7 @@ def get_records(self, doctype):
@frappe.whitelist()
-def search(query, only_articles=False):
+def search(query, only_articles=False) -> list[dict[str, list[dict]]]:
search = HelpdeskSearch()
query = search.clean_query(query)
query_parts = query.split()