mirror of
https://github.com/hanzoai/helpdesk.git
synced 2026-10-12 16:56:02 +00:00
@@ -151,6 +151,7 @@ const htmlContent = computed(
|
||||
font-weight: 700;
|
||||
height: 12px;
|
||||
line-height: 0.1;
|
||||
color: #383838;
|
||||
background: #e8eaed;
|
||||
width: 23px;
|
||||
justify-content: center;
|
||||
@@ -159,6 +160,13 @@ const htmlContent = computed(
|
||||
.replied-content .collapse:hover {
|
||||
background: #dadce0;
|
||||
}
|
||||
[data-theme='dark'] .replied-content .collapse {
|
||||
color: #e2e2e2;
|
||||
background: #424242;
|
||||
}
|
||||
[data-theme='dark'] .replied-content .collapse:hover {
|
||||
background: #525252;
|
||||
}
|
||||
.replied-content .collapse + input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -193,18 +193,25 @@ const onPriorityChange = () => {
|
||||
setAssignmentRuleValue("priority", props.data.priority);
|
||||
};
|
||||
|
||||
const onToggle = () => {
|
||||
const onToggle = (enabled: boolean) => {
|
||||
if (!props.data.users_exists && props.data.disabled) {
|
||||
toast.error(__("Cannot enable rule without adding users in it"));
|
||||
return;
|
||||
}
|
||||
setAssignmentRuleValue("disabled", !props.data.disabled, "status");
|
||||
// Optimistically flip so the switch reflects the new state immediately;
|
||||
// revert if the backend update fails.
|
||||
const previous = props.data.disabled;
|
||||
props.data.disabled = enabled ? 0 : 1;
|
||||
setAssignmentRuleValue("disabled", props.data.disabled, "status", () => {
|
||||
props.data.disabled = previous;
|
||||
});
|
||||
};
|
||||
|
||||
const setAssignmentRuleValue = (
|
||||
key: string,
|
||||
value: any,
|
||||
fieldName?: string
|
||||
fieldName?: string,
|
||||
onError?: () => void
|
||||
) => {
|
||||
createResource({
|
||||
url: "frappe.client.set_value",
|
||||
@@ -215,11 +222,16 @@ const setAssignmentRuleValue = (
|
||||
value: value,
|
||||
},
|
||||
onSuccess: () => {
|
||||
assignmentRulesListData?.reload();
|
||||
toast.success(
|
||||
__("Assignment rule {0} updated successfully.", fieldName || key)
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
onError?.();
|
||||
toast.error(
|
||||
__("Failed to update assignment rule {0}.", fieldName || key)
|
||||
);
|
||||
},
|
||||
auto: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -195,7 +195,7 @@ import { useNotificationStore } from "@/stores/notification";
|
||||
import { useSidebarStore } from "@/stores/sidebar";
|
||||
import { capture } from "@/telemetry";
|
||||
import { isCustomerPortal } from "@/utils";
|
||||
import { call, toast } from "frappe-ui";
|
||||
import { call, toast, useTheme } from "frappe-ui";
|
||||
import {
|
||||
GettingStartedBanner,
|
||||
HelpModal,
|
||||
@@ -224,6 +224,8 @@ import LucideBell from "~icons/lucide/bell";
|
||||
import FileText from "~icons/lucide/file-text";
|
||||
import Globe from "~icons/lucide/globe";
|
||||
import LucideKeyboard from "~icons/lucide/keyboard";
|
||||
import LucideMoon from "~icons/lucide/moon";
|
||||
import LucideSun from "~icons/lucide/sun";
|
||||
import LucideMail from "~icons/lucide/mail";
|
||||
import MailOpen from "~icons/lucide/mail-open";
|
||||
import MessageCircle from "~icons/lucide/message-circle";
|
||||
@@ -254,6 +256,13 @@ const showShortcutsModal = ref(false);
|
||||
const showCommandPalette = ref(false);
|
||||
|
||||
const { pinnedViews, publicViews } = useView();
|
||||
const { currentTheme, toggleTheme } = useTheme();
|
||||
|
||||
const themeMenuItem = computed(() => ({
|
||||
label: __("Toggle theme"),
|
||||
icon: currentTheme.value === "dark" ? LucideSun : LucideMoon,
|
||||
onClick: () => toggleTheme(),
|
||||
}));
|
||||
|
||||
const isFCSite = ref(window.is_fc_site);
|
||||
|
||||
@@ -326,6 +335,7 @@ function parseViews(views) {
|
||||
}
|
||||
|
||||
const customerPortalDropdown = computed(() => [
|
||||
themeMenuItem.value,
|
||||
{
|
||||
group: __("Danger"),
|
||||
hideLabel: true,
|
||||
|
||||
@@ -122,12 +122,16 @@
|
||||
>
|
||||
<UserAvatar :name="agent.value" size="sm" />
|
||||
</Tooltip>
|
||||
<div
|
||||
class="absolute bottom-0 -right-0.5 size-2 rounded-full outline outline-white outline-1.5"
|
||||
:class="
|
||||
agentStatusStore.statusColor(agent.availability || '')
|
||||
"
|
||||
/>
|
||||
<span
|
||||
class="absolute block translate-x-1/2 translate-y-1/2 transform rounded-full bottom-0.5 right-0.5"
|
||||
>
|
||||
<span
|
||||
class="block h-2 w-2 rounded-full border border-slate-2"
|
||||
:class="
|
||||
agentStatusStore.statusColor(agent.availability || '')
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-ink-gray-7 flex-1 text-start truncate">
|
||||
{{ agent.label }}
|
||||
|
||||
@@ -211,8 +211,7 @@ function handleDeleteTicket() {
|
||||
iconLeft: "trash-2",
|
||||
variant: "solid",
|
||||
onClick({ close }) {
|
||||
call("frappe.client.delete", {
|
||||
doctype: "HD Ticket",
|
||||
call("helpdesk.api.ticket.delete_ticket", {
|
||||
name: ticket?.value?.doc.name,
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="showHeader"
|
||||
class="mx-6 md:mx-10 md:my-2 flex items-center justify-between text-lg font-medium mb-4 !mt-6 md:h-8 md:text-xl md:font-semibold md:text-ink-gray-8"
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
<div class="overflow-auto px-6 md:px-10 grow">
|
||||
<div class="overflow-auto px-5 md:px-10 grow">
|
||||
<div
|
||||
v-for="(c, i) in communications"
|
||||
:id="c.name"
|
||||
@@ -52,10 +53,12 @@ import { ITicket } from "./symbols";
|
||||
|
||||
interface P {
|
||||
focus?: string;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<P>(), {
|
||||
focus: "",
|
||||
showHeader: true,
|
||||
});
|
||||
const route = useRoute();
|
||||
const ticket = inject(ITicket);
|
||||
|
||||
@@ -38,11 +38,24 @@
|
||||
>
|
||||
</Alert>
|
||||
</div>
|
||||
<!-- show for only mobile -->
|
||||
<TicketCustomerTemplateFields v-if="isMobileView" />
|
||||
<!-- Mobile: Activity / Details tabs -->
|
||||
<Tabs
|
||||
v-if="isMobileView"
|
||||
v-model="activeTab"
|
||||
:tabs="tabs"
|
||||
class="[&_[role='tablist']]:px-5"
|
||||
>
|
||||
<template #tab-panel="{ tab }">
|
||||
<TicketCustomerTemplateFields v-if="tab.name === 'details'" />
|
||||
<TicketConversation v-else :show-header="false" class="grow" />
|
||||
</template>
|
||||
</Tabs>
|
||||
|
||||
<!-- Desktop: conversation -->
|
||||
<TicketConversation v-else class="grow" />
|
||||
|
||||
<TicketConversation class="grow" />
|
||||
<div
|
||||
v-if="!isMobileView || activeTab === 0"
|
||||
class="w-full p-5"
|
||||
@keydown.ctrl.enter.capture.stop="sendEmail"
|
||||
@keydown.meta.enter.capture.stop="sendEmail"
|
||||
@@ -92,12 +105,14 @@ import { globalStore } from "@/stores/globalStore";
|
||||
import { useTicketStatusStore } from "@/stores/ticketStatus";
|
||||
import { __ } from "@/translation";
|
||||
import { isContentEmpty, isCustomerPortal, uploadFunction } from "@/utils";
|
||||
import { ActivityIcon, DetailsIcon } from "@/components/icons";
|
||||
import {
|
||||
Alert,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createResource,
|
||||
Tabs,
|
||||
toast,
|
||||
} from "frappe-ui";
|
||||
import {
|
||||
@@ -162,6 +177,12 @@ const { isMobileView } = useScreenSize();
|
||||
const { $dialog, $socket } = globalStore();
|
||||
const isDismissed = ref(false);
|
||||
|
||||
const activeTab = ref(0);
|
||||
const tabs = computed(() => [
|
||||
{ name: "activity", label: __("Activity"), icon: ActivityIcon },
|
||||
{ name: "details", label: __("Details"), icon: DetailsIcon },
|
||||
]);
|
||||
|
||||
function getTodayKey() {
|
||||
return new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
@@ -1,44 +1,59 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-3 md:grid-cols-1 gap-4 border-b px-5 py-2.5">
|
||||
<div class="space-y-1.5">
|
||||
<span class="block text-sm text-ink-gray-7"> Status </span>
|
||||
<span class="block break-words text-base font-medium text-ink-gray-9">
|
||||
<div class="space-y-2 px-6 py-3.5 border-b">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">Status</span>
|
||||
<span
|
||||
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
|
||||
>
|
||||
{{ ticket.data.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<span class="block text-sm text-ink-gray-7"> Priority </span>
|
||||
<span class="block break-words text-base font-medium text-ink-gray-9">
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">Priority</span>
|
||||
<span
|
||||
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
|
||||
>
|
||||
{{ ticket.data.priority }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="data in slaData" :key="data.label" class="space-y-1.5">
|
||||
|
||||
<div
|
||||
v-for="data in slaData"
|
||||
:key="data.label"
|
||||
class="flex items-center gap-4"
|
||||
>
|
||||
<Tooltip :text="dayjs(data.value).format('LLLL')">
|
||||
<span class="block text-sm text-ink-gray-7">{{ data.title }}</span>
|
||||
<span class="w-[160px] shrink-0 text-sm text-ink-gray-5">{{
|
||||
data.title
|
||||
}}</span>
|
||||
</Tooltip>
|
||||
<span class="block break-words text-base font-medium text-ink-gray-9">
|
||||
<span
|
||||
class="flex-1 rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
|
||||
>
|
||||
<Badge
|
||||
v-if="data.showSla"
|
||||
:label="data.label"
|
||||
:theme="data.theme"
|
||||
variant="outline"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ dayjs.tz(data.value).fromNow() }}
|
||||
</span>
|
||||
<span v-else>{{ dayjs.tz(data.value).fromNow() }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="field in customFields"
|
||||
:key="field.fieldname"
|
||||
class="space-y-1.5"
|
||||
class="flex items-center gap-4"
|
||||
>
|
||||
<span class="block text-sm text-ink-gray-7">
|
||||
{{ field.label }}
|
||||
</span>
|
||||
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">{{
|
||||
field.label
|
||||
}}</span>
|
||||
<span
|
||||
class="block break-words text-base font-medium text-ink-gray-9"
|
||||
:class="!ticket.data[field.fieldname] && 'text-ink-gray-4'"
|
||||
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium"
|
||||
:class="
|
||||
ticket.data[field.fieldname] ? 'text-ink-gray-9' : 'text-ink-gray-4'
|
||||
"
|
||||
>
|
||||
{{ ticket.data[field.fieldname] || "—" }}
|
||||
</span>
|
||||
|
||||
+12
-1
@@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from helpdesk.utils import agent_only
|
||||
from helpdesk.utils import agent_only, is_admin
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -66,3 +66,14 @@ def assign_ticket_to_agent(ticket_id, agent_id=None):
|
||||
|
||||
ticket_doc.assign_agent(agent_id)
|
||||
return ticket_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_ticket(name: str):
|
||||
if not is_admin():
|
||||
frappe.throw(
|
||||
msg=_("Only administrators can delete tickets."),
|
||||
title=_("Not Allowed"),
|
||||
exc=frappe.PermissionError,
|
||||
)
|
||||
frappe.delete_doc("HD Ticket", name, force=True, ignore_permissions=True)
|
||||
|
||||
Reference in New Issue
Block a user