1<template>
2 <AppSidebar :profile-settings="profileSettings">
3 <template #footer="{ isCollapsed }">
4 <!-- The Sidebar container already has p-2; the extra px-2 only fits when
5 expanded. Collapsed, it would squeeze the banners' icon buttons to 0. -->
6 <div
7 class="flex flex-col gap-2"
8 :class="isCollapsed ? 'items-center' : 'px-2'"
9 >
10 <TrialBanner v-if="isFCSite" :isSidebarCollapsed="isCollapsed" />
11 <GettingStartedBanner
12 v-if="showOnboardingBanner"
13 :isSidebarCollapsed="isCollapsed"
14 appName="opendesk"
15 />
16 <CustomerPortalPermissionBanner
17 v-if="isBannerVisible('customer_portal_permission')"
18 :isSidebarCollapsed="isCollapsed"
19 />
20 <TicketFieldPermissionBanner
21 v-if="isBannerVisible('ticket_field_permission')"
22 :isSidebarCollapsed="isCollapsed"
23 />
24 </div>
25 <SidebarItem
26 v-if="isOnboardingStepsCompleted"
27 :label="__('Help')"
28 :icon="HelpIcon"
29 :on-click="
30 () => {
31 showHelpModal = minimize ? true : !showHelpModal;
32 minimize = !showHelpModal;
33 }
34 "
35 />
36 </template>
37 </AppSidebar>
38
39 <SettingsModal v-model="showSettingsModal" />
40 <ShortcutsModal v-model="showShortcutsModal" />
41 <HelpModal
42 v-if="showHelpModal"
43 v-model="showHelpModal"
44 v-model:articles="articles"
45 appName="opendesk"
46 title="Opendesk"
47 :logo="logo"
48 docsLink="https://github.com/ikhsanddy/opendesk"
49 :afterSkip="(step: string) => capture('onboarding_step_skipped_' + step)"
50 :afterSkipAll="() => capture('onboarding_steps_skipped')"
51 :afterReset="(step: string) => capture('onboarding_step_reset_' + step)"
52 :afterResetAll="() => capture('onboarding_steps_reset')"
53 />
54 <IntermediateStepModal
55 v-model="showIntermediateModal"
56 :currentStep="currentStep"
57 />
58</template>
59
60<script setup lang="ts">
61import ODLogo from "@/assets/logos/ODLogo.vue";
62import {
63 ArticleIcon,
64 FrappeCloudIcon,
65 InviteCustomer,
66} from "@/components/icons";
67import CustomerPortalPermissionBanner from "@/components/banners/CustomerPortalPermissionBanner.vue";
68import TicketFieldPermissionBanner from "@/components/banners/TicketFieldPermissionBanner.vue";
69import ShortcutsModal from "@/components/modals/ShortcutsModal.vue";
70import SettingsModal from "@/components/Settings/SettingsModal.vue";
71import { confirmLoginToFrappeCloud } from "@/composables/fc";
72import { useApps } from "@/composables/useApps";
73import { useBanners } from "@/composables/useBanners";
74import { useScreenSize } from "@/composables/screen";
75import { showNewContactModal } from "@/pages/contact/dialogState";
76import {
77 showAssignmentModal,
78 showCommentBox,
79 showEmailBox,
80} from "@/pages/ticket/modalStates";
81import { useAuthStore } from "@/stores/auth";
82import { capture } from "@/telemetry";
83import { CUSTOMER_PORTAL_ROOT } from "@/utils";
84import { call, SidebarItem, toast, useColorScheme } from "frappe-ui";
85import {
86 GettingStartedBanner,
87 HelpModal,
88 IntermediateStepModal,
89 minimize,
90 showHelpModal,
91 TrialBanner,
92 useOnboarding,
93} from "@framework/ui";
94
95import { HelpIcon } from "frappe-ui/icons";
96import { computed, h, markRaw, onMounted, ref } from "vue";
97import { useRouter } from "vue-router";
98import AppSidebar from "./AppSidebar.vue";
99import { showShortcutsModal } from "./layoutSettings";
100
101import { useShortcut } from "@opendesk/shared/shortcuts";
102import { __ } from "@/translation";
103import Globe from "~icons/lucide/globe";
104import LucideKeyboard from "~icons/lucide/keyboard";
105import LucideMoon from "~icons/lucide/moon";
106import LucideSun from "~icons/lucide/sun";
107import LucideMail from "~icons/lucide/mail";
108import MailOpen from "~icons/lucide/mail-open";
109import MessageCircle from "~icons/lucide/message-circle";
110import Ticket from "~icons/lucide/ticket";
111import Timer from "~icons/lucide/timer";
112import UserPen from "~icons/lucide/user-pen";
113import LucideUserPlus from "~icons/lucide/user-plus";
114
115import {
116 setActiveSettingsTab,
117 showSettingsModal,
118} from "../Settings/settingsModal";
119
120const { isMobileView } = useScreenSize();
121
122const router = useRouter();
123const authStore = useAuthStore();
124const { isVisible: isBannerVisible } = useBanners();
125
126const { appsMenuOption } = useApps();
127const { colorScheme, toggleColorScheme } = useColorScheme();
128
129const themeMenuItem = computed(() => ({
130 label: __("Toggle theme"),
131 icon: colorScheme.value === "dark" ? LucideSun : LucideMoon,
132 onClick: () => toggleColorScheme(),
133}));
134
135const isFCSite = ref(window.is_fc_site);
136
137const profileSettings = computed(() => [
138 appsMenuOption.value,
139 {
140 label: __("Customer portal"),
141 icon: "lucide-users",
142 onClick: () => window.open(CUSTOMER_PORTAL_ROOT),
143 },
144 {
145 icon: "lucide-life-buoy",
146 label: __("Support"),
147 onClick: () => window.open("https://t.me/opendesk"),
148 },
149 {
150 icon: "lucide-book-open",
151 label: __("Docs"),
152 onClick: () => window.open("https://github.com/ikhsanddy/opendesk"),
153 },
154 {
155 label: __("Login to Frappe Cloud"),
156 icon: FrappeCloudIcon,
157 onClick: () => confirmLoginToFrappeCloud(),
158 condition: () => !isMobileView.value && window.is_fc_site,
159 },
160 {
161 label: __("Shortcuts"),
162 icon: h(LucideKeyboard),
163 onClick: () => (showShortcutsModal.value = true),
164 },
165 {
166 label: __("Settings"),
167 icon: "lucide-settings",
168 onClick: () => (showSettingsModal.value = true),
169 },
170 {
171 group: __("Danger"),
172 hideLabel: true,
173 options: [
174 {
175 label: __("Log out"),
176 icon: "lucide-log-out",
177 onClick: () => authStore.logout(),
178 },
179 ],
180 },
181]);
182
183const logo = h(
184 ODLogo,
185 {
186 class: "h-12 w-12",
187 },
188 null
189);
190
191const showOnboardingBanner = computed(() => {
192 return !isOnboardingStepsCompleted?.value && authStore.isManager;
193});
194
195const steps = [
196 {
197 name: "setup_email_account",
198 title: __("Connect your support email"),
199 completed: false,
200 icon: markRaw(LucideMail),
201 onClick: () => {
202 minimize.value = true;
203 showSettingsModal.value = true;
204 setActiveSettingsTab("Email Accounts");
205 },
206 },
207 {
208 name: "invite_agents",
209 title: __("Invite agents"),
210 completed: false,
211 icon: markRaw(LucideUserPlus),
212 onClick: () => {
213 minimize.value = true;
214 showSettingsModal.value = true;
215 setActiveSettingsTab("Invite Agents");
216 },
217 },
218 {
219 name: "setup_sla",
220 title: __("Setup SLA"),
221 completed: false,
222 icon: markRaw(Timer),
223 onClick: () => {
224 setActiveSettingsTab("SLA Policies");
225 showSettingsModal.value = true;
226 minimize.value = true;
227 },
228 },
229 {
230 name: "create_first_ticket",
231 title: __("Create a ticket"),
232 completed: false,
233 icon: markRaw(Ticket),
234 onClick: () => {
235 router.push({ name: "TicketAgentNew" });
236 minimize.value = true;
237 },
238 },
239 {
240 name: "assign_to_agent",
241 title: __("Assign a ticket to an agent"),
242 completed: false,
243 dependsOn: "create_first_ticket",
244 icon: markRaw(UserPen),
245 onClick: async () => {
246 await handleFirstTicketNavigation();
247 showAssignmentModal.value = true;
248 minimize.value = true;
249 },
250 },
251 {
252 name: "reply_on_ticket",
253 title: __("Reply on a ticket"),
254 completed: false,
255 dependsOn: "create_first_ticket",
256 icon: markRaw(MailOpen),
257 onClick: async () => {
258 await handleFirstTicketNavigation();
259 showEmailBox.value = true;
260 showCommentBox.value = false;
261 minimize.value = true;
262 },
263 },
264 {
265 name: "comment_on_ticket",
266 title: __("Add a comment on a ticket"),
267 completed: false,
268 dependsOn: "create_first_ticket",
269 icon: markRaw(MessageCircle),
270 onClick: async () => {
271 await handleFirstTicketNavigation();
272 showCommentBox.value = true;
273 showEmailBox.value = false;
274 minimize.value = true;
275 },
276 },
277 {
278 name: "first_article",
279 title: __("Create an article"),
280 completed: false,
281 icon: markRaw(ArticleIcon),
282 onClick: async () => {
283 const generalCategory = await getGeneralCategory();
284 router.push({
285 name: "NewArticle",
286 query: {
287 title: __("General"),
288 },
289 params: { id: generalCategory },
290 });
291 minimize.value = true;
292 },
293 },
294 {
295 name: "add_invite_contact",
296 title: __("Create & invite a contact"),
297 completed: false,
298 icon: markRaw(InviteCustomer),
299 onClick: () => {
300 minimize.value = true;
301 currentStep.value = {
302 title: __("Create & invite a contact"),
303 buttonLabel: __("Create"),
304 videoURL: "/assets/opendesk/desk/videos/createInviteContact.mp4",
305 onClick: async () => {
306 showIntermediateModal.value = false;
307 router.push({ name: "ContactList" });
308 showNewContactModal.value = true;
309 },
310 };
311 showIntermediateModal.value = true;
312 },
313 },
314 {
315 name: "explore_customer_portal",
316 title: __("Explore customer portal"),
317 completed: false,
318 icon: markRaw(Globe),
319 onClick: () => {
320 window.open(CUSTOMER_PORTAL_ROOT, "_blank");
321 updateOnboardingStep?.("explore_customer_portal");
322 minimize.value = true;
323 },
324 },
325];
326
327const articles = ref([
328 {
329 title: __("Introduction"),
330 opened: false,
331 subArticles: [
332 { name: "introduction", title: __("Introduction") },
333 { name: "setting-up", title: __("Setting up") },
334 ],
335 },
336 {
337 title: __("Getting Started"),
338 opened: false,
339 subArticles: [
340 {
341 name: "lesson-1-your-first-ticket",
342 title: __("Creating a ticket"),
343 },
344 {
345 name: "lesson-2understanding-ticket-view",
346 title: __("Understanding ticket view"),
347 },
348 {
349 name: "lesson-3-agents-teams",
350 title: __("Agents & Teams"),
351 },
352 {
353 name: "customers-contacts",
354 title: __("Customers & Contacts"),
355 },
356 {
357 name: "lesson-4-knowledge-base",
358 title: __("Knowledge Base"),
359 },
360 {
361 name: "customer-portal",
362 title: __("Customer Portal"),
363 },
364 ],
365 },
366 {
367 title: __("Masters"),
368 opened: false,
369 subArticles: [
370 { name: "ticket", title: __("Ticket") },
371 { name: "agent", title: __("Agent") },
372 { name: "team", title: __("Team") },
373 { name: "contact", title: __("Contact") },
374 { name: "customer", title: __("Customer") },
375 { name: "knowledge-base", title: __("Knowledge Base") },
376 { name: "saved-replies", title: __("Saved Replies") },
377 { name: "service-level-agreement", title: __("Service Level Agreement") },
378 { name: "ticket-type", title: __("Ticket Type") },
379 { name: "ticket-priority", title: __("Ticket Priority") },
380 ],
381 },
382 {
383 title: __("Customizations"),
384 opened: false,
385 subArticles: [
386 { name: "custom-actions", title: __("Custom Actions") },
387 { name: "field-dependency", title: __("Field Dependency") },
388 { name: "custom-views", title: __("Custom Views") },
389 {
390 name: "settings",
391 title: __("Settings"),
392 },
393 ],
394 },
395 {
396 title: __("Opendesk Mobile"),
397 opened: false,
398 subArticles: [
399 { name: "pwa-installation", title: __("Mobile App Installation") },
400 ],
401 },
402]);
403
404const showIntermediateModal = ref(false);
405const currentStep = ref({});
406
407const { isOnboardingStepsCompleted, setUp, updateOnboardingStep } =
408 useOnboarding("opendesk") ?? {};
409
410async function handleFirstTicketNavigation() {
411 const ticket = await getFirstTicket();
412
413 if (!ticket) {
414 router.push({ name: "TicketAgentNew" });
415 updateOnboardingStep?.("create_first_ticket", false); // reset the step as first ticket is not created
416 toast.error(
417 __("Please create a new ticket to proceed with the next step.")
418 );
419 return;
420 }
421
422 router.push({
423 name: "TicketAgent",
424 params: { ticketId: ticket },
425 });
426}
427
428async function getFirstTicket() {
429 let cachedTicket = localStorage.getItem("firstTicket");
430 const ticket = await call("opendesk.api.onboarding.get_first_ticket", {
431 ticket: cachedTicket,
432 });
433 if (ticket) {
434 localStorage.setItem("firstTicket", ticket);
435 } else {
436 localStorage.removeItem("firstTicket");
437 }
438 return ticket;
439}
440
441async function getGeneralCategory() {
442 let generalCategory = localStorage.getItem("generalCategoryId");
443 if (!generalCategory) {
444 generalCategory = await call(
445 "opendesk.api.onboarding.get_general_category_id"
446 );
447 if (!generalCategory) return;
448 localStorage.setItem("generalCategoryId", generalCategory);
449 }
450 return generalCategory;
451}
452
453function setUpOnboarding() {
454 if (!authStore.isManager) return;
455 setUp?.(steps);
456 useShortcut({ key: "h", meta: true }, () => {
457 showHelpModal.value = !showHelpModal.value;
458 });
459}
460
461onMounted(() => {
462 setUpOnboarding();
463 useShortcut({ key: ",", meta: true }, () => {
464 showSettingsModal.value = !showSettingsModal.value;
465 });
466});
467</script>
468