@7365admin1/layer-common 1.11.41 → 1.11.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/components/AttachmentsViewer.vue +44 -15
- package/components/BulletinBoardForm.vue +11 -2
- package/components/BulletinBoardManagement.vue +2 -1
- package/components/ClientMain.vue +36 -22
- package/components/Input/DateTimePicker.vue +14 -1
- package/components/InvitationClientForm.vue +114 -60
- package/components/InvitationForm.vue +15 -4
- package/components/Layout/NavigationDrawer.vue +20 -19
- package/components/MemberMain.vue +9 -12
- package/components/TableMain.vue +1 -1
- package/components/VisitorManagement.vue +215 -628
- package/composables/useFile.ts +1 -1
- package/composables/useLocalAuth.ts +12 -0
- package/composables/useMember.ts +41 -14
- package/composables/useOrg.ts +49 -1
- package/composables/useUser.ts +46 -14
- package/package.json +1 -1
- package/types/nuxt-app.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<!-- Loading Skeleton -->
|
|
8
8
|
<template v-if="loading && files?.length > 0">
|
|
9
|
-
<v-card v-for="(file, index) in files" :key="index" height="50px" flat border="sm black"
|
|
9
|
+
<v-card v-for="(file, index) in files" :key="index" height="50px" flat border="sm black"
|
|
10
|
+
class="mb-2">
|
|
10
11
|
<v-skeleton-loader type="list-item" class="mb-2" />
|
|
11
12
|
</v-card>
|
|
12
13
|
</template>
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
</template>
|
|
57
58
|
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
58
59
|
<v-list-item-subtitle class="text-caption">{{ item.mimeType
|
|
59
|
-
|
|
60
|
+
}}</v-list-item-subtitle>
|
|
60
61
|
<template #append>
|
|
61
62
|
<v-icon size="16" color="grey-lighten-1">mdi-eye-outline</v-icon>
|
|
62
63
|
</template>
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
</template>
|
|
82
83
|
<v-list-item-title class="text-body-2">{{ item.file.name }}</v-list-item-title>
|
|
83
84
|
<v-list-item-subtitle class="text-caption">{{ item.mimeType
|
|
84
|
-
|
|
85
|
+
}}</v-list-item-subtitle>
|
|
85
86
|
<template #append>
|
|
86
87
|
<v-icon size="16" color="grey-lighten-1">mdi-open-in-new</v-icon>
|
|
87
88
|
</template>
|
|
@@ -200,26 +201,54 @@ watchEffect(async () => {
|
|
|
200
201
|
for (const item of (props.files || [])) {
|
|
201
202
|
// Handle building files format: { id, name }
|
|
202
203
|
if (item.id && item.name && !item.file) {
|
|
204
|
+
let file: File = new File([], item.name);
|
|
203
205
|
let mimeType = 'application/octet-stream';
|
|
206
|
+
|
|
204
207
|
try {
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// Try to get the actual file with proper MIME type detection from blob
|
|
209
|
+
const fileUrl = getFileUrl(item.id);
|
|
210
|
+
file = await urlToFile(fileUrl, item.name);
|
|
211
|
+
mimeType = file.type || 'application/octet-stream';
|
|
212
|
+
|
|
213
|
+
// If blob detection returned generic type, try API metadata or filename
|
|
214
|
+
if (mimeType === 'application/octet-stream') {
|
|
215
|
+
try {
|
|
216
|
+
const fileMeta = await getFileById(item.id) as any;
|
|
217
|
+
const apiMimeType = fileMeta?.data?.mimeType || fileMeta?.mimeType;
|
|
218
|
+
if (apiMimeType && apiMimeType !== 'application/octet-stream') {
|
|
219
|
+
mimeType = apiMimeType;
|
|
220
|
+
} else {
|
|
221
|
+
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
222
|
+
mimeType = inferMimeType(ext);
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Fallback to filename-based detection
|
|
226
|
+
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
227
|
+
mimeType = inferMimeType(ext);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Fallback: try API metadata
|
|
232
|
+
try {
|
|
233
|
+
const fileMeta = await getFileById(item.id) as any;
|
|
234
|
+
const apiMimeType = fileMeta?.data?.mimeType || fileMeta?.mimeType;
|
|
235
|
+
if (apiMimeType && apiMimeType !== 'application/octet-stream') {
|
|
236
|
+
mimeType = apiMimeType;
|
|
237
|
+
} else {
|
|
238
|
+
// Fallback to filename-based detection
|
|
239
|
+
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
240
|
+
mimeType = inferMimeType(ext);
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Final fallback: infer from filename
|
|
212
244
|
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
213
245
|
mimeType = inferMimeType(ext);
|
|
214
246
|
}
|
|
215
|
-
} catch {
|
|
216
|
-
// Fallback: infer MIME type from file name
|
|
217
|
-
const ext = item.name?.split('.').pop()?.toLowerCase();
|
|
218
|
-
mimeType = inferMimeType(ext);
|
|
219
247
|
}
|
|
248
|
+
|
|
220
249
|
processedFiles.push({
|
|
221
250
|
_id: item.id,
|
|
222
|
-
file
|
|
251
|
+
file,
|
|
223
252
|
preview: item.preview ?? false,
|
|
224
253
|
mimeType
|
|
225
254
|
});
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
|
|
53
53
|
<v-col cols="12">
|
|
54
54
|
<InputLabel class="text-capitalize" title="Start Date" required />
|
|
55
|
-
<InputDateTimePicker v-model:utc="bulletinForm.startDate" :rules="[validStartDateRule]"
|
|
55
|
+
<InputDateTimePicker v-model:utc="bulletinForm.startDate" :rules="[validStartDateRule]"/>
|
|
56
56
|
</v-col>
|
|
57
57
|
|
|
58
58
|
<v-col cols="12" v-if="!bulletinForm.noExpiration">
|
|
59
59
|
<InputLabel class="text-capitalize" title="End Date" required />
|
|
60
|
-
<InputDateTimePicker v-model:utc="bulletinForm.endDate" :rules="[validExpiryDateRule]" />
|
|
60
|
+
<InputDateTimePicker v-model:utc="bulletinForm.endDate" :rules="[validExpiryDateRule]" :min="today" />
|
|
61
61
|
</v-col>
|
|
62
62
|
|
|
63
63
|
<v-col cols="12">
|
|
@@ -103,6 +103,15 @@
|
|
|
103
103
|
</template>
|
|
104
104
|
|
|
105
105
|
<script setup lang="ts">
|
|
106
|
+
const today = computed(() => {
|
|
107
|
+
const d = new Date()
|
|
108
|
+
const yyyy = d.getFullYear()
|
|
109
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
110
|
+
const dd = String(d.getDate()).padStart(2, '0')
|
|
111
|
+
console.log('${yyyy}-${mm}-${dd}',`${yyyy}-${mm}-${dd}`)
|
|
112
|
+
return `${yyyy}-${mm}-${dd}`
|
|
113
|
+
})
|
|
114
|
+
|
|
106
115
|
const prop = defineProps({
|
|
107
116
|
siteId: {
|
|
108
117
|
type: String,
|
|
@@ -214,6 +214,7 @@ function eventStatusFormat(status: any) {
|
|
|
214
214
|
|
|
215
215
|
const tabOptions = [
|
|
216
216
|
{ name: "Active", status: "active" },
|
|
217
|
+
{ name: "Upcoming", status: "upcoming" },
|
|
217
218
|
{ name: "Expired", status: "expired" },
|
|
218
219
|
];
|
|
219
220
|
|
|
@@ -332,7 +333,7 @@ watch([searchInput], ([search]) => {
|
|
|
332
333
|
|
|
333
334
|
onMounted(() => {
|
|
334
335
|
const statusQuery = (useRoute()?.query?.status)
|
|
335
|
-
status.value = (statusQuery === "active" || statusQuery === "expired") ? statusQuery : "active"
|
|
336
|
+
status.value = (statusQuery === "active" || statusQuery === "upcoming" || statusQuery === "expired") ? statusQuery : "active"
|
|
336
337
|
})
|
|
337
338
|
|
|
338
339
|
</script>
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
<!-- ACTIONS -->
|
|
36
36
|
<template #actions>
|
|
37
|
-
<v-btn
|
|
38
|
-
@click="openInviteDialog">
|
|
37
|
+
<v-btn class="text-none" rounded="pill" variant="tonal" size="large" @click="openInviteDialog">
|
|
39
38
|
Invite Client
|
|
40
39
|
</v-btn>
|
|
41
40
|
</template>
|
|
@@ -71,7 +70,7 @@
|
|
|
71
70
|
</template>
|
|
72
71
|
|
|
73
72
|
<script setup lang="ts">
|
|
74
|
-
import { computed, onMounted, ref } from 'vue'
|
|
73
|
+
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
75
74
|
import useOrg from '../composables/useOrg'
|
|
76
75
|
import useVerification from '../composables/useVerification'
|
|
77
76
|
import useRole from '../composables/useRole'
|
|
@@ -82,7 +81,7 @@ import useSubscription from '../composables/useSubscription'
|
|
|
82
81
|
const tab = ref<'active' | 'suspended' | 'pending'>('active')
|
|
83
82
|
const dialog = ref(false)
|
|
84
83
|
|
|
85
|
-
const {
|
|
84
|
+
const { getAllV2 } = useOrg()
|
|
86
85
|
const { getVerifications, cancelUserInvitation } = useVerification()
|
|
87
86
|
const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
|
|
88
87
|
const { getByOrgId: getSubscriptionByOrgId } = useSubscription()
|
|
@@ -112,6 +111,7 @@ const APP_LIST = [
|
|
|
112
111
|
/* ================= HEADERS ================= */
|
|
113
112
|
const orgHeaders = [
|
|
114
113
|
{ title: 'Organization Name', key: 'name' },
|
|
114
|
+
{ title: 'Email', key: 'email' },
|
|
115
115
|
{ title: 'Sites', key: 'sites' },
|
|
116
116
|
{ title: 'Plan Type', key: 'planType' },
|
|
117
117
|
{ title: 'Billing Cycle', key: 'billingCycle' },
|
|
@@ -143,9 +143,22 @@ const pageRange = computed(() => {
|
|
|
143
143
|
})
|
|
144
144
|
|
|
145
145
|
/* ================= HELPERS ================= */
|
|
146
|
-
function formatDate(date?: string) {
|
|
147
|
-
if (
|
|
148
|
-
|
|
146
|
+
function formatDate(date?: string | Date) {
|
|
147
|
+
if (date == null || date === '') return '-'
|
|
148
|
+
const d = typeof date === 'object' && date instanceof Date ? date : new Date(date as string)
|
|
149
|
+
if (Number.isNaN(d.getTime())) return '-'
|
|
150
|
+
return d.toLocaleDateString('en-US')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Subscription document không luôn có `paidType`; fallback theo type/description. */
|
|
154
|
+
function planTypeLabel(sub: Record<string, any>) {
|
|
155
|
+
if (!sub) return '-'
|
|
156
|
+
if (sub.paidType) return String(sub.paidType)
|
|
157
|
+
if (sub.type === 'organization') return 'Organization'
|
|
158
|
+
if (sub.type === 'affiliate') return 'Affiliate'
|
|
159
|
+
const desc = String(sub.description || '').trim()
|
|
160
|
+
if (desc) return desc.length > 48 ? `${desc.slice(0, 45)}…` : desc
|
|
161
|
+
return '-'
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
async function resolveRoleName(id: string) {
|
|
@@ -159,10 +172,11 @@ async function resolveRoleName(id: string) {
|
|
|
159
172
|
const limit = ref(10)
|
|
160
173
|
/* ================= FETCH ================= */
|
|
161
174
|
async function fetchActive() {
|
|
162
|
-
const res = await
|
|
175
|
+
const res = await getAllV2({
|
|
163
176
|
page: page.value,
|
|
164
177
|
search: search.value,
|
|
165
178
|
limit: limit.value,
|
|
179
|
+
status: 'active',
|
|
166
180
|
})
|
|
167
181
|
|
|
168
182
|
const orgs = res?.data?.items || res?.items || []
|
|
@@ -189,7 +203,7 @@ async function fetchActive() {
|
|
|
189
203
|
const subscription = subRes?.data || subRes
|
|
190
204
|
|
|
191
205
|
if (subscription) {
|
|
192
|
-
planType = subscription
|
|
206
|
+
planType = planTypeLabel(subscription)
|
|
193
207
|
billingCycle = subscription?.billingCycle || '-'
|
|
194
208
|
subscriptionStart = formatDate(subscription?.createdAt)
|
|
195
209
|
subscriptionEnd = formatDate(subscription?.nextBillingDate)
|
|
@@ -207,6 +221,7 @@ async function fetchActive() {
|
|
|
207
221
|
subscriptionStart,
|
|
208
222
|
subscriptionEnd,
|
|
209
223
|
status: org?.status || 'active',
|
|
224
|
+
actions: '—',
|
|
210
225
|
}
|
|
211
226
|
})
|
|
212
227
|
)
|
|
@@ -225,10 +240,11 @@ async function fetchActive() {
|
|
|
225
240
|
}
|
|
226
241
|
|
|
227
242
|
async function fetchSuspended() {
|
|
228
|
-
const res = await
|
|
243
|
+
const res = await getAllV2({
|
|
229
244
|
page: page.value,
|
|
230
245
|
search: search.value,
|
|
231
|
-
|
|
246
|
+
limit: limit.value,
|
|
247
|
+
status: 'suspended',
|
|
232
248
|
})
|
|
233
249
|
const orgs = res?.data?.items || res?.items || []
|
|
234
250
|
|
|
@@ -252,7 +268,7 @@ async function fetchSuspended() {
|
|
|
252
268
|
const subRes = await getSubscriptionByOrgId(org._id)
|
|
253
269
|
const subscription = subRes?.data || subRes
|
|
254
270
|
if (subscription) {
|
|
255
|
-
planType = subscription
|
|
271
|
+
planType = planTypeLabel(subscription)
|
|
256
272
|
billingCycle = subscription?.billingCycle || '-'
|
|
257
273
|
subscriptionStart = formatDate(subscription?.createdAt)
|
|
258
274
|
subscriptionEnd = formatDate(subscription?.nextBillingDate)
|
|
@@ -262,7 +278,7 @@ async function fetchSuspended() {
|
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
return {
|
|
265
|
-
_id: org._id
|
|
281
|
+
_id: `${org._id}-${page.value}`,
|
|
266
282
|
...org,
|
|
267
283
|
sites: `${sitesCount} Sites`,
|
|
268
284
|
planType,
|
|
@@ -270,17 +286,17 @@ async function fetchSuspended() {
|
|
|
270
286
|
subscriptionStart,
|
|
271
287
|
subscriptionEnd,
|
|
272
288
|
status: org?.status || 'suspended',
|
|
289
|
+
actions: '—',
|
|
273
290
|
}
|
|
274
291
|
})
|
|
275
292
|
)
|
|
276
293
|
|
|
294
|
+
items.value = []
|
|
295
|
+
await nextTick()
|
|
277
296
|
items.value = [...mapped]
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const match = res?.pageRange?.match(/of\s+(\d+)/i)
|
|
282
|
-
|
|
283
|
-
totalItems.value = match ? Number(match[1]) : mapped.length
|
|
297
|
+
pages.value = res?.pages || 1
|
|
298
|
+
const match = res?.pageRange?.match(/of\s+(\d+)/i)
|
|
299
|
+
totalItems.value = match ? Number(match[1]) : mapped.length
|
|
284
300
|
}
|
|
285
301
|
|
|
286
302
|
async function fetchPending() {
|
|
@@ -322,9 +338,7 @@ async function fetchData() {
|
|
|
322
338
|
await fetchActive()
|
|
323
339
|
}
|
|
324
340
|
else if (tab.value === 'suspended') {
|
|
325
|
-
|
|
326
|
-
pages.value = 1
|
|
327
|
-
totalItems.value = 0
|
|
341
|
+
await fetchSuspended()
|
|
328
342
|
}
|
|
329
343
|
else {
|
|
330
344
|
await fetchPending()
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</template>
|
|
8
8
|
</v-text-field>
|
|
9
9
|
<div class="w-100 d-flex align-end ga-3 hidden-input">
|
|
10
|
-
<input ref="dateInput" :type="inputType" v-model="dateTime" />
|
|
10
|
+
<input ref="dateInput" :type="inputType" v-model="dateTime" :min="minValue" />
|
|
11
11
|
</div>
|
|
12
12
|
</div>
|
|
13
13
|
</template>
|
|
@@ -27,6 +27,10 @@ const prop = defineProps({
|
|
|
27
27
|
dateOnly: {
|
|
28
28
|
type: Boolean,
|
|
29
29
|
default: false
|
|
30
|
+
},
|
|
31
|
+
min: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: undefined
|
|
30
34
|
}
|
|
31
35
|
})
|
|
32
36
|
|
|
@@ -36,6 +40,15 @@ const dateTimeUTC = defineModel<string | null>('utc', { default: null }) // UTC
|
|
|
36
40
|
|
|
37
41
|
const dateTimeFormattedReadOnly = ref<string | null>(null)
|
|
38
42
|
const inputType = computed(() => (prop.dateOnly ? 'date' : 'datetime-local'))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const minValue = computed(() => {
|
|
46
|
+
if (!prop.min) return undefined
|
|
47
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(prop.min)) {
|
|
48
|
+
return prop.dateOnly ? prop.min : `${prop.min}T00:00`
|
|
49
|
+
}
|
|
50
|
+
return prop.min
|
|
51
|
+
})
|
|
39
52
|
const inputPlaceholder = computed(() => (
|
|
40
53
|
prop.placeholder || (prop.dateOnly ? 'MM/DD/YYYY' : 'DD/MM/YYYY, HH:MM AM/PM')
|
|
41
54
|
))
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
:rules="[requiredRule, emailRule]"
|
|
22
22
|
:loading="loading.verifyingEmail"
|
|
23
23
|
/>
|
|
24
|
+
<div v-if="existingUserMessage" class="text-subtitle-2 text-error mt-n3 mb-2">
|
|
25
|
+
{{ existingUserMessage }}
|
|
26
|
+
</div>
|
|
24
27
|
</v-col>
|
|
25
28
|
|
|
26
29
|
<!-- APP MULTI SELECT -->
|
|
@@ -37,7 +40,35 @@
|
|
|
37
40
|
density="comfortable"
|
|
38
41
|
:rules="[requiredRule]"
|
|
39
42
|
@update:model-value="handleUpdateApp"
|
|
40
|
-
|
|
43
|
+
>
|
|
44
|
+
<template #item="{ props: itemProps, item }">
|
|
45
|
+
<v-list-item
|
|
46
|
+
v-bind="itemProps"
|
|
47
|
+
:disabled="item.raw.disabled"
|
|
48
|
+
>
|
|
49
|
+
<template #prepend>
|
|
50
|
+
<v-checkbox-btn
|
|
51
|
+
:model-value="invite.app.includes(item.raw.value)"
|
|
52
|
+
:disabled="item.raw.disabled"
|
|
53
|
+
color="black"
|
|
54
|
+
density="comfortable"
|
|
55
|
+
/>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #title>
|
|
59
|
+
<span :class="item.raw.disabled ? 'text-disabled' : ''">
|
|
60
|
+
{{ item.raw.title }}
|
|
61
|
+
</span>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<template #subtitle>
|
|
65
|
+
<span v-if="item.raw.disabled" class="text-error text-caption">
|
|
66
|
+
Already a member
|
|
67
|
+
</span>
|
|
68
|
+
</template>
|
|
69
|
+
</v-list-item>
|
|
70
|
+
</template>
|
|
71
|
+
</v-autocomplete>
|
|
41
72
|
</v-col>
|
|
42
73
|
|
|
43
74
|
<!-- ROLE -->
|
|
@@ -65,21 +96,11 @@
|
|
|
65
96
|
/>
|
|
66
97
|
</v-col>
|
|
67
98
|
|
|
68
|
-
<!-- CREATE MORE -->
|
|
69
|
-
<v-col cols="12" class="mt-2">
|
|
70
|
-
<v-checkbox v-model="createMore" density="comfortable" hide-details>
|
|
71
|
-
<template #label>
|
|
72
|
-
<span class="text-subtitle-2 font-weight-bold">
|
|
73
|
-
Create more
|
|
74
|
-
</span>
|
|
75
|
-
</template>
|
|
76
|
-
</v-checkbox>
|
|
77
|
-
</v-col>
|
|
78
99
|
|
|
79
100
|
<!-- ERROR -->
|
|
80
101
|
<v-col cols="12" class="my-2 text-center">
|
|
81
102
|
<span class="text-subtitle-2 text-error">
|
|
82
|
-
{{
|
|
103
|
+
{{ submitError }}
|
|
83
104
|
</span>
|
|
84
105
|
</v-col>
|
|
85
106
|
|
|
@@ -116,14 +137,14 @@
|
|
|
116
137
|
</template>
|
|
117
138
|
|
|
118
139
|
<script setup lang="ts">
|
|
119
|
-
import { computed, ref, reactive, watchEffect } from 'vue'
|
|
140
|
+
import { computed, ref, reactive, watchEffect, watch } from 'vue'
|
|
120
141
|
import useCustomerSite from '../composables/useCustomerSite'
|
|
121
142
|
import useLocal from '../composables/useLocal'
|
|
122
143
|
import useRole from '../composables/useRole'
|
|
123
144
|
import useUser from '../composables/useUser'
|
|
124
145
|
import useUtils from '../composables/useUtils'
|
|
125
146
|
import useOrg from '../composables/useOrg'
|
|
126
|
-
import
|
|
147
|
+
import useMember from '../composables/useMember'
|
|
127
148
|
|
|
128
149
|
const APP = useRuntimeConfig().public.APP
|
|
129
150
|
|
|
@@ -156,7 +177,6 @@ const invite = ref({
|
|
|
156
177
|
isMemberInvite: false,
|
|
157
178
|
})
|
|
158
179
|
|
|
159
|
-
/* default app */
|
|
160
180
|
if (props.mode === "create") {
|
|
161
181
|
invite.value.app = props.app ? [props.app] : []
|
|
162
182
|
}
|
|
@@ -168,11 +188,14 @@ const APP_LIST = [
|
|
|
168
188
|
{ title: "Cleaning Services", value: "cleaning_services" },
|
|
169
189
|
{ title: "Mechanical & Electrical Services", value: "mechanical_electrical_services" },
|
|
170
190
|
{ title: "Pest Control Services", value: "pest_control_services" },
|
|
191
|
+
{ title: "Landscaping Services", value: "landscaping_services" },
|
|
171
192
|
{ title: "Pool Maintenance Services", value: "pool_maintenance_services" },
|
|
172
193
|
]
|
|
194
|
+
|
|
173
195
|
const { getByName } = useOrg()
|
|
174
196
|
const org = ref<any>(null)
|
|
175
|
-
|
|
197
|
+
|
|
198
|
+
watch(
|
|
176
199
|
() => props.org,
|
|
177
200
|
async (name) => {
|
|
178
201
|
if (!name) return
|
|
@@ -181,7 +204,14 @@ const org = ref<any>(null)
|
|
|
181
204
|
{ immediate: true }
|
|
182
205
|
)
|
|
183
206
|
|
|
184
|
-
const apps = computed(() =>
|
|
207
|
+
const apps = computed(() => {
|
|
208
|
+
return APP_LIST.map(app => {
|
|
209
|
+
const disabled = userAccess.value.some(
|
|
210
|
+
(a: any) => a.type === app.value
|
|
211
|
+
)
|
|
212
|
+
return { ...app, disabled }
|
|
213
|
+
})
|
|
214
|
+
})
|
|
185
215
|
|
|
186
216
|
/* ================= SITE ================= */
|
|
187
217
|
const { natureOfBusiness } = useLocal()
|
|
@@ -221,18 +251,14 @@ const { getRoles } = useRole()
|
|
|
221
251
|
|
|
222
252
|
const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
|
|
223
253
|
"roles",
|
|
224
|
-
() =>
|
|
225
|
-
getRoles({
|
|
226
|
-
org: org.value?._id,
|
|
227
|
-
type: props.app,
|
|
228
|
-
limit: 50,
|
|
229
|
-
}),
|
|
254
|
+
() => getRoles({ org: org.value?._id, type: props.app, limit: 50 }),
|
|
230
255
|
{ watch: [() => props.app] }
|
|
231
256
|
)
|
|
232
257
|
|
|
233
258
|
watchEffect(() => {
|
|
234
259
|
if (roleData.value) roles.value = roleData.value.items
|
|
235
260
|
})
|
|
261
|
+
|
|
236
262
|
watch(
|
|
237
263
|
() => org.value?._id,
|
|
238
264
|
async (id) => {
|
|
@@ -240,6 +266,7 @@ watch(
|
|
|
240
266
|
await refreshRoles()
|
|
241
267
|
}
|
|
242
268
|
)
|
|
269
|
+
|
|
243
270
|
/* ================= EVENTS ================= */
|
|
244
271
|
async function handleUpdateApp(value: string[]) {
|
|
245
272
|
invite.value.role = ""
|
|
@@ -252,59 +279,86 @@ function handleUpdateSite(id: string) {
|
|
|
252
279
|
invite.value.siteName = obj?.title || ""
|
|
253
280
|
}
|
|
254
281
|
|
|
255
|
-
/* =================
|
|
256
|
-
const { inviteUserClient } = useUser()
|
|
282
|
+
/* ================= EMAIL CHECK ================= */
|
|
283
|
+
const { inviteUserClient, getUserByEmail } = useUser()
|
|
284
|
+
const { getAllByUserId } = useMember()
|
|
257
285
|
const { requiredRule, emailRule } = useUtils()
|
|
258
286
|
|
|
259
|
-
const
|
|
260
|
-
const
|
|
287
|
+
const existingUserMessage = ref("")
|
|
288
|
+
const submitError = ref("")
|
|
261
289
|
const disable = ref(false)
|
|
290
|
+
const existingUser = ref<any>(null)
|
|
291
|
+
const userAccess = ref<any[]>([])
|
|
262
292
|
|
|
263
|
-
|
|
264
|
-
|
|
293
|
+
let timeout: any = null
|
|
294
|
+
|
|
295
|
+
watch(
|
|
296
|
+
() => invite.value.email,
|
|
297
|
+
(email) => {
|
|
298
|
+
clearTimeout(timeout)
|
|
299
|
+
existingUserMessage.value = ""
|
|
300
|
+
existingUser.value = null
|
|
301
|
+
userAccess.value = []
|
|
302
|
+
|
|
303
|
+
if (!email || email.length < 3) return
|
|
304
|
+
|
|
305
|
+
timeout = setTimeout(async () => {
|
|
306
|
+
await checkEmail(email)
|
|
307
|
+
}, 400)
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
async function checkEmail(email: string) {
|
|
312
|
+
loading.verifyingEmail = true
|
|
313
|
+
existingUserMessage.value = ""
|
|
265
314
|
|
|
266
315
|
try {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
role: invite.value.role,
|
|
275
|
-
// org: invite.value.org,
|
|
276
|
-
org: org.value?._id,
|
|
277
|
-
site: invite.value.site,
|
|
278
|
-
siteName: invite.value.siteName,
|
|
279
|
-
// isMemberInvite: invite.value.isMemberInvite,
|
|
280
|
-
})
|
|
281
|
-
)
|
|
282
|
-
)
|
|
316
|
+
const user = await getUserByEmail(email)
|
|
317
|
+
|
|
318
|
+
if (user?._id) {
|
|
319
|
+
existingUser.value = user
|
|
320
|
+
existingUserMessage.value = `User already exists: ${email}`
|
|
321
|
+
|
|
322
|
+
const memberRes = await getAllByUserId(user._id)
|
|
283
323
|
|
|
284
|
-
|
|
285
|
-
form.value?.reset()
|
|
286
|
-
invite.value = {
|
|
287
|
-
email: "",
|
|
288
|
-
app: [],
|
|
289
|
-
role: "",
|
|
290
|
-
org: org.value?._id || "",
|
|
291
|
-
site: "",
|
|
292
|
-
siteName: "",
|
|
293
|
-
isMemberInvite: false,
|
|
294
|
-
}
|
|
295
|
-
emit("success", false)
|
|
324
|
+
userAccess.value = memberRes?.items || []
|
|
296
325
|
} else {
|
|
297
|
-
|
|
326
|
+
existingUser.value = null
|
|
327
|
+
userAccess.value = []
|
|
298
328
|
}
|
|
329
|
+
} catch (e) {
|
|
330
|
+
existingUser.value = null
|
|
331
|
+
userAccess.value = []
|
|
332
|
+
} finally {
|
|
333
|
+
loading.verifyingEmail = false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ================= SUBMIT ================= */
|
|
338
|
+
async function submit() {
|
|
339
|
+
loading.submittingForm = true
|
|
340
|
+
submitError.value = ""
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
await inviteUserClient({
|
|
344
|
+
email: invite.value.email,
|
|
345
|
+
app: invite.value.app,
|
|
346
|
+
role: invite.value.role,
|
|
347
|
+
org: org.value?._id,
|
|
348
|
+
site: invite.value.site,
|
|
349
|
+
siteName: invite.value.siteName,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
emit("success", true)
|
|
299
353
|
|
|
300
354
|
} catch (e: any) {
|
|
301
|
-
|
|
355
|
+
submitError.value = e?.response?._data?.message || "Error"
|
|
302
356
|
} finally {
|
|
303
357
|
loading.submittingForm = false
|
|
304
358
|
}
|
|
305
359
|
}
|
|
306
|
-
|
|
307
360
|
function cancel() {
|
|
308
361
|
emit("cancel")
|
|
309
362
|
}
|
|
310
363
|
</script>
|
|
364
|
+
|
|
@@ -190,6 +190,10 @@ const props = defineProps({
|
|
|
190
190
|
hideApp: {
|
|
191
191
|
type: Boolean,
|
|
192
192
|
default: false
|
|
193
|
+
},
|
|
194
|
+
inviteMember: {
|
|
195
|
+
type: Boolean,
|
|
196
|
+
default: false
|
|
193
197
|
}
|
|
194
198
|
});
|
|
195
199
|
|
|
@@ -377,20 +381,27 @@ function handleUpdateSite(siteId: string) {
|
|
|
377
381
|
invite.value.siteName = obj?.title || "";
|
|
378
382
|
}
|
|
379
383
|
|
|
380
|
-
const { inviteUser } = useUser();
|
|
384
|
+
const { inviteUser, inviteMemberClient } = useUser();
|
|
381
385
|
|
|
382
386
|
async function submit() {
|
|
383
387
|
loading.submittingForm = true;
|
|
388
|
+
|
|
384
389
|
try {
|
|
385
|
-
|
|
390
|
+
if (props.inviteMember) {
|
|
391
|
+
await inviteMemberClient(invite.value);
|
|
392
|
+
} else {
|
|
393
|
+
await inviteUser(invite.value);
|
|
394
|
+
}
|
|
386
395
|
|
|
387
396
|
if (createMore.value) {
|
|
388
397
|
form.value?.reset();
|
|
389
398
|
resetInvite();
|
|
390
399
|
emit("success", false);
|
|
391
|
-
} else
|
|
400
|
+
} else {
|
|
401
|
+
emit("success", true);
|
|
402
|
+
}
|
|
392
403
|
} catch (error: any) {
|
|
393
|
-
message.value = error.response
|
|
404
|
+
message.value = error.response?._data?.message || error.message;
|
|
394
405
|
} finally {
|
|
395
406
|
loading.submittingForm = false;
|
|
396
407
|
}
|