1<template>
2 <SettingsLayoutBase
3 :description="__('Sync customers between Opendesk and ERPNext.')"
4 >
5 <template #title>
6 <div class="flex items-center gap-2">
7 <h1 class="text-md-semibold text-ink-gray-8">
8 {{ __("ERPNext") }}
9 </h1>
10 <Badge
11 v-if="installed === false"
12 theme="gray"
13 variant="subtle"
14 :label="__('Not installed')"
15 />
16 </div>
17 </template>
18 <template #content>
19 <!-- Loading State -->
20 <div
21 v-if="syncInfoResource.loading && !syncInfoResource.data"
22 class="flex items-center justify-center absolute inset-x-0 top-5.5 bottom-0"
23 >
24 <LoadingIndicator class="w-4" />
25 </div>
26 <div v-else class="flex flex-col gap-4">
27 <!-- ERPNext not installed -->
28 <div class="flex items-center justify-between gap-4">
29 <div class="flex min-w-0 items-center gap-3">
30 <ErpnextIcon
31 class="size-9 shrink-0"
32 :class="{ 'opacity-60': installed === false }"
33 />
34 <div class="flex min-w-0 flex-col gap-1">
35 <span class="text-base-medium text-ink-gray-8">
36 {{ __("Enable ERPNext Integration") }}
37 </span>
38 <span class="text-p-sm text-ink-gray-6">
39 {{ __("Sync customers between Opendesk and ERPNext.") }}
40 </span>
41 </div>
42 </div>
43 <div
44 v-if="installed === false"
45 class="cursor-not-allowed"
46 @click="showNotInstalledToast"
47 >
48 <Switch :model-value="false" disabled class="pointer-events-none" />
49 </div>
50 <Switch v-else v-model="erpnextIntegrationEnabled" />
51 </div>
52
53 <!-- ERPNext not installed -->
54 <div
55 v-if="installed === false"
56 class="flex items-center gap-2 text-p-sm text-ink-gray-7"
57 >
58 <span>
59 {{ __("Install the") }}
60 <a
61 href="https://github.com/ikhsanddy/opendesk"
62 target="_blank"
63 rel="noopener noreferrer"
64 class="font-medium text-ink-gray-8 underline underline-offset-2 hover:text-ink-gray-9"
65 >
66 {{ __("ERPNext app") }}
67 </a>
68 {{ __("to enable this integration.") }}
69 </span>
70 </div>
71
72 <!-- Enabled & in sync -->
73 <div
74 v-else-if="erpnextIntegrationEnabled && inSync"
75 class="flex items-center gap-3 rounded-6 bg-surface-green-1 p-3"
76 >
77 <div class="grid size-7 shrink-0 place-items-center text-ink-green-6">
78 <LucideCheck class="h-4 w-4" />
79 </div>
80 <div class="flex min-w-0 flex-1 flex-col">
81 <span class="text-p-sm-medium text-ink-gray-8">
82 {{ __("Customers are in sync") }}
83 </span>
84 <span class="text-p-sm text-ink-gray-6">
85 {{
86 __(
87 "New and updated customers sync automatically between Opendesk and ERPNext."
88 )
89 }}
90 </span>
91 </div>
92 </div>
93
94 <!-- Enabled & not in sync -->
95 <div
96 v-else-if="erpnextIntegrationEnabled"
97 class="flex items-center gap-3 rounded-6 bg-surface-amber-1 p-3"
98 >
99 <div class="grid size-7 shrink-0 place-items-center text-ink-amber-6">
100 <LucideTriangleAlert class="h-4 w-4" />
101 </div>
102 <div class="flex min-w-0 flex-1 flex-col">
103 <span class="text-p-sm-medium text-ink-gray-8">
104 {{ __("Sync your existing customers") }}
105 </span>
106 <span class="text-p-sm text-ink-gray-6">
107 {{
108 __(
109 "Run an initial sync to reconcile existing records. New ones update automatically."
110 )
111 }}
112 </span>
113 </div>
114 <Button
115 theme="gray"
116 variant="subtle"
117 class="border border-outline-gray-2 bg-surface-base hover:bg-surface-base hover:border-outline-gray-3 active:bg-surface-gray-2 focus-visible:bg-surface-base focus-visible:ring-2 focus-visible:ring-outline-gray-3"
118 :loading="isSyncing || syncAction.loading"
119 :disabled="isSyncing || syncAction.loading"
120 :label="isSyncing ? __('Syncing…') : __('Sync now')"
121 @click="syncAction.submit()"
122 >
123 <template v-if="!isSyncing && !syncAction.loading" #prefix>
124 <LucideRefreshCw class="h-4 w-4" />
125 </template>
126 </Button>
127 </div>
128 </div>
129 </template>
130 </SettingsLayoutBase>
131</template>
132
133<script setup lang="ts">
134import { ErpnextIcon } from "@/components/icons";
135import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
136import { globalStore } from "@/stores/globalStore";
137import { capture } from "@/telemetry";
138import { __ } from "@/translation";
139import { Error } from "@/types";
140import {
141 Badge,
142 Button,
143 LoadingIndicator,
144 Switch,
145 createResource,
146 toast,
147} from "frappe-ui";
148import { computed, onMounted, onUnmounted, ref, watch } from "vue";
149import LucideCheck from "~icons/lucide/check";
150import LucideRefreshCw from "~icons/lucide/refresh-cw";
151import LucideTriangleAlert from "~icons/lucide/triangle-alert";
152
153const { $socket } = globalStore();
154
155const erpnextIntegrationEnabled = ref(false);
156const isSyncing = ref(false);
157
158const syncInfoResource = createResource({
159 url: "opendesk.integrations.erpnext.api.get_sync_info",
160 cache: ["Customers-Sync-Info"],
161 onSuccess(data: any) {
162 erpnextIntegrationEnabled.value = Boolean(data?.enabled);
163 },
164});
165
166const installed = window.apps?.includes("erpnext") ?? false;
167const inSync = computed(() => Boolean(syncInfoResource.data?.in_sync));
168
169const saveResource = createResource({
170 url: "frappe.client.set_value",
171 makeParams() {
172 return {
173 doctype: "ERPNext OD Settings",
174 name: "ERPNext OD Settings",
175 fieldname: { enabled: erpnextIntegrationEnabled.value },
176 };
177 },
178 onSuccess() {
179 if (erpnextIntegrationEnabled.value) capture("erpnext_integration_enabled");
180 syncInfoResource.reload();
181 toast.success(
182 erpnextIntegrationEnabled.value
183 ? __("ERPNext Integration is enabled")
184 : __("ERPNext Integration is disabled")
185 );
186 },
187 onError(error: Error) {
188 erpnextIntegrationEnabled.value = false;
189 const msg = error.exc_type
190 ? (error.messages || error.message || []).join(", ")
191 : error.message;
192 toast.error(msg);
193 },
194});
195
196const syncAction = createResource({
197 url: "opendesk.integrations.erpnext.api.sync_od_erpnext_customers",
198 onSuccess() {
199 toast.success(__("Sync started"));
200 },
201});
202
203function showNotInstalledToast() {
204 toast.error(
205 __(
206 "ERPNext is not installed on your site. Please install ERPNext to enable this setting"
207 )
208 );
209}
210
211function onSyncStart() {
212 isSyncing.value = true;
213}
214
215function onSyncEnd() {
216 isSyncing.value = false;
217 syncInfoResource.reload();
218 toast.success(__("Sync completed successfully"));
219}
220
221watch(erpnextIntegrationEnabled, (newVal) => {
222 if (syncInfoResource.data?.enabled === undefined) return;
223 if (newVal === Boolean(syncInfoResource.data.enabled)) return;
224 saveResource.submit();
225});
226
227onMounted(() => {
228 syncInfoResource.reload();
229 $socket.on("opendesk:erpnext-sync-start", onSyncStart);
230 $socket.on("opendesk:erpnext-sync-end", onSyncEnd);
231});
232
233onUnmounted(() => {
234 $socket.off("opendesk:erpnext-sync-start", onSyncStart);
235 $socket.off("opendesk:erpnext-sync-end", onSyncEnd);
236});
237</script>
238