@7365admin1/layer-common 1.11.32 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.34
4
+
5
+ ### Patch Changes
6
+
7
+ - 299c4fa: Update layer-common version
8
+
9
+ ## 1.11.33
10
+
11
+ ### Patch Changes
12
+
13
+ - ed63329: Update Layer-common version
14
+
3
15
  ## 1.11.32
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,231 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+
4
+ <!-- MAIN TABLE -->
5
+ <v-col cols="12">
6
+ <TableMain :headers="currentHeaders" :items="currentItems" :loading="loadingState" :page="currentPage"
7
+ :pages="currentPages" :pageRange="pageRange" :extension-height="120" :offset="300" show-header
8
+ @refresh="handleRefresh" @update:page="handleUpdatePage">
9
+
10
+ <!-- EXTENSION -->
11
+ <template #extension>
12
+ <v-row no-gutters class="w-100 d-flex flex-column ga-2 pt-2">
13
+
14
+ <v-tabs v-model="tab" class="w-100" height="32" @update:model-value="onTabChange">
15
+ <v-tab value="active">Active</v-tab>
16
+ <v-tab value="suspended">Suspended</v-tab>
17
+ <v-tab value="pending">Pending Invitation</v-tab>
18
+ </v-tabs>
19
+
20
+ <div class="px-3 py-2 w-100">
21
+ <v-text-field v-model="search" density="compact" placeholder="Search" clearable max-width="300"
22
+ append-inner-icon="mdi-magnify" hide-details @update:model-value="handleSearch" />
23
+ </div>
24
+
25
+ </v-row>
26
+ </template>
27
+
28
+ <!-- ACTIONS (ONLY ONE INVITE BUTTON HERE) -->
29
+ <template #actions>
30
+ <v-btn class="text-none" rounded="pill" variant="tonal" size="large" @click="openInviteDialog">
31
+ Invite Client
32
+ </v-btn>
33
+ </template>
34
+
35
+ <!-- CUSTOM CELL -->
36
+ <template #item.index="{ index }">
37
+ {{ (currentPage - 1) * 10 + index + 1 }}
38
+ </template>
39
+
40
+ <template #item.status="{ value }">
41
+ <v-chip size="small" variant="tonal">
42
+ {{ value }}
43
+ </v-chip>
44
+ </template>
45
+ <template #item.action="{ item }">
46
+ <v-btn v-if="tab === 'pending'" size="small" color="error" variant="tonal"
47
+ :loading="cancelLoadingId === item._id" :disabled="cancelLoadingId === item._id"
48
+ @click="cancelInvite(item)">
49
+ Cancel Invite
50
+ </v-btn>
51
+ </template>
52
+ </TableMain>
53
+ </v-col>
54
+
55
+ <!-- DIALOG -->
56
+ <v-dialog v-model="dialog" max-width="500">
57
+ <InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
58
+ </v-dialog>
59
+ s
60
+ </v-row>
61
+ </template>
62
+ <script setup lang="ts">
63
+
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
+
70
+
71
+ const tab = ref<'active' | 'suspended' | 'pending'>('active')
72
+ const dialog = ref(false)
73
+
74
+ const { getAll } = useOrg()
75
+ const { getVerifications, cancelUserInvitation } = useVerification()
76
+ const { getRoleById } = useRole()
77
+ const roleCache = ref<Record<string, string>>({})
78
+
79
+ /* ================= STATE ================= */
80
+ const items = ref<any[]>([])
81
+ const loading = ref(false)
82
+
83
+ const page = ref(1)
84
+ const pages = ref(1)
85
+ const search = ref("")
86
+ async function resolveRoleName(id: string) {
87
+ if (!id) return "-"
88
+
89
+ if (roleCache.value[id]) return roleCache.value[id]
90
+
91
+ const res = await getRoleById(id)
92
+
93
+ const roleName = res?.name ?? "-"
94
+
95
+ roleCache.value[id] = roleName
96
+
97
+ return roleName
98
+ }
99
+ /* ================= HEADERS ================= */
100
+ const orgHeaders = [
101
+ { title: "Organization Name", key: "name" },
102
+ { title: "Site", key: "sites" },
103
+ { title: "Plan Type", key: "plan" },
104
+ { title: "Billing Cycle", key: "bill" },
105
+ { title: "Subscription Start", key: "start" },
106
+ { title: "Subscription End", key: "end" },
107
+ { title: "Status", key: "status" },
108
+ { title: "Action", key: "actions" },
109
+ ]
110
+
111
+ const inviteHeaders = [
112
+ { title: "Email", key: "email" },
113
+ { title: "App", key: "app" },
114
+ { title: "Role", key: "role" },
115
+ { title: "Status", key: "status" },
116
+ { title: "Action", key: "action" },
117
+ ]
118
+
119
+ /* ================= COMPUTED ================= */
120
+ const currentHeaders = computed(() =>
121
+ tab.value === 'pending' ? inviteHeaders : orgHeaders
122
+ )
123
+
124
+ const currentItems = computed(() => items.value)
125
+ const currentPage = computed(() => page.value)
126
+ const currentPages = computed(() => pages.value)
127
+
128
+ const loadingState = computed(() => loading.value)
129
+ const totalItems = ref(0)
130
+ /* ================= API ================= */
131
+ async function fetchData() {
132
+ loading.value = true
133
+
134
+ try {
135
+ if (tab.value === 'pending') {
136
+ const res = await getVerifications({
137
+ status: "pending",
138
+ type: "user-invite",
139
+ page: page.value,
140
+ search: search.value,
141
+ app: "organization",
142
+ })
143
+
144
+ items.value = await Promise.all(
145
+ (res?.items || []).map(async (i: any) => ({
146
+ ...i,
147
+ app: i.metadata?.app ?? "-",
148
+ role: await resolveRoleName(i.metadata?.role),
149
+ }))
150
+ )
151
+
152
+ pages.value = res?.pages || 1
153
+ totalItems.value = res?.total || items.value.length
154
+ } else {
155
+ const res = await getAll({
156
+ page: page.value,
157
+ search: search.value,
158
+ nature: tab.value === "active" ? "" : "suspended",
159
+ })
160
+
161
+ items.value = res?.data?.items || res?.items || []
162
+
163
+ pages.value = res?.data?.totalPages || res?.totalPages || 1
164
+ totalItems.value =
165
+ res?.data?.totalItems ||
166
+ res?.totalItems ||
167
+ items.value.length
168
+ }
169
+ } finally {
170
+ loading.value = false
171
+ }
172
+ }
173
+ const pageRange = computed(() => {
174
+ if (!totalItems.value) return "0-0 of 0"
175
+
176
+ const pageSize = 10
177
+
178
+ const start = (page.value - 1) * pageSize + 1
179
+ const end = Math.min(page.value * pageSize, totalItems.value)
180
+
181
+ return `${start}-${end} of ${totalItems.value}`
182
+ })
183
+ /* ================= EVENTS ================= */
184
+ function onTabChange() {
185
+ page.value = 1
186
+ fetchData()
187
+ }
188
+
189
+ function handleUpdatePage(p: number) {
190
+ page.value = p
191
+ fetchData()
192
+ }
193
+
194
+ function handleRefresh() {
195
+ fetchData()
196
+ }
197
+
198
+ function handleSearch() {
199
+ page.value = 1
200
+ fetchData()
201
+ }
202
+
203
+ /* ================= DIALOG ================= */
204
+ function openInviteDialog() {
205
+ dialog.value = true
206
+ }
207
+
208
+ function handleSuccess() {
209
+ dialog.value = false
210
+ fetchData()
211
+ }
212
+
213
+ /* ================= INIT ================= */
214
+ onMounted(() => {
215
+ fetchData()
216
+ })
217
+
218
+ const cancelLoadingId = ref<string | null>(null)
219
+
220
+ async function cancelInvite(item: any) {
221
+ try {
222
+ cancelLoadingId.value = item._id
223
+
224
+ await cancelUserInvitation(item._id)
225
+
226
+ await fetchData()
227
+ } finally {
228
+ cancelLoadingId.value = null
229
+ }
230
+ }
231
+ </script>
@@ -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>
@@ -3,7 +3,7 @@
3
3
  <v-list>
4
4
  <v-list-item>
5
5
  <v-list-item-title class="text-h6 text-white">
6
- {{ APP_NAME }}
6
+ {{ props.title || APP_NAME }}
7
7
  </v-list-item-title>
8
8
  </v-list-item>
9
9
  <slot name="action" />
@@ -36,6 +36,7 @@
36
36
  <script setup lang="ts">
37
37
  const props = defineProps({
38
38
  navigationItems: { type: Array<TNavigationItem>, required: true },
39
+ title: { type: String, default: "" }
39
40
  });
40
41
 
41
42
  const { drawer } = useLocal();
@@ -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
- {{ props.title }}
7
- </span>
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
- <v-card-text style="max-height: 100vh; overflow-y: auto">
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 class="text-capitalize" title="Name" required />
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
- ></v-text-field>
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
- <InputListGroupSelection
28
- v-model="selectedPermissions"
29
- :items="props.permissions"
30
- variant="outlined"
31
- border="thin"
32
- :error-messages="requireListRule(selectedPermissions)"
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-2">
37
- <v-checkbox v-model="createMore" density="comfortable" hide-details>
38
- <template #label>
39
- <span class="text-subtitle-2 font-weight-bold">
40
- Create more
41
- </span>
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-checkbox>
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 class="px-6">
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
- size="large"
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 '../composables/useRole';
95
- import useUtils from '../composables/useUtils';
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 createMore = ref(false);
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 message = ref("");
222
+ const enabledCategories = ref<Set<string>>(new Set());
128
223
 
129
- const { createRole } = useRole();
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
- createMore.value = false;
306
+ enabledCategories.value = new Set();
307
+ platform.value = "web";
161
308
  message.value = "";
162
309
  emit("cancel");
163
310
  }
@@ -83,6 +83,7 @@
83
83
  @cancel="createDialog = false"
84
84
  :permissions="props.permissions"
85
85
  :org="props.orgId"
86
+ :site-id="props.siteId"
86
87
  @success="success()"
87
88
  :type="props.type"
88
89
  @success:create-more="getRoles()"
@@ -327,7 +327,8 @@
327
327
  </template>
328
328
 
329
329
  <script lang="ts" setup>
330
- import useServiceProvider from '../composables/useServiceProvider';
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
- email: serviceProvider.value.email?.trim(),
543
- orgId: props.orgId,
544
- siteId: props.siteId,
545
- siteName: site.value?.name || props.siteName,
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
- if (printer?.vendorId && printer?.productId && accessCards.length) {
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
- if (!qrData) continue;
970
- await testConnection(
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
  }
@@ -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
 
@@ -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("Print QR code error:", error);
387
+ console.error("[WebUSB] printQrCode error:", error);
378
388
  return { success: false, error: error.message };
379
389
  }
380
390
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.11.32",
5
+ "version": "1.11.34",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {