1<template>
2 <SettingsLayoutBase
3 :back-label="teamName"
4 :on-back="() => emit('update:step', 'team-list')"
5 >
6 <template #header-actions>
7 <div class="flex items-center gap-4">
8 <div class="flex items-center justify-between gap-2 cursor-pointer">
9 <Switch v-model="teamEnabled" v-if="!team.loading" />
10 <span class="text-sm-medium text-ink-gray-7">
11 {{ __("Enabled") }}
12 </span>
13 </div>
14 <Dropdown
15 align="end"
16 :options="options"
17 @update:open="(open) => open && (isConfirmingTeamDelete = false)"
18 >
19 <Button variant="ghost">
20 <template #icon>
21 <LucideMoreHorizontal class="h-4 w-4" />
22 </template>
23 </Button>
24 </Dropdown>
25 </div>
26 </template>
27 <template #header-bottom>
28 <!-- Add member -->
29 <div class="flex gap-2 items-center">
30 <!-- Form control for search -->
31 <div class="flex flex-col gap-1.5 w-full">
32 <div class="flex gap-2">
33 <div class="flex flex-1">
34 <AgentSelector
35 v-model="invitees"
36 :existing-agents="teamMembers.map((m) => m.name)"
37 />
38 </div>
39 <Button
40 label="Add Member"
41 variant="solid"
42 :disabled="!invitees.length"
43 @click="addMember(invitees)"
44 class="h-8"
45 >
46 <template #prefix>
47 <LucidePlus class="h-4 w-4 stroke-1.5" />
48 </template>
49 </Button>
50 </div>
51 </div>
52 </div>
53 </template>
54 <template #content>
55 <div class="w-full h-full" v-if="teamMembers?.length > 0">
56 <div
57 class="grid grid-cols-8 items-center gap-3 text-sm text-ink-gray-5"
58 >
59 <div class="col-span-6 text-p-sm">
60 {{ __("Members ({0})", teamMembers.length) }}
61 </div>
62 </div>
63 <hr class="mt-2" />
64 <div v-for="(member, idx) in teamMembers" :key="member.name">
65 <div class="grid grid-cols-8 items-center gap-4 group">
66 <div class="w-full p-2 ps-0 col-span-8">
67 <AgentCard :agent="member" class="!py-0">
68 <template #right>
69 <Dropdown
70 v-if="teamMembers.length > 1"
71 :options="memberDropdownOptions(member)"
72 align="end"
73 @update:open="
74 (open) => open && (isConfirmingDelete = false)
75 "
76 >
77 <Button icon="lucide-more-horizontal" variant="ghost" />
78 </Dropdown>
79 </template>
80 </AgentCard>
81 </div>
82 </div>
83 <hr v-if="member !== teamMembers.at(-1)" />
84 </div>
85 </div>
86 <div
87 v-else
88 class="flex flex-col items-center justify-center gap-4 h-full"
89 >
90 <div
91 class="p-4 size-14.5 rounded-full bg-surface-gray-1 flex justify-center items-center"
92 >
93 <UserIcon class="size-6 text-ink-gray-6" />
94 </div>
95 <div class="flex flex-col items-center gap-1">
96 <div class="text-base-medium text-ink-gray-6">
97 {{ __("No members found") }}
98 </div>
99 <div class="text-p-sm text-ink-gray-5 max-w-60 text-center">
100 {{ __("Add members to this team to get started.") }}
101 </div>
102 </div>
103 </div>
104 </template>
105 </SettingsLayoutBase>
106 <RenameTeamModal
107 v-model="showRename"
108 @onRename="
109 () => {
110 teamsList.reload();
111 emit('update:step', 'team-list');
112 }
113 "
114 />
115</template>
116
117<script setup lang="ts">
118import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
119import { useAgentStore } from "@/stores/agent";
120import { assignmentRulesActiveScreen } from "@/stores/assignmentRules";
121import { useConfigStore } from "@/stores/config";
122import { useUserStore } from "@/stores/user";
123import { __ } from "@/translation";
124import { TeamListResourceSymbol } from "@/types";
125import { ConfirmDelete } from "@/utils";
126import {
127 Button,
128 createDocumentResource,
129 Dropdown,
130 Switch,
131 toast,
132} from "frappe-ui";
133import { computed, h, inject, markRaw, onMounted, ref } from "vue";
134import RenameTeamModal from "./RenameTeamModal.vue";
135import LucideLock from "~icons/lucide/lock";
136import Settings from "~icons/lucide/settings-2";
137import LucideUnlock from "~icons/lucide/unlock";
138import UserIcon from "~icons/lucide/user";
139import AgentCard from "../AgentCard.vue";
140import { setActiveSettingsTab } from "../settingsModal";
141import AgentSelector from "./components/AgentSelector.vue";
142
143const props = defineProps<{
144 teamName: string;
145}>();
146
147interface E {
148 (event: "update:step", step: string, team?: string): void;
149}
150const emit = defineEmits<E>();
151
152const { getUser } = useUserStore();
153const { agents } = useAgentStore();
154const teamsList = inject(TeamListResourceSymbol);
155
156const { teamRestrictionApplied } = useConfigStore();
157const invitees = ref<string[]>([]);
158
159const team = createDocumentResource({
160 doctype: "OD Team",
161 name: props.teamName,
162 auto: true,
163 delete: {
164 onSuccess() {
165 toast.success(__("Team deleted successfully."));
166 emit("update:step", "team-list");
167 },
168 },
169});
170
171const teamEnabled = computed({
172 get() {
173 return !team.doc?.disabled;
174 },
175 set(value: boolean) {
176 if (!team.doc) return;
177 team.doc.disabled = !value;
178 team.setValue.submit(
179 {
180 disabled: !value,
181 },
182 {
183 onSuccess: () => {
184 toast.success(
185 value
186 ? __("Team enabled successfully.")
187 : __("Team disabled successfully.")
188 );
189 team.reload();
190 },
191 }
192 );
193 },
194});
195
196const ignoreRestrictions = computed({
197 get() {
198 return !!team.doc?.ignore_restrictions;
199 },
200 set(value: boolean) {
201 if (!team.doc) return;
202 team.setValue.submit({
203 ignore_restrictions: value,
204 });
205 },
206});
207
208const teamMembers = computed(() => {
209 let users = team.doc?.users || [];
210 return users.map((user) => {
211 let _user = getUser(user.user);
212 return {
213 name: user.user,
214 user_image: _user.user_image,
215 agent_name: _user.full_name,
216 };
217 });
218});
219
220function removeMemberFromTeam(member: string) {
221 const users = team.doc?.users?.filter((u) => u.user !== member);
222 team.setValue.submit({
223 users,
224 });
225}
226
227function addMember(users: string[]) {
228 const _users = team.doc.users.concat(users.map((user) => ({ user })));
229 team.setValue.submit({
230 users: _users,
231 });
232 invitees.value = [];
233}
234
235const showRename = ref({
236 show: false,
237 teamName: props.teamName,
238});
239
240const isConfirmingTeamDelete = ref(false);
241
242const options = computed(() => [
243 {
244 label: __("View Assignment rule"),
245 icon: markRaw(h(Settings, { class: "rotate-90" })),
246 onClick: () => {
247 assignmentRulesActiveScreen.value = {
248 data: { name: team.doc?.assignment_rule },
249 screen: "view",
250 };
251 setActiveSettingsTab("Assignment Rules");
252 },
253 },
254 {
255 label: __("Rename"),
256 icon: "lucide-edit-3",
257 onClick: () => {
258 showRename.value = { show: true, teamName: props.teamName };
259 },
260 },
261 ...(teamRestrictionApplied
262 ? [
263 {
264 label: ignoreRestrictions.value
265 ? __("Access only this team's tickets")
266 : __("Access all team tickets"),
267 icon: markRaw(ignoreRestrictions.value ? LucideLock : LucideUnlock),
268 onClick: () => {
269 ignoreRestrictions.value = !ignoreRestrictions.value;
270 toast.success(
271 ignoreRestrictions.value
272 ? __("Team can now access all tickets.")
273 : __("Team will only see their own tickets.")
274 );
275 },
276 },
277 ]
278 : []),
279 ...ConfirmDelete({
280 isConfirmingDelete: isConfirmingTeamDelete,
281 onConfirmDelete: () => team.delete.submit(),
282 }),
283]);
284
285const isConfirmingDelete = ref(false);
286
287const memberDropdownOptions = (member) => {
288 return ConfirmDelete({
289 onConfirmDelete: () => removeMemberFromTeam(member.name),
290 isConfirmingDelete,
291 });
292};
293
294onMounted(() => {
295 if (agents.loading || agents.data?.length || agents.list.promise) {
296 return;
297 }
298 agents.fetch();
299});
300</script>
301