1<template>
2 <SettingsLayoutBase
3 :title="__('Agents')"
4 :description="__('Add, manage agents and assign roles to them.')"
5 >
6 <template #header-actions>
7 <Button
8 @click="() => setActiveSettingsTab('Invite Agents')"
9 label="New"
10 variant="solid"
11 class="rtl:flex-row-reverse"
12 >
13 <template #prefix>
14 <LucidePlus class="h-4 w-4 stroke-1.5" />
15 </template>
16 </Button>
17 </template>
18 <template #header-bottom>
19 <div class="flex items-center gap-2 justify-between">
20 <div class="relative grow">
21 <TextInput
22 :model-value="search"
23 @update:model-value="search = $event"
24 :placeholder="__('Search')"
25 type="text"
26 class="focus:ring-0 border-outline-gray-2"
27 :debounce="300"
28 >
29 <template #prefix>
30 <LucideSearch class="size-4" />
31 </template>
32 </TextInput>
33 <Button
34 v-if="search"
35 icon="lucide-x"
36 variant="ghost"
37 @click="search = ''"
38 class="absolute end-1 top-1/2 -translate-y-1/2"
39 />
40 </div>
41 <Dropdown :options="dropdownOptions" align="end">
42 <template #default="{ open }">
43 <Button :label="activeFilter">
44 <template #suffix>
45 <component
46 :is="open ? LucideChevronUp : LucideChevronDown"
47 class="h-4"
48 />
49 </template>
50 </Button>
51 </template>
52 <template #item-label="{ item }">
53 <button
54 class="group flex text-ink-gray-6 gap-4 w-full justify-between items-center rounded-4 text-base"
55 @click="item.onClick"
56 >
57 <div class="flex items-center justify-between flex-1">
58 <span class="whitespace-nowrap">
59 {{ item.label }}
60 </span>
61 <LucideCheck
62 v-if="activeFilter === item.label"
63 class="size-4 text-ink-gray-7"
64 />
65 </div>
66 </button>
67 </template>
68 </Dropdown>
69 </div>
70 </template>
71 <template #content>
72 <div class="grow">
73 <!-- loading state -->
74 <div
75 v-if="agents.loading"
76 class="flex mt-28 justify-between w-full h-full"
77 >
78 <Button
79 :loading="agents.loading"
80 variant="ghost"
81 class="w-full"
82 size="lg"
83 />
84 </div>
85 <!-- Empty State -->
86 <EmptyState
87 v-if="!agents.loading && !agents.data?.length"
88 variant="badge"
89 :icon="AgentIcon"
90 title="No agent found"
91 :description="
92 activeFilter.length
93 ? 'Try a different search or filter.'
94 : 'Add one to get started.'
95 "
96 />
97 <!-- Agent List -->
98 <div
99 class="w-full"
100 v-if="!agents.loading && Boolean(agents.data?.length)"
101 >
102 <div
103 class="grid grid-cols-8 items-center gap-3 text-sm text-ink-gray-5"
104 >
105 <div class="col-span-6 text-p-sm">{{ __("Agent name") }}</div>
106 </div>
107 <hr class="mt-2" />
108 <div v-for="(agent, index) in agents.data" :key="agent.agent_name">
109 <div class="flex items-center justify-between h-14 group rounded-4">
110 <div class="flex items-center gap-x-3 grow">
111 <Avatar
112 :image="agent.user_image"
113 :label="agent.agent_name"
114 size="xl"
115 />
116 <div>
117 <div class="flex items-center gap-2">
118 <p class="text-base">
119 {{ agent.agent_name }}
120 </p>
121 <Badge
122 :label="__('Inactive')"
123 :theme="'gray'"
124 :class="!agent.is_active ? 'opacity-100' : 'opacity-0'"
125 variant="subtle"
126 />
127 </div>
128 <div class="text-base text-ink-gray-6 mt-1">
129 {{ agent.name }}
130 </div>
131 </div>
132 </div>
133 <div class="flex items-center gap-2">
134 <Dropdown
135 v-if="isManager"
136 class="flex justify-end items-center"
137 :options="getRoles(agent.name)"
138 :label="getUserRole(agent.name)"
139 :button="{
140 label: getUserRole(agent.name),
141 iconRight: 'lucide-chevron-down',
142 iconLeft:
143 getUserRole(agent.name) === 'Agent'
144 ? 'lucide-user'
145 : getUserRole(agent.name) === 'Manager'
146 ? 'lucide-briefcase'
147 : null,
148 }"
149 align="end"
150 />
151 <Dropdown :options="getOptions(agent)" :key="agent" align="end">
152 <Button icon="lucide-more-horizontal" variant="ghost" />
153 </Dropdown>
154 </div>
155 </div>
156 <hr v-if="index !== agents.data.length - 1" />
157 </div>
158 <!-- Load More Button -->
159 <div class="flex justify-center">
160 <Button
161 v-if="!agents.loading && agents.hasNextPage"
162 class="mt-3.5 p-2"
163 @click="() => agents.next()"
164 :loading="agents.loading"
165 :label="__('Load More')"
166 icon-left="lucide-refresh-cw"
167 />
168 </div>
169 </div>
170 </div>
171 </template>
172 </SettingsLayoutBase>
173</template>
174
175<script setup lang="ts">
176import LucideChevronUp from "~icons/lucide/chevron-up";
177import LucideChevronDown from "~icons/lucide/chevron-down";
178import LucideCheck from "~icons/lucide/check";
179import EmptyState from "@/components/EmptyState.vue";
180import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
181import { useAuthStore } from "@/stores/auth";
182import { useUserStore } from "@/stores/user";
183import { __ } from "@/translation";
184import { Avatar, Button, call, Dropdown, toast } from "frappe-ui";
185import { h, onUnmounted } from "vue";
186import AgentIcon from "../icons/AgentIcon.vue";
187import { activeFilter, useAgents } from "./agents";
188import { setActiveSettingsTab } from "./settingsModal";
189
190const { getUserRole, updateUserRoleCache } = useUserStore();
191const { isManager } = useAuthStore();
192
193const agentStore = useAgents();
194const search = agentStore.search;
195const agents = agentStore.agents;
196
197function getRoles(agent: string) {
198 const agentRole = getUserRole(agent);
199 const role = (label: string, icon: string) => ({
200 label,
201 icon,
202 selected: agentRole === label,
203 onClick: () => updateRole(agent, label),
204 });
205 const roles = [role("Agent", "lucide-user")];
206 if (isManager) {
207 roles.unshift(role("Manager", "lucide-briefcase"));
208 }
209
210 return roles;
211}
212
213function updateRole(agent: string, newRole: string) {
214 const currentRole = getUserRole(agent);
215 if (currentRole === newRole) {
216 return;
217 }
218
219 call("opendesk.opendesk.doctype.od_agent.od_agent.update_agent_role", {
220 user: agent,
221 new_role: newRole,
222 }).then(() => {
223 updateUserRoleCache(agent, newRole);
224 toast.success(__(`Role updated to ${newRole} successfully.`));
225 });
226}
227
228function getOptions(agent) {
229 let filters = agentStore.filters;
230 return [
231 {
232 label: "Disable Agent",
233 icon: "lucide-x-circle",
234 onClick: async () => {
235 await agentStore.updateAgent(agent.name, 0);
236 agents.reload({ ...filters, search: search.value });
237 },
238 condition: () => agent.is_active,
239 },
240 {
241 label: "Enable Agent",
242 icon: "lucide-check-circle",
243 onClick: async () => {
244 await agentStore.updateAgent(agent.name, 1);
245 agents.reload({ ...filters, search: search.value });
246 },
247 condition: () => !agent.is_active,
248 },
249 ];
250}
251
252const dropdownOptions = [
253 {
254 label: "All",
255 onClick: () => {
256 agentStore.filters["is_active"] = ["in", [0, 1]];
257 activeFilter.value = "All";
258 },
259 },
260 {
261 label: "Active",
262 onClick: () => {
263 agentStore.filters["is_active"] = ["=", 1];
264 activeFilter.value = "Active";
265 },
266 },
267 {
268 label: "Inactive",
269 onClick: () => {
270 agentStore.filters["is_active"] = ["=", 0];
271 activeFilter.value = "Inactive";
272 },
273 },
274];
275
276onUnmounted(() => {
277 agents.filters = {};
278});
279</script>
280