@7365admin1/layer-common 1.11.33 → 1.11.34
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/ClientMain.vue +8 -5
- package/components/InvitationClientForm.vue +292 -0
- package/components/RolePermissionFormCreate.vue +195 -48
- package/components/RolePermissionMain.vue +1 -0
- package/components/ServiceProviderMain.vue +16 -10
- package/components/VisitorForm.vue +18 -3
- package/composables/useRole.ts +4 -2
- package/composables/useWebUsb.ts +11 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -54,16 +54,19 @@
|
|
|
54
54
|
|
|
55
55
|
<!-- DIALOG -->
|
|
56
56
|
<v-dialog v-model="dialog" max-width="500">
|
|
57
|
-
<
|
|
57
|
+
<InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
|
|
58
58
|
</v-dialog>
|
|
59
|
-
|
|
59
|
+
s
|
|
60
60
|
</v-row>
|
|
61
61
|
</template>
|
|
62
62
|
<script setup lang="ts">
|
|
63
|
-
|
|
64
|
-
import useRole from '@7365admin1/layer-common/composables/useRole'
|
|
65
|
-
import useVerification from '@7365admin1/layer-common/composables/useVerification'
|
|
63
|
+
|
|
66
64
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
65
|
+
import useOrg from '../composables/useOrg'
|
|
66
|
+
import useVerification from '../composables/useVerification'
|
|
67
|
+
import useRole from '../composables/useRole'
|
|
68
|
+
import InvitationClientForm from './InvitationClientForm.vue'
|
|
69
|
+
|
|
67
70
|
|
|
68
71
|
const tab = ref<'active' | 'suspended' | 'pending'>('active')
|
|
69
72
|
const dialog = ref(false)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
+
<span class="font-weight-bold text-h5">
|
|
6
|
+
{{ props.title }}
|
|
7
|
+
</span>
|
|
8
|
+
</v-row>
|
|
9
|
+
</v-toolbar>
|
|
10
|
+
|
|
11
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto">
|
|
12
|
+
<v-form v-model="validForm" ref="form" :disabled="disable">
|
|
13
|
+
<v-row no-gutters>
|
|
14
|
+
|
|
15
|
+
<!-- EMAIL -->
|
|
16
|
+
<v-col cols="12" class="mt-2">
|
|
17
|
+
<InputLabel title="Email" required />
|
|
18
|
+
<v-text-field
|
|
19
|
+
v-model="invite.email"
|
|
20
|
+
density="comfortable"
|
|
21
|
+
:rules="[requiredRule, emailRule]"
|
|
22
|
+
:loading="loading.verifyingEmail"
|
|
23
|
+
/>
|
|
24
|
+
</v-col>
|
|
25
|
+
|
|
26
|
+
<!-- APP MULTI SELECT -->
|
|
27
|
+
<v-col v-if="APP" cols="12" class="mt-2">
|
|
28
|
+
<InputLabel title="App" required />
|
|
29
|
+
<v-autocomplete
|
|
30
|
+
v-model="invite.app"
|
|
31
|
+
:items="apps"
|
|
32
|
+
item-title="title"
|
|
33
|
+
item-value="value"
|
|
34
|
+
multiple
|
|
35
|
+
chips
|
|
36
|
+
closable-chips
|
|
37
|
+
density="comfortable"
|
|
38
|
+
:rules="[requiredRule]"
|
|
39
|
+
@update:model-value="handleUpdateApp"
|
|
40
|
+
/>
|
|
41
|
+
</v-col>
|
|
42
|
+
|
|
43
|
+
<!-- ROLE -->
|
|
44
|
+
<v-col cols="12">
|
|
45
|
+
<InputLabel title="Role" />
|
|
46
|
+
<v-autocomplete
|
|
47
|
+
v-model="invite.role"
|
|
48
|
+
:items="roles"
|
|
49
|
+
item-title="name"
|
|
50
|
+
item-value="_id"
|
|
51
|
+
density="comfortable"
|
|
52
|
+
:rules="[requiredRule]"
|
|
53
|
+
/>
|
|
54
|
+
</v-col>
|
|
55
|
+
|
|
56
|
+
<!-- SITE -->
|
|
57
|
+
<v-col v-if="hasSite" cols="12">
|
|
58
|
+
<InputLabel title="Site" />
|
|
59
|
+
<v-autocomplete
|
|
60
|
+
v-model="invite.site"
|
|
61
|
+
:items="sites"
|
|
62
|
+
density="comfortable"
|
|
63
|
+
:rules="[requiredRule]"
|
|
64
|
+
@update:model-value="handleUpdateSite"
|
|
65
|
+
/>
|
|
66
|
+
</v-col>
|
|
67
|
+
|
|
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
|
+
|
|
79
|
+
<!-- ERROR -->
|
|
80
|
+
<v-col cols="12" class="my-2 text-center">
|
|
81
|
+
<span class="text-subtitle-2 text-error">
|
|
82
|
+
{{ message }}
|
|
83
|
+
</span>
|
|
84
|
+
</v-col>
|
|
85
|
+
|
|
86
|
+
</v-row>
|
|
87
|
+
</v-form>
|
|
88
|
+
</v-card-text>
|
|
89
|
+
|
|
90
|
+
<!-- ACTION -->
|
|
91
|
+
<v-toolbar density="compact">
|
|
92
|
+
<v-row no-gutters>
|
|
93
|
+
<v-col cols="6">
|
|
94
|
+
<v-btn block variant="text" class="text-none" size="48" @click="cancel">
|
|
95
|
+
Cancel
|
|
96
|
+
</v-btn>
|
|
97
|
+
</v-col>
|
|
98
|
+
|
|
99
|
+
<v-col cols="6">
|
|
100
|
+
<v-btn
|
|
101
|
+
block
|
|
102
|
+
variant="flat"
|
|
103
|
+
color="black"
|
|
104
|
+
class="text-none"
|
|
105
|
+
size="48"
|
|
106
|
+
:disabled="!validForm"
|
|
107
|
+
:loading="loading.submittingForm"
|
|
108
|
+
@click="submit"
|
|
109
|
+
>
|
|
110
|
+
Submit
|
|
111
|
+
</v-btn>
|
|
112
|
+
</v-col>
|
|
113
|
+
</v-row>
|
|
114
|
+
</v-toolbar>
|
|
115
|
+
</v-card>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup lang="ts">
|
|
119
|
+
import { computed, ref, reactive, watchEffect } from 'vue'
|
|
120
|
+
import useCustomerSite from '../composables/useCustomerSite'
|
|
121
|
+
import useLocal from '../composables/useLocal'
|
|
122
|
+
import { useLocalSetup } from '../composables/useLocalSetup'
|
|
123
|
+
import useRole from '../composables/useRole'
|
|
124
|
+
import useUser from '../composables/useUser'
|
|
125
|
+
import useUtils from '../composables/useUtils'
|
|
126
|
+
|
|
127
|
+
const APP = useRuntimeConfig().public.APP
|
|
128
|
+
|
|
129
|
+
/* ================= PROPS ================= */
|
|
130
|
+
const props = defineProps({
|
|
131
|
+
title: { type: String, default: "Invite Form" },
|
|
132
|
+
app: { type: String, default: "organization" },
|
|
133
|
+
org: { type: String, default: "" },
|
|
134
|
+
mode: { type: String, default: "create" },
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const emit = defineEmits(["cancel", "success"])
|
|
138
|
+
|
|
139
|
+
const validForm = ref(false)
|
|
140
|
+
const form = ref()
|
|
141
|
+
|
|
142
|
+
const loading = reactive({
|
|
143
|
+
submittingForm: false,
|
|
144
|
+
verifyingEmail: false,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/* ================= FORM ================= */
|
|
148
|
+
const invite = ref({
|
|
149
|
+
email: "",
|
|
150
|
+
app: [] as string[],
|
|
151
|
+
role: "",
|
|
152
|
+
org: props.org ?? "",
|
|
153
|
+
site: "",
|
|
154
|
+
siteName: "",
|
|
155
|
+
isMemberInvite: false,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
/* default app */
|
|
159
|
+
if (props.mode === "create") {
|
|
160
|
+
invite.value.app = props.app ? [props.app] : []
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ================= APP LIST ================= */
|
|
164
|
+
const APP_LIST = [
|
|
165
|
+
{ title: "Organization", value: "organization" },
|
|
166
|
+
{ title: "Security Agency", value: "security_agency" },
|
|
167
|
+
{ title: "Cleaning Services", value: "cleaning_services" },
|
|
168
|
+
{ title: "Property Management Agency", value: "property_management_agency" },
|
|
169
|
+
{ title: "Mechanical & Electrical Services", value: "mechanical_electrical_services" },
|
|
170
|
+
{ title: "Pest Control Services", value: "pest_control_services" },
|
|
171
|
+
{ title: "Landscaping Services", value: "landscaping_services" },
|
|
172
|
+
{ title: "Pool Maintenance Services", value: "pool_maintenance_services" },
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
const { orgNature } = useLocalSetup()
|
|
176
|
+
|
|
177
|
+
const apps = computed(() => APP_LIST)
|
|
178
|
+
|
|
179
|
+
/* ================= SITE ================= */
|
|
180
|
+
const { natureOfBusiness } = useLocal()
|
|
181
|
+
|
|
182
|
+
const hasSite = computed(() =>
|
|
183
|
+
natureOfBusiness.some(i => i.value === invite.value.app)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const sites = ref<any[]>([])
|
|
187
|
+
const { getAll: getAllCustomerSite } = useCustomerSite()
|
|
188
|
+
|
|
189
|
+
const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
|
|
190
|
+
"sites",
|
|
191
|
+
() => getAllCustomerSite({ org: props.org, limit: 50 })
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
watchEffect(() => {
|
|
195
|
+
if (siteData.value) {
|
|
196
|
+
sites.value = siteData.value.items.map((i: any) => ({
|
|
197
|
+
title: i.name,
|
|
198
|
+
value: i.site,
|
|
199
|
+
}))
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
watchEffect(() => {
|
|
204
|
+
if (hasSite.value) refreshSiteData()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
/* ================= ROLE ================= */
|
|
208
|
+
const roles = ref<any[]>([])
|
|
209
|
+
const { getRoles } = useRole()
|
|
210
|
+
|
|
211
|
+
const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
|
|
212
|
+
"roles",
|
|
213
|
+
() =>
|
|
214
|
+
getRoles({
|
|
215
|
+
org: props.org,
|
|
216
|
+
type: props.app,
|
|
217
|
+
limit: 50,
|
|
218
|
+
}),
|
|
219
|
+
{ watch: [() => props.app] }
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
watchEffect(() => {
|
|
223
|
+
if (roleData.value) roles.value = roleData.value.items
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
/* ================= EVENTS ================= */
|
|
227
|
+
async function handleUpdateApp(value: string[]) {
|
|
228
|
+
invite.value.role = ""
|
|
229
|
+
invite.value.site = ""
|
|
230
|
+
await refreshRoles()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleUpdateSite(id: string) {
|
|
234
|
+
const obj = sites.value.find(x => x.value === id)
|
|
235
|
+
invite.value.siteName = obj?.title || ""
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ================= SUBMIT ================= */
|
|
239
|
+
const { inviteUser } = useUser()
|
|
240
|
+
const { requiredRule, emailRule } = useUtils()
|
|
241
|
+
|
|
242
|
+
const message = ref("")
|
|
243
|
+
const createMore = ref(false)
|
|
244
|
+
const disable = ref(false)
|
|
245
|
+
|
|
246
|
+
async function submit() {
|
|
247
|
+
loading.submittingForm = true
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const apps = invite.value.app || []
|
|
251
|
+
|
|
252
|
+
await Promise.all(
|
|
253
|
+
apps.map(app =>
|
|
254
|
+
inviteUser({
|
|
255
|
+
email: invite.value.email,
|
|
256
|
+
app,
|
|
257
|
+
role: invite.value.role,
|
|
258
|
+
org: invite.value.org,
|
|
259
|
+
site: invite.value.site,
|
|
260
|
+
siteName: invite.value.siteName,
|
|
261
|
+
isMemberInvite: invite.value.isMemberInvite,
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if (createMore.value) {
|
|
267
|
+
form.value?.reset()
|
|
268
|
+
invite.value = {
|
|
269
|
+
email: "",
|
|
270
|
+
app: [],
|
|
271
|
+
role: "",
|
|
272
|
+
org: props.org,
|
|
273
|
+
site: "",
|
|
274
|
+
siteName: "",
|
|
275
|
+
isMemberInvite: false,
|
|
276
|
+
}
|
|
277
|
+
emit("success", false)
|
|
278
|
+
} else {
|
|
279
|
+
emit("success", true)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
} catch (e: any) {
|
|
283
|
+
message.value = e?.response?._data?.message || "Error"
|
|
284
|
+
} finally {
|
|
285
|
+
loading.submittingForm = false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function cancel() {
|
|
290
|
+
emit("cancel")
|
|
291
|
+
}
|
|
292
|
+
</script>
|
|
@@ -2,45 +2,135 @@
|
|
|
2
2
|
<v-card width="100%">
|
|
3
3
|
<v-toolbar>
|
|
4
4
|
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
-
<span class="font-weight-bold text-h5">
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<span class="font-weight-bold text-h5">Create Role</span>
|
|
6
|
+
<v-spacer />
|
|
7
|
+
<v-btn icon density="comfortable" @click="cancel">
|
|
8
|
+
<v-icon>mdi-close</v-icon>
|
|
9
|
+
</v-btn>
|
|
8
10
|
</v-row>
|
|
9
11
|
</v-toolbar>
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
<v-card-text style="max-height: 75vh; overflow-y: auto">
|
|
11
14
|
<v-form v-model="validForm" :disabled="disable">
|
|
12
15
|
<v-row no-gutters>
|
|
13
16
|
<v-col cols="12" class="mt-2">
|
|
14
17
|
<v-row no-gutters>
|
|
15
|
-
<InputLabel
|
|
18
|
+
<InputLabel title="Role Name" required />
|
|
16
19
|
<v-col cols="12">
|
|
17
20
|
<v-text-field
|
|
18
21
|
v-model="name"
|
|
19
22
|
density="comfortable"
|
|
20
23
|
:rules="[requiredRule]"
|
|
21
|
-
|
|
24
|
+
/>
|
|
25
|
+
</v-col>
|
|
26
|
+
</v-row>
|
|
27
|
+
</v-col>
|
|
28
|
+
|
|
29
|
+
<v-col cols="12" class="mt-2">
|
|
30
|
+
<v-row no-gutters>
|
|
31
|
+
<InputLabel title="Site" />
|
|
32
|
+
<v-col cols="12">
|
|
33
|
+
<v-text-field
|
|
34
|
+
:model-value="siteName"
|
|
35
|
+
density="comfortable"
|
|
36
|
+
readonly
|
|
37
|
+
disabled
|
|
38
|
+
:loading="loadSite"
|
|
39
|
+
/>
|
|
22
40
|
</v-col>
|
|
23
41
|
</v-row>
|
|
24
42
|
</v-col>
|
|
25
43
|
|
|
26
|
-
<v-col cols="12">
|
|
27
|
-
<
|
|
28
|
-
v-model="
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
<v-col cols="12" class="mt-1">
|
|
45
|
+
<v-radio-group
|
|
46
|
+
v-model="platform"
|
|
47
|
+
inline
|
|
48
|
+
color="primary"
|
|
49
|
+
hide-details
|
|
50
|
+
>
|
|
51
|
+
<v-radio label="Website" value="web" />
|
|
52
|
+
<v-radio label="Mobile" value="mobile" />
|
|
53
|
+
</v-radio-group>
|
|
34
54
|
</v-col>
|
|
35
55
|
|
|
36
|
-
<v-col cols="12" class="mt-
|
|
37
|
-
<v-
|
|
38
|
-
<template
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
<v-col cols="12" class="mt-3">
|
|
57
|
+
<v-card variant="outlined" border="thin" rounded="lg">
|
|
58
|
+
<template
|
|
59
|
+
v-for="(
|
|
60
|
+
actions, resourceKey, resourceIndex
|
|
61
|
+
) in props.permissions"
|
|
62
|
+
:key="resourceKey"
|
|
63
|
+
>
|
|
64
|
+
<v-divider v-if="resourceIndex > 0" />
|
|
65
|
+
|
|
66
|
+
<!-- Category row -->
|
|
67
|
+
<v-list-item density="compact" :ripple="false">
|
|
68
|
+
<template #prepend>
|
|
69
|
+
<v-chip class="mr-3" size="small" label>
|
|
70
|
+
{{ selectedActionCount(String(resourceKey)) }}
|
|
71
|
+
</v-chip>
|
|
72
|
+
</template>
|
|
73
|
+
<v-list-item-title>
|
|
74
|
+
<span
|
|
75
|
+
class="text-capitalize text-subtitle-2 font-weight-medium"
|
|
76
|
+
>
|
|
77
|
+
{{ String(resourceKey).replace(/-/g, " ") }}
|
|
78
|
+
</span>
|
|
79
|
+
</v-list-item-title>
|
|
80
|
+
<template #append>
|
|
81
|
+
<v-switch
|
|
82
|
+
:model-value="isCategoryEnabled(String(resourceKey))"
|
|
83
|
+
@update:model-value="
|
|
84
|
+
toggleCategory(String(resourceKey), $event)
|
|
85
|
+
"
|
|
86
|
+
color="green"
|
|
87
|
+
density="compact"
|
|
88
|
+
hide-details
|
|
89
|
+
inset
|
|
90
|
+
/>
|
|
91
|
+
</template>
|
|
92
|
+
</v-list-item>
|
|
93
|
+
|
|
94
|
+
<!-- Expanded checkboxes when toggled on -->
|
|
95
|
+
<template v-if="isCategoryEnabled(String(resourceKey))">
|
|
96
|
+
<template
|
|
97
|
+
v-for="(action, actionKey) in actions"
|
|
98
|
+
:key="actionKey"
|
|
99
|
+
>
|
|
100
|
+
<v-divider />
|
|
101
|
+
<v-list-item density="compact" :ripple="false">
|
|
102
|
+
<template #prepend>
|
|
103
|
+
<v-checkbox-btn
|
|
104
|
+
:model-value="
|
|
105
|
+
selectedPermissions.includes(
|
|
106
|
+
`${String(resourceKey)}:${String(actionKey)}`
|
|
107
|
+
)
|
|
108
|
+
"
|
|
109
|
+
@update:model-value="
|
|
110
|
+
toggleAction(
|
|
111
|
+
String(resourceKey),
|
|
112
|
+
String(actionKey),
|
|
113
|
+
$event
|
|
114
|
+
)
|
|
115
|
+
"
|
|
116
|
+
color="primary"
|
|
117
|
+
density="compact"
|
|
118
|
+
class="mr-2"
|
|
119
|
+
/>
|
|
120
|
+
</template>
|
|
121
|
+
<v-list-item-title
|
|
122
|
+
class="text-subtitle-2 font-weight-medium text-capitalize"
|
|
123
|
+
>
|
|
124
|
+
{{ String(actionKey).replace(/-/g, " ") }}
|
|
125
|
+
</v-list-item-title>
|
|
126
|
+
<v-list-item-subtitle class="text-caption">
|
|
127
|
+
{{ action.description }}
|
|
128
|
+
</v-list-item-subtitle>
|
|
129
|
+
</v-list-item>
|
|
130
|
+
</template>
|
|
131
|
+
</template>
|
|
42
132
|
</template>
|
|
43
|
-
</v-
|
|
133
|
+
</v-card>
|
|
44
134
|
</v-col>
|
|
45
135
|
|
|
46
136
|
<v-col cols="12" class="my-2">
|
|
@@ -58,28 +148,30 @@
|
|
|
58
148
|
</v-form>
|
|
59
149
|
</v-card-text>
|
|
60
150
|
|
|
61
|
-
<v-toolbar>
|
|
62
|
-
<v-row
|
|
63
|
-
<v-col cols="6">
|
|
151
|
+
<v-toolbar class="pa-0" density="compact">
|
|
152
|
+
<v-row no-gutters>
|
|
153
|
+
<v-col cols="6" class="pa-0">
|
|
64
154
|
<v-btn
|
|
65
155
|
block
|
|
66
156
|
variant="text"
|
|
67
157
|
class="text-none"
|
|
68
158
|
size="large"
|
|
159
|
+
height="48"
|
|
69
160
|
@click="cancel"
|
|
70
161
|
>
|
|
71
162
|
Cancel
|
|
72
163
|
</v-btn>
|
|
73
164
|
</v-col>
|
|
74
165
|
|
|
75
|
-
<v-col cols="6">
|
|
166
|
+
<v-col cols="6" class="pa-0">
|
|
76
167
|
<v-btn
|
|
77
168
|
block
|
|
78
169
|
variant="flat"
|
|
79
170
|
color="black"
|
|
80
|
-
class="text-none"
|
|
81
|
-
|
|
171
|
+
class="text-none font-weight-bold rounded-0"
|
|
172
|
+
height="48"
|
|
82
173
|
:disabled="!validForm"
|
|
174
|
+
:loading="disable"
|
|
83
175
|
@click="submit"
|
|
84
176
|
>
|
|
85
177
|
Submit
|
|
@@ -91,14 +183,11 @@
|
|
|
91
183
|
</template>
|
|
92
184
|
|
|
93
185
|
<script setup lang="ts">
|
|
94
|
-
import useRole from
|
|
95
|
-
import useUtils from
|
|
186
|
+
import useRole from "../composables/useRole";
|
|
187
|
+
import useUtils from "../composables/useUtils";
|
|
188
|
+
import useSite from "../composables/useSite";
|
|
96
189
|
|
|
97
190
|
const props = defineProps({
|
|
98
|
-
title: {
|
|
99
|
-
type: String,
|
|
100
|
-
default: "Role Permission Form",
|
|
101
|
-
},
|
|
102
191
|
permissions: {
|
|
103
192
|
type: Object,
|
|
104
193
|
default: () => ({}),
|
|
@@ -107,6 +196,10 @@ const props = defineProps({
|
|
|
107
196
|
type: String,
|
|
108
197
|
default: "",
|
|
109
198
|
},
|
|
199
|
+
siteId: {
|
|
200
|
+
type: String,
|
|
201
|
+
default: "",
|
|
202
|
+
},
|
|
110
203
|
type: {
|
|
111
204
|
type: String,
|
|
112
205
|
default: "app",
|
|
@@ -116,36 +209,89 @@ const props = defineProps({
|
|
|
116
209
|
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
117
210
|
|
|
118
211
|
const validForm = ref(false);
|
|
119
|
-
|
|
120
212
|
const name = ref("");
|
|
121
|
-
const selectedPermissions = ref([]);
|
|
122
|
-
const
|
|
213
|
+
const selectedPermissions = ref<Array<string>>([]);
|
|
214
|
+
const platform = ref("web");
|
|
123
215
|
const disable = ref(false);
|
|
216
|
+
const message = ref("");
|
|
124
217
|
|
|
125
218
|
const { requiredRule, requireListRule } = useUtils();
|
|
219
|
+
const { createRole } = useRole();
|
|
220
|
+
const { getSiteById } = useSite();
|
|
126
221
|
|
|
127
|
-
const
|
|
222
|
+
const enabledCategories = ref<Set<string>>(new Set());
|
|
128
223
|
|
|
129
|
-
|
|
224
|
+
function selectedActionCount(resource: string) {
|
|
225
|
+
return selectedPermissions.value.filter((p) => p.startsWith(`${resource}:`))
|
|
226
|
+
.length;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isCategoryEnabled(resource: string) {
|
|
230
|
+
return enabledCategories.value.has(resource);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toggleCategory(resource: string, value: boolean | null) {
|
|
234
|
+
const updated = new Set(enabledCategories.value);
|
|
235
|
+
if (value) {
|
|
236
|
+
updated.add(resource);
|
|
237
|
+
} else {
|
|
238
|
+
updated.delete(resource);
|
|
239
|
+
|
|
240
|
+
selectedPermissions.value = selectedPermissions.value.filter(
|
|
241
|
+
(p) => !p.startsWith(`${resource}:`)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
enabledCategories.value = updated;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toggleAction(resource: string, action: string, value: boolean | null) {
|
|
248
|
+
const key = `${resource}:${action}`;
|
|
249
|
+
if (value) {
|
|
250
|
+
if (!selectedPermissions.value.includes(key)) {
|
|
251
|
+
selectedPermissions.value = [...selectedPermissions.value, key];
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
selectedPermissions.value = selectedPermissions.value.filter(
|
|
255
|
+
(p) => p !== key
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const siteName = ref("");
|
|
261
|
+
const loadSite = ref(false);
|
|
262
|
+
|
|
263
|
+
watch(
|
|
264
|
+
() => props.siteId,
|
|
265
|
+
async (id) => {
|
|
266
|
+
if (!id) {
|
|
267
|
+
siteName.value = "";
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
loadSite.value = true;
|
|
271
|
+
try {
|
|
272
|
+
const site = await getSiteById(id);
|
|
273
|
+
siteName.value = site?.name ?? id;
|
|
274
|
+
} catch {
|
|
275
|
+
siteName.value = id;
|
|
276
|
+
} finally {
|
|
277
|
+
loadSite.value = false;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{ immediate: true }
|
|
281
|
+
);
|
|
130
282
|
|
|
131
283
|
async function submit() {
|
|
132
284
|
disable.value = true;
|
|
133
285
|
try {
|
|
286
|
+
const platformValue = platform.value === "web" ? "website" : platform.value;
|
|
134
287
|
await createRole({
|
|
135
288
|
name: name.value,
|
|
136
289
|
permissions: selectedPermissions.value,
|
|
137
290
|
type: props.type,
|
|
138
291
|
org: props.org,
|
|
292
|
+
site: props.siteId,
|
|
293
|
+
platform: platformValue,
|
|
139
294
|
});
|
|
140
|
-
|
|
141
|
-
if (createMore.value) {
|
|
142
|
-
name.value = "";
|
|
143
|
-
selectedPermissions.value = [];
|
|
144
|
-
message.value = "";
|
|
145
|
-
emit("success:create-more");
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
295
|
emit("success");
|
|
150
296
|
} catch (error: any) {
|
|
151
297
|
message.value = error.response._data.message;
|
|
@@ -157,7 +303,8 @@ async function submit() {
|
|
|
157
303
|
function cancel() {
|
|
158
304
|
name.value = "";
|
|
159
305
|
selectedPermissions.value = [];
|
|
160
|
-
|
|
306
|
+
enabledCategories.value = new Set();
|
|
307
|
+
platform.value = "web";
|
|
161
308
|
message.value = "";
|
|
162
309
|
emit("cancel");
|
|
163
310
|
}
|
|
@@ -327,7 +327,8 @@
|
|
|
327
327
|
</template>
|
|
328
328
|
|
|
329
329
|
<script lang="ts" setup>
|
|
330
|
-
import useServiceProvider from
|
|
330
|
+
import useServiceProvider from "../composables/useServiceProvider";
|
|
331
|
+
import { errorConverter } from "../utils/data";
|
|
331
332
|
|
|
332
333
|
const props = defineProps({
|
|
333
334
|
orgId: {
|
|
@@ -420,7 +421,7 @@ const serviceProvider = ref({
|
|
|
420
421
|
const {
|
|
421
422
|
getAll: getAllServiceProvider,
|
|
422
423
|
add: addServiceProvider,
|
|
423
|
-
createServiceProviderInvite
|
|
424
|
+
createServiceProviderInvite,
|
|
424
425
|
} = useServiceProvider();
|
|
425
426
|
|
|
426
427
|
const { getSiteById } = useSite();
|
|
@@ -440,6 +441,7 @@ const {
|
|
|
440
441
|
status: getAllReqStatus,
|
|
441
442
|
} = await useLazyAsyncData("get-all-service-providers", () =>
|
|
442
443
|
getAllServiceProvider({
|
|
444
|
+
page: page.value,
|
|
443
445
|
siteId: props.siteId,
|
|
444
446
|
search: searchText.value || "",
|
|
445
447
|
})
|
|
@@ -522,13 +524,18 @@ async function submitServiceProviderAdd() {
|
|
|
522
524
|
try {
|
|
523
525
|
console.log("serviceProvider.value");
|
|
524
526
|
console.log(serviceProvider.value);
|
|
525
|
-
await addServiceProvider(serviceProvider.value);
|
|
527
|
+
const result = await addServiceProvider(serviceProvider.value);
|
|
526
528
|
await setServiceProvider({ mode: "add", dialog: false });
|
|
527
529
|
await _getAllServiceProvider();
|
|
530
|
+
|
|
531
|
+
if (result?.id) {
|
|
532
|
+
showMessage(result?.message, "success");
|
|
533
|
+
}
|
|
528
534
|
} catch (error: any) {
|
|
529
535
|
messageServiceProvider.value =
|
|
530
536
|
error?.response?._data?.message ??
|
|
531
537
|
"An error occurred while adding the service provider.";
|
|
538
|
+
showMessage(errorConverter(error), "error");
|
|
532
539
|
} finally {
|
|
533
540
|
disableServiceProvider.value = false;
|
|
534
541
|
}
|
|
@@ -539,14 +546,13 @@ async function submitServiceProviderInvite() {
|
|
|
539
546
|
messageServiceProvider.value = "";
|
|
540
547
|
try {
|
|
541
548
|
const payload = {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
};
|
|
547
|
-
|
|
549
|
+
email: serviceProvider.value.email?.trim(),
|
|
550
|
+
orgId: props.orgId,
|
|
551
|
+
siteId: props.siteId,
|
|
552
|
+
siteName: site.value?.name || props.siteName,
|
|
553
|
+
};
|
|
548
554
|
|
|
549
|
-
await createServiceProviderInvite(payload);
|
|
555
|
+
await createServiceProviderInvite(payload);
|
|
550
556
|
if (createMoreServiceProvider.value) {
|
|
551
557
|
serviceProvider.value.email = "";
|
|
552
558
|
} else {
|
|
@@ -951,9 +951,16 @@ async function submit() {
|
|
|
951
951
|
});
|
|
952
952
|
|
|
953
953
|
if (passType.value === "QR") {
|
|
954
|
+
console.log("[QR Print] passRes:", passRes);
|
|
954
955
|
const accessCards: any[] = (passRes as any)?.accessCards ?? [];
|
|
956
|
+
console.log("[QR Print] accessCards:", accessCards);
|
|
955
957
|
const printer = entryPassSettings.value?.data?.settings?.printer;
|
|
956
|
-
|
|
958
|
+
console.log("[QR Print] printer config:", printer);
|
|
959
|
+
if (!printer?.vendorId || !printer?.productId) {
|
|
960
|
+
console.warn("[QR Print] Aborted: no printer configured.");
|
|
961
|
+
} else if (!accessCards.length) {
|
|
962
|
+
console.warn("[QR Print] Aborted: accessCards is empty.");
|
|
963
|
+
} else {
|
|
957
964
|
const vendorId = parseInt(printer.vendorId);
|
|
958
965
|
const productId = parseInt(printer.productId);
|
|
959
966
|
const companyName = visitor.company || "";
|
|
@@ -961,13 +968,20 @@ async function submit() {
|
|
|
961
968
|
const levelLabel = levelsArray.value.find((l: any) => l.value === visitor.level)?.title || "";
|
|
962
969
|
const unitLabel = unitsArray.value.find((u: any) => u.value === visitor.unit)?.title || "";
|
|
963
970
|
const address = [blockLabel, levelLabel, unitLabel].filter(Boolean).join("/");
|
|
971
|
+
console.log("[QR Print] Printing", accessCards.length, "card(s). vendorId:", vendorId, "productId:", productId, "address:", address);
|
|
964
972
|
|
|
965
973
|
await nextTick();
|
|
966
974
|
for (const qrCode of accessCards) {
|
|
975
|
+
console.log("[QR Print] Signing QR for card:", qrCode._id);
|
|
967
976
|
const signed = await signQr({ cardId: qrCode._id, purpose: "vms" });
|
|
968
977
|
const qrData = signed?.data;
|
|
969
|
-
|
|
970
|
-
|
|
978
|
+
console.log("[QR Print] signQr result:", signed, "| qrData:", qrData);
|
|
979
|
+
if (!qrData) {
|
|
980
|
+
console.warn("[QR Print] Skipping card", qrCode._id, "— signQr returned no data.");
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
console.log("[QR Print] Sending to printer — accessLevel:", qrCode.accessLevel, "liftLevel:", qrCode.liftAccessLevel);
|
|
984
|
+
const printResult = await testConnection(
|
|
971
985
|
vendorId,
|
|
972
986
|
productId,
|
|
973
987
|
true,
|
|
@@ -977,6 +991,7 @@ async function submit() {
|
|
|
977
991
|
companyName,
|
|
978
992
|
address,
|
|
979
993
|
);
|
|
994
|
+
console.log("[QR Print] Print result for card", qrCode._id, ":", printResult);
|
|
980
995
|
}
|
|
981
996
|
}
|
|
982
997
|
}
|
package/composables/useRole.ts
CHANGED
|
@@ -2,16 +2,18 @@ import useMember from "./useMember";
|
|
|
2
2
|
|
|
3
3
|
export default function useRole() {
|
|
4
4
|
function createRole(
|
|
5
|
-
{ name, permissions, type, org } = {} as {
|
|
5
|
+
{ name, permissions, type, org, site, platform } = {} as {
|
|
6
6
|
name: string;
|
|
7
7
|
permissions: Array<string>;
|
|
8
8
|
type: string;
|
|
9
9
|
org?: string;
|
|
10
|
+
site?: string;
|
|
11
|
+
platform?: string;
|
|
10
12
|
}
|
|
11
13
|
) {
|
|
12
14
|
return useNuxtApp().$api("/api/roles", {
|
|
13
15
|
method: "POST",
|
|
14
|
-
body: { name, permissions, type, org },
|
|
16
|
+
body: { name, permissions, type, org, site, platform },
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
package/composables/useWebUsb.ts
CHANGED
|
@@ -99,17 +99,21 @@ export default function useWebUsb() {
|
|
|
99
99
|
}
|
|
100
100
|
try {
|
|
101
101
|
let devices = await navigator.usb.getDevices();
|
|
102
|
+
console.log("[WebUSB] Available devices:", devices.map((d: USBDevice) => `${d.vendorId}:${d.productId}`));
|
|
102
103
|
let device = devices.find(
|
|
103
104
|
(d: USBDevice) => d.vendorId === vendorId && d.productId === productId,
|
|
104
105
|
);
|
|
105
106
|
if (!device) {
|
|
107
|
+
console.log("[WebUSB] Device not in granted list, requesting permission for", vendorId, productId);
|
|
106
108
|
device = await navigator.usb.requestDevice({
|
|
107
109
|
filters: [{ vendorId, productId }],
|
|
108
110
|
});
|
|
109
111
|
}
|
|
112
|
+
console.log("[WebUSB] Opening device:", device.vendorId, device.productId);
|
|
110
113
|
await device.open();
|
|
111
114
|
await device.selectConfiguration(1);
|
|
112
115
|
await device.claimInterface(0);
|
|
116
|
+
console.log("[WebUSB] Device opened and interface claimed.");
|
|
113
117
|
return device;
|
|
114
118
|
} catch (error) {
|
|
115
119
|
console.error("Error connecting to USB device:", error);
|
|
@@ -349,12 +353,14 @@ export default function useWebUsb() {
|
|
|
349
353
|
qrSubText?: string,
|
|
350
354
|
) => {
|
|
351
355
|
try {
|
|
356
|
+
console.log("[WebUSB] printQrCode start — doorLevel:", doorLevel, "liftLevel:", liftLevel, "company:", companyName, "address:", address);
|
|
352
357
|
const usbInterface = device.configuration.interfaces[0];
|
|
353
358
|
const alternate = usbInterface.alternates[0];
|
|
354
359
|
const outputEndpoint = alternate.endpoints.find(
|
|
355
360
|
(ep: any) => ep.direction === "out",
|
|
356
361
|
);
|
|
357
362
|
if (!outputEndpoint) throw new Error("No output endpoint found");
|
|
363
|
+
console.log("[WebUSB] Using endpoint:", outputEndpoint.endpointNumber);
|
|
358
364
|
|
|
359
365
|
const canvas = await createReceiptLayout(
|
|
360
366
|
urlImage,
|
|
@@ -365,16 +371,20 @@ export default function useWebUsb() {
|
|
|
365
371
|
qrHeader,
|
|
366
372
|
qrSubText,
|
|
367
373
|
);
|
|
374
|
+
console.log("[WebUSB] Canvas created:", canvas.width, "x", canvas.height);
|
|
368
375
|
const rasterData = canvasToRaster(canvas);
|
|
376
|
+
console.log("[WebUSB] Raster data size:", rasterData.byteLength, "bytes");
|
|
369
377
|
|
|
370
378
|
await device.transferOut(outputEndpoint.endpointNumber, rasterData.buffer);
|
|
379
|
+
console.log("[WebUSB] Raster data sent.");
|
|
371
380
|
|
|
372
381
|
const CUT = new Uint8Array([0x1d, 0x56, 0x41, 0x10]);
|
|
373
382
|
await device.transferOut(outputEndpoint.endpointNumber, CUT.buffer);
|
|
383
|
+
console.log("[WebUSB] Cut command sent.");
|
|
374
384
|
|
|
375
385
|
return { success: true, message: "QR print sent successfully" };
|
|
376
386
|
} catch (error: any) {
|
|
377
|
-
console.error("
|
|
387
|
+
console.error("[WebUSB] printQrCode error:", error);
|
|
378
388
|
return { success: false, error: error.message };
|
|
379
389
|
}
|
|
380
390
|
};
|