@7365admin1/layer-common 1.11.35 → 1.11.37

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.37
4
+
5
+ ### Patch Changes
6
+
7
+ - 100370d: Update layer-common version
8
+
9
+ ## 1.11.36
10
+
11
+ ### Patch Changes
12
+
13
+ - c708a39: updates and fixes
14
+
3
15
  ## 1.11.35
4
16
 
5
17
  ### Patch Changes
@@ -1,16 +1,23 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
-
4
- <!-- MAIN TABLE -->
5
3
  <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
-
4
+ <TableMain
5
+ :headers="currentHeaders"
6
+ :items="items"
7
+ :loading="loading"
8
+ :page="page"
9
+ :pages="pages"
10
+ :pageRange="pageRange"
11
+ :items-per-page="-1"
12
+ :extension-height="120"
13
+ :offset="300"
14
+ show-header
15
+ @refresh="handleRefresh"
16
+ @update:page="handleUpdatePage"
17
+ >
10
18
  <!-- EXTENSION -->
11
19
  <template #extension>
12
20
  <v-row no-gutters class="w-100 d-flex flex-column ga-2 pt-2">
13
-
14
21
  <v-tabs v-model="tab" class="w-100" height="32" @update:model-value="onTabChange">
15
22
  <v-tab value="active">Active</v-tab>
16
23
  <v-tab value="suspended">Suspended</v-tab>
@@ -18,34 +25,61 @@
18
25
  </v-tabs>
19
26
 
20
27
  <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" />
28
+ <v-text-field
29
+ v-model="search"
30
+ density="compact"
31
+ placeholder="Search"
32
+ clearable
33
+ max-width="300"
34
+ append-inner-icon="mdi-magnify"
35
+ hide-details
36
+ @update:model-value="handleSearch"
37
+ />
23
38
  </div>
24
-
25
39
  </v-row>
26
40
  </template>
27
41
 
28
- <!-- ACTIONS (ONLY ONE INVITE BUTTON HERE) -->
42
+ <!-- ACTIONS -->
29
43
  <template #actions>
30
- <v-btn class="text-none" rounded="pill" variant="tonal" size="large" @click="openInviteDialog">
44
+ <v-btn
45
+ v-if="tab !== 'suspended'"
46
+ class="text-none"
47
+ rounded="pill"
48
+ variant="tonal"
49
+ size="large"
50
+ @click="openInviteDialog"
51
+ >
31
52
  Invite Client
32
53
  </v-btn>
33
54
  </template>
34
55
 
35
- <!-- CUSTOM CELL -->
36
- <template #item.index="{ index }">
56
+ <!-- INDEX CELL -->
57
+ <!-- <template #item.index="{ index }">
37
58
  {{ (currentPage - 1) * 10 + index + 1 }}
38
- </template>
59
+ </template> -->
39
60
 
61
+ <!-- STATUS CELL -->
40
62
  <template #item.status="{ value }">
41
- <v-chip size="small" variant="tonal">
63
+ <v-chip
64
+ size="small"
65
+ :color="value === 'suspended' ? 'error' : 'success'"
66
+ variant="flat"
67
+ >
42
68
  {{ value }}
43
69
  </v-chip>
44
70
  </template>
71
+
72
+ <!-- ACTION CELL (pending only) -->
45
73
  <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)">
74
+ <v-btn
75
+ v-if="tab === 'pending'"
76
+ size="small"
77
+ color="error"
78
+ variant="tonal"
79
+ :loading="cancelLoadingId === item._id"
80
+ :disabled="cancelLoadingId === item._id"
81
+ @click="cancelInvite(item)"
82
+ >
49
83
  Cancel Invite
50
84
  </v-btn>
51
85
  </template>
@@ -54,66 +88,61 @@
54
88
 
55
89
  <!-- DIALOG -->
56
90
  <v-dialog v-model="dialog" max-width="500">
57
- <InvitationClientForm title="Invite Client" @success="handleSuccess" @cancel="dialog = false" />
91
+ <InvitationClientForm
92
+ title="Invite Client"
93
+ @success="handleSuccess"
94
+ @cancel="dialog = false"
95
+ />
58
96
  </v-dialog>
59
- s
60
97
  </v-row>
61
98
  </template>
62
- <script setup lang="ts">
63
99
 
64
- import { computed, onMounted, ref, watch } from 'vue'
100
+ <script setup lang="ts">
101
+ import { computed, onMounted, ref } from 'vue'
65
102
  import useOrg from '../composables/useOrg'
66
103
  import useVerification from '../composables/useVerification'
67
104
  import useRole from '../composables/useRole'
68
105
  import InvitationClientForm from './InvitationClientForm.vue'
69
-
106
+ import useCustomerSite from '../composables/useCustomerSite'
107
+ import useSubscription from '../composables/useSubscription'
70
108
 
71
109
  const tab = ref<'active' | 'suspended' | 'pending'>('active')
72
110
  const dialog = ref(false)
73
111
 
74
112
  const { getAll } = useOrg()
75
113
  const { getVerifications, cancelUserInvitation } = useVerification()
114
+ const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
115
+ const { getByOrgId: getSubscriptionByOrgId } = useSubscription()
76
116
  const { getRoleById } = useRole()
77
117
  const roleCache = ref<Record<string, string>>({})
78
118
 
79
119
  /* ================= STATE ================= */
80
120
  const items = ref<any[]>([])
81
121
  const loading = ref(false)
82
-
83
122
  const page = ref(1)
84
123
  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
124
+ const search = ref('')
125
+ const totalItems = ref(0)
126
+ const cancelLoadingId = ref<string | null>(null)
96
127
 
97
- return roleName
98
- }
99
128
  /* ================= HEADERS ================= */
100
129
  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" },
130
+ { title: 'Organization Name', key: 'name' },
131
+ { title: 'Sites', key: 'sites' },
132
+ { title: 'Plan Type', key: 'planType' },
133
+ { title: 'Billing Cycle', key: 'billingCycle' },
134
+ { title: 'Subscription Start', key: 'subscriptionStart' },
135
+ { title: 'Subscription End', key: 'subscriptionEnd' },
136
+ { title: 'Status', key: 'status' },
137
+ { title: 'Actions', key: 'actions' },
109
138
  ]
110
139
 
111
140
  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" },
141
+ { title: 'Email', key: 'email' },
142
+ { title: 'App', key: 'app' },
143
+ { title: 'Role', key: 'role' },
144
+ { title: 'Status', key: 'status' },
145
+ { title: 'Action', key: 'action' },
117
146
  ]
118
147
 
119
148
  /* ================= COMPUTED ================= */
@@ -121,65 +150,173 @@ const currentHeaders = computed(() =>
121
150
  tab.value === 'pending' ? inviteHeaders : orgHeaders
122
151
  )
123
152
 
124
- const currentItems = computed(() => items.value)
125
- const currentPage = computed(() => page.value)
126
- const currentPages = computed(() => pages.value)
153
+ const pageRange = computed(() => {
154
+ if (!totalItems.value) return '0-0 of 0'
155
+ const pageSize = 10
156
+ const start = (page.value - 1) * pageSize + 1
157
+ const end = Math.min(page.value * pageSize, totalItems.value)
158
+ return `${start}-${end} of ${totalItems.value}`
159
+ })
160
+
161
+ /* ================= HELPERS ================= */
162
+ function formatDate(date?: string) {
163
+ if (!date) return '-'
164
+ return new Date(date).toLocaleDateString('en-US')
165
+ }
166
+
167
+ async function resolveRoleName(id: string) {
168
+ if (!id) return '-'
169
+ if (roleCache.value[id]) return roleCache.value[id]
170
+ const res = await getRoleById(id)
171
+ const roleName = res?.name ?? '-'
172
+ roleCache.value[id] = roleName
173
+ return roleName
174
+ }
175
+
176
+ /* ================= FETCH ================= */
177
+ async function fetchActive() {
178
+ const res = await getAll({ page: page.value, search: search.value })
179
+ const orgs = res?.data?.items || res?.items || []
180
+
181
+ const mapped = await Promise.all(
182
+ orgs.map(async (org: any) => {
183
+ let sitesCount = 0
184
+ let planType = '-'
185
+ let billingCycle = '-'
186
+ let subscriptionStart = '-'
187
+ let subscriptionEnd = '-'
188
+
189
+ try {
190
+ const siteRes = await getCustomerSitesByOrgId(org._id)
191
+ sitesCount =
192
+ siteRes?.data?.totalItems ||
193
+ siteRes?.totalItems ||
194
+ siteRes?.data?.items?.length ||
195
+ siteRes?.items?.length ||
196
+ 0
197
+
198
+ const subRes = await getSubscriptionByOrgId(org._id)
199
+ const subscription = subRes?.data || subRes
200
+ if (subscription) {
201
+ planType = subscription?.paidType || '-'
202
+ billingCycle = subscription?.billingCycle || '-'
203
+ subscriptionStart = formatDate(subscription?.createdAt)
204
+ subscriptionEnd = formatDate(subscription?.nextBillingDate)
205
+ }
206
+ } catch (e) {
207
+ console.error('Error mapping org:', org._id, e)
208
+ }
209
+
210
+ return {
211
+ _id: org._id,
212
+ ...org,
213
+ sites: `${sitesCount} Sites`,
214
+ planType,
215
+ billingCycle,
216
+ subscriptionStart,
217
+ subscriptionEnd,
218
+ status: org?.status || 'Active',
219
+ }
220
+ })
221
+ )
222
+
223
+ items.value = mapped
224
+ pages.value = res?.data?.totalPages || res?.totalPages || 1
225
+ totalItems.value = res?.data?.totalItems || res?.totalItems || mapped.length
226
+ }
227
+
228
+ async function fetchSuspended() {
229
+ const res = await getAll({
230
+ page: page.value,
231
+ search: search.value,
232
+ // status: 'suspended',
233
+ })
234
+ const orgs = res?.data?.items || res?.items || []
235
+
236
+ const mapped = await Promise.all(
237
+ orgs.map(async (org: any) => {
238
+ let sitesCount = 0
239
+ let planType = '-'
240
+ let billingCycle = '-'
241
+ let subscriptionStart = '-'
242
+ let subscriptionEnd = '-'
243
+
244
+ try {
245
+ const siteRes = await getCustomerSitesByOrgId(org._id)
246
+ sitesCount =
247
+ siteRes?.data?.totalItems ||
248
+ siteRes?.totalItems ||
249
+ siteRes?.data?.items?.length ||
250
+ siteRes?.items?.length ||
251
+ 0
252
+
253
+ const subRes = await getSubscriptionByOrgId(org._id)
254
+ const subscription = subRes?.data || subRes
255
+ if (subscription) {
256
+ planType = subscription?.paidType || '-'
257
+ billingCycle = subscription?.billingCycle || '-'
258
+ subscriptionStart = formatDate(subscription?.createdAt)
259
+ subscriptionEnd = formatDate(subscription?.nextBillingDate)
260
+ }
261
+ } catch (e) {
262
+ console.error('Error mapping suspended org:', org._id, e)
263
+ }
264
+
265
+ return {
266
+ _id: org._id,
267
+ ...org,
268
+ sites: `${sitesCount} Sites`,
269
+ planType,
270
+ billingCycle,
271
+ subscriptionStart,
272
+ subscriptionEnd,
273
+ status: org?.status || 'suspended',
274
+ }
275
+ })
276
+ )
277
+
278
+ items.value = mapped
279
+ pages.value = res?.data?.totalPages || res?.totalPages || 1
280
+ totalItems.value = res?.data?.totalItems || res?.totalItems || mapped.length
281
+ }
282
+
283
+ async function fetchPending() {
284
+ const res = await getVerifications({
285
+ status: 'pending',
286
+ type: 'user-invite',
287
+ page: page.value,
288
+ search: search.value,
289
+ app: 'organization',
290
+ })
291
+
292
+ items.value = await Promise.all(
293
+ (res?.items || []).map(async (i: any) => ({
294
+ ...i,
295
+ app: i.metadata?.app ?? '-',
296
+ role: await resolveRoleName(i.metadata?.role),
297
+ }))
298
+ )
299
+
300
+ pages.value = res?.pages || 1
301
+ totalItems.value = res?.total || items.value.length
302
+ }
127
303
 
128
- const loadingState = computed(() => loading.value)
129
- const totalItems = ref(0)
130
- /* ================= API ================= */
131
304
  async function fetchData() {
132
305
  loading.value = true
133
-
134
306
  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
307
+ if (tab.value === 'suspended') {
308
+ items.value = []
309
+ pages.value = 1
310
+ totalItems.value = 0
311
+ return
168
312
  }
313
+ if (tab.value === 'active') await fetchActive()
314
+ else await fetchPending()
169
315
  } finally {
170
316
  loading.value = false
171
317
  }
172
318
  }
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
319
 
181
- return `${start}-${end} of ${totalItems.value}`
182
- })
183
320
  /* ================= EVENTS ================= */
184
321
  function onTabChange() {
185
322
  page.value = 1
@@ -210,22 +347,19 @@ function handleSuccess() {
210
347
  fetchData()
211
348
  }
212
349
 
213
- /* ================= INIT ================= */
214
- onMounted(() => {
215
- fetchData()
216
- })
217
-
218
- const cancelLoadingId = ref<string | null>(null)
219
-
350
+ /* ================= CANCEL INVITE ================= */
220
351
  async function cancelInvite(item: any) {
221
352
  try {
222
353
  cancelLoadingId.value = item._id
223
-
224
354
  await cancelUserInvitation(item._id)
225
-
226
355
  await fetchData()
227
356
  } finally {
228
357
  cancelLoadingId.value = null
229
358
  }
230
359
  }
360
+
361
+ /* ================= INIT ================= */
362
+ onMounted(() => {
363
+ fetchData()
364
+ })
231
365
  </script>
@@ -236,7 +236,7 @@ function handleUpdateSite(id: string) {
236
236
  }
237
237
 
238
238
  /* ================= SUBMIT ================= */
239
- const { inviteUser } = useUser()
239
+ const { inviteUserClient } = useUser()
240
240
  const { requiredRule, emailRule } = useUtils()
241
241
 
242
242
  const message = ref("")
@@ -251,14 +251,14 @@ async function submit() {
251
251
 
252
252
  await Promise.all(
253
253
  apps.map(app =>
254
- inviteUser({
254
+ inviteUserClient({
255
255
  email: invite.value.email,
256
256
  app,
257
257
  role: invite.value.role,
258
258
  org: invite.value.org,
259
259
  site: invite.value.site,
260
260
  siteName: invite.value.siteName,
261
- isMemberInvite: invite.value.isMemberInvite,
261
+ // isMemberInvite: invite.value.isMemberInvite,
262
262
  })
263
263
  )
264
264
  )
@@ -35,34 +35,32 @@
35
35
  <v-col v-if="shouldShowField('block')" cols="12">
36
36
  <InputLabel class="text-capitalize" title="Block" required />
37
37
  <v-select v-model="vehicle.block" :items="blocksArray" item-value="value" item-title="title"
38
- @update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]"
39
- />
38
+ @update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" />
40
39
  </v-col>
41
40
 
42
41
  <v-col v-if="shouldShowField('level')" cols="12">
43
42
  <InputLabel class="text-capitalize" title="Level" required />
44
- <v-select v-model="vehicle.level" :items="levelsArray" density="comfortable"
45
- :disabled="!vehicle.block" @update:model-value="handleChangeLevel"
46
- :rules="[requiredRule]" />
43
+ <v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block"
44
+ @update:model-value="handleChangeLevel" :rules="[requiredRule]" />
47
45
  </v-col>
48
46
 
49
47
  <v-col v-if="shouldShowField('unit')" cols="12">
50
48
  <InputLabel class="text-capitalize" title="Unit" required />
51
- <v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable"
52
- :disabled="!vehicle.level" :rules="[requiredRule]" />
49
+ <v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level"
50
+ :rules="[requiredRule]" />
53
51
  </v-col>
54
52
 
55
53
  <v-col v-if="shouldShowField('nric')" cols="12">
56
54
  <InputLabel class="text-capitalize" title="NRIC" required />
57
- <InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs && type !== 'blocklist'" />
55
+ <InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]"
56
+ :disabled="disablePrefilledInputs && type !== 'blocklist'" />
58
57
  </v-col>
59
58
 
60
59
  <v-col v-if="shouldShowField('name')" cols="12">
61
60
  <v-row>
62
61
  <v-col cols="12">
63
62
  <InputLabel class="text-capitalize" title="Full Name" required />
64
- <v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]"
65
- />
63
+ <v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" />
66
64
  </v-col>
67
65
  </v-row>
68
66
  </v-col>
@@ -75,42 +73,49 @@
75
73
  </v-col>
76
74
 
77
75
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
78
-
79
- <!-- <v-text-field v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" /> -->
80
- <template v-for="plate in vehicle.plates" :key="plate.plateNumber">
81
- <InputLabel class="text-capitalize" title="Existing Vehicle Numbers" required />
82
- <v-text-field v-model="plate.plateNumber" density="comfortable" :rules="[requiredRule]" class="mb-2"
83
- readonly disabled>
84
- <template #append-inner>
85
- <v-chip size="small" class="ma-0" :color="formatVehicleStatus(plate?.status)?.color as string">{{
86
- formatVehicleStatus(plate?.status)?.label }}</v-chip>
87
- </template>
88
- </v-text-field>
89
- </template>
90
76
 
91
- <InputLabel class="text-capitalize" title="New Vehicle Numbers" required />
92
- <template v-for="(plate, index) in newPlateNumbers" :key="index">
93
- <v-row class="" :class="`${index === newPlateNumbers.length - 1 ? 'mb-1' : 'mb-5'}`" no-gutters>
94
- <v-col :cols="newPlateNumbers.length > 1 ? '11' : '12'">
95
- <InputVehicleNumber v-model="newPlateNumbers[index]" density="comfortable"
96
- placeholder="New Vehicle Number" :rules="[requiredRule]" />
97
- </v-col>
77
+ <template v-if="prop.mode === 'add'">
78
+ <!-- <v-text-field v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" /> -->
79
+ <template v-for="plate in vehicle.plates" :key="plate.plateNumber">
80
+ <InputLabel class="text-capitalize" title="Existing Vehicle Numbers" required />
81
+ <v-text-field v-model="plate.plateNumber" density="comfortable" :rules="[requiredRule]"
82
+ class="mb-2 class-readonly" readonly>
83
+ <template #append-inner>
84
+ <v-chip size="small" class="ma-0" :color="formatVehicleStatus(plate?.status)?.color as string">{{
85
+ formatVehicleStatus(plate?.status)?.label }}</v-chip>
86
+ </template>
87
+ </v-text-field>
88
+ </template>
98
89
 
99
- <v-col :cols="newPlateNumbers.length > 1 ? '1' : '0'" class="d-flex align-start">
100
- <v-btn icon="mdi-delete-outline" variant="text" color="error" v-if="newPlateNumbers.length > 1"
101
- @click="removePlate(index)" />
102
- </v-col>
103
- </v-row>
90
+ <InputLabel class="text-capitalize" title="New Vehicle Numbers" required />
91
+ <template v-for="(plate, index) in newPlateNumbers" :key="index">
92
+ <v-row class="" :class="`${index === newPlateNumbers.length - 1 ? 'mb-1' : 'mb-5'}`" no-gutters>
93
+ <v-col :cols="newPlateNumbers.length > 1 ? '11' : '12'">
94
+ <InputVehicleNumber v-model="newPlateNumbers[index]" density="comfortable"
95
+ placeholder="New Vehicle Number" :rules="[requiredRule]" />
96
+ </v-col>
97
+
98
+ <v-col :cols="newPlateNumbers.length > 1 ? '1' : '0'" class="d-flex align-start">
99
+ <v-btn icon="mdi-delete-outline" variant="text" color="error" v-if="newPlateNumbers.length > 1"
100
+ @click="removePlate(index)" />
101
+ </v-col>
102
+ </v-row>
103
+
104
+ <v-btn prepend-icon="mdi-plus" variant="outlined" class="text-none" @click="addPlate">
105
+ Add Plate
106
+ </v-btn>
107
+ </template>
104
108
  </template>
105
109
 
106
- <v-btn prepend-icon="mdi-plus" variant="outlined" class="text-none" @click="addPlate">
107
- Add Plate
108
- </v-btn>
110
+ <template v-else-if="prop.mode === 'edit'">
111
+ <InputLabel class="text-capitalize" title="Vehicle Number" required />
112
+ <InputVehicleNumber v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" />
113
+ </template>
109
114
  </v-col>
110
115
 
111
116
  <v-col v-if="shouldShowField('remarks')" cols="12">
112
- <InputLabel class="text-capitalize" title="Remarks" required />
113
- <v-textarea v-model="vehicle.remarks" density="comfortable" :rows="3" no-resize :rules="[requiredRule]" />
117
+ <InputLabel class="text-capitalize" title="Remarks" />
118
+ <v-textarea v-model="vehicle.remarks" density="comfortable" :rows="3" no-resize />
114
119
  </v-col>
115
120
 
116
121
  <v-col v-if="shouldShowField('start')" cols="12">
@@ -192,8 +197,7 @@
192
197
  Matching Records for "{{ searchUnitName ?? "-" }}"
193
198
  </span>
194
199
  <span>
195
- <v-btn icon="mdi-close" variant="text"
196
- @click="handleCloseMatchDialog" />
200
+ <v-btn icon="mdi-close" variant="text" @click="handleCloseMatchDialog" />
197
201
  </span>
198
202
  </v-row>
199
203
  </v-toolbar-title>
@@ -209,7 +213,7 @@
209
213
  </v-list-item-title>
210
214
 
211
215
  <v-list-item-subtitle>
212
- NRIC: {{ v.nric || "-" }}
216
+ NRIC: {{ v.nric || "-" }}
213
217
  </v-list-item-subtitle>
214
218
 
215
219
  <div class="mt-1">
@@ -262,6 +266,10 @@ const prop = defineProps({
262
266
  vehicleData: {
263
267
  type: Object as PropType<Partial<TVehicle> | null>,
264
268
  default: null
269
+ },
270
+ plateNumberId: {
271
+ type: String,
272
+ default: ''
265
273
  }
266
274
  });
267
275
 
@@ -291,6 +299,7 @@ const vehicle = reactive<Omit<ExceptPartial<TVehicle>, 'type'> & { type: TVehicl
291
299
  org: prop.org,
292
300
  _id: '',
293
301
  peopleId: '',
302
+ plateNumber: "",
294
303
  });
295
304
 
296
305
  const newPlateNumbers = ref<string[]>(['']);
@@ -520,7 +529,6 @@ async function submit() {
520
529
  const { plateNumber, type, category, name, phoneNumber, block, level, unit, nric, remarks, seasonPassType, start, end, site, org, peopleId } = vehicle
521
530
 
522
531
  let payload: Partial<TVehiclePayload> = {
523
- plateNumber: newPlateNumbers.value,
524
532
  name,
525
533
  phoneNumber,
526
534
  type: type === 'seasonpass' ? 'whitelist' : type, // season pass will be created as whitelist type in backend with additional seasonPassType field,
@@ -533,7 +541,16 @@ async function submit() {
533
541
  ...payload,
534
542
  category,
535
543
  site,
536
- org
544
+ org,
545
+ plateNumber: newPlateNumbers.value
546
+ }
547
+ }
548
+
549
+ if (prop.mode === 'edit' && prop.plateNumberId) {
550
+ payload = {
551
+ ...payload,
552
+ site: prop.site,
553
+ plateNumber
537
554
  }
538
555
  }
539
556
 
@@ -564,8 +581,8 @@ async function submit() {
564
581
  if (prop.mode === 'add') {
565
582
  await addVehicle(payload);
566
583
  } else if (prop.mode === 'edit') {
567
- const vehicleId = vehicle?._id as string;
568
- await updateVehicle(vehicleId, payload);
584
+ const plateNumberId = prop.plateNumberId as string;
585
+ await updateVehicle(plateNumberId, payload);
569
586
  }
570
587
  emit("done");
571
588
 
@@ -692,14 +709,14 @@ function selectPeopleRecord(record: TPeople) {
692
709
 
693
710
 
694
711
  async function checkPeopleById() {
695
- if (!vehicle.unit) return;
712
+ if (!vehicle.unit || prop.mode === 'edit') return;
696
713
 
697
714
  checkingUnit.value = true;
698
715
  showMatchingPeopleDialog.value = true;
699
716
  // Lock editable fields while we verify if this NRIC has existing records.
700
717
  disablePrefilledInputs.value = true;
701
718
  try {
702
- const res = await getPeopleByUnit(vehicle.unit, { status: 'active', type: 'resident,tenant' }) as TPeople[]| null;
719
+ const res = await getPeopleByUnit(vehicle.unit, { status: 'active', type: 'resident,tenant' }) as TPeople[] | null;
703
720
 
704
721
  if (Array.isArray(res) && res.length > 0) {
705
722
  matchingPeople.value = res || []
@@ -745,12 +762,23 @@ const resetVehicleDetails = () => {
745
762
 
746
763
  function handleCloseMatchDialog() {
747
764
  showMatchingPeopleDialog.value = false;
748
- if(matchingPeople.value.length > 0) {
765
+ if (matchingPeople.value.length > 0) {
749
766
  vehicle.unit = null
750
767
  }
751
768
  }
752
769
 
753
-
770
+ onMounted(() => {
771
+ setTimeout(() => {
772
+ console.log("VehicleForm mounted with props:", prop);
773
+ if (prop.mode === 'edit' && prop.vehicleData) {
774
+ // In edit mode, we only want to check for matching people records if there is a pre-filled unit value. If there is no unit value, we can assume that this vehicle record is not linked to any existing people record and skip the check.
775
+ vehicle.seasonPassType = prop.vehicleData.seasonPassType || '';
776
+ vehicle.end = prop.vehicleData.end || '';
777
+ vehicle.start = prop.vehicleData.start || '';
778
+ vehicle.plateNumber = prop.vehicleData.plates?.find((p) => p._id === prop.plateNumberId)?.plateNumber || '';
779
+ }
780
+ }, 300);
781
+ })
754
782
 
755
783
 
756
784
  </script>
@@ -758,4 +786,8 @@ function handleCloseMatchDialog() {
758
786
  .button-outline-class {
759
787
  border: 1px solid rgba(var(--v-theme-primary));
760
788
  }
789
+
790
+ .class-readonly {
791
+ pointer-events: none;
792
+ }
761
793
  </style>
@@ -37,10 +37,15 @@
37
37
  </v-dialog>
38
38
 
39
39
  <v-dialog v-model="dialog.createVehicle" v-if="vehicleType" width="450" persistent>
40
- <VehicleForm :type="vehicleType" mode="add" :vehicle-data="selectedVehicleObject" @back="handleBackToSelection"
40
+ <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedVehicleObject" :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection"
41
41
  @done="handleAddVehicleComplete" :org="org" :site="props.site" @close:all="handleCloseAll" />
42
42
  </v-dialog>
43
43
 
44
+ <v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
45
+ <VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedPlateNumberObject" :plate-number-id="selectedPlateNumberId" @back="handleBackToSelection"
46
+ @done="handleAddVehicleComplete" :org="org" :site="props.site" @close:all="handleCloseAll" @close="handleCloseAll" />
47
+ </v-dialog>
48
+
44
49
  <!-- <v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
45
50
  <VehicleForm :type="vehicleType" mode="edit" :vehicle-data="selectedVehicleObject"
46
51
  @back="dialog.updateVehicle = false" @close="dialog.updateVehicle = false" @done="handleUpdateVehicleComplete"
@@ -58,7 +63,7 @@
58
63
  <span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong></span>
59
64
  <!-- <PlateNumberDisplay :plate-numbers="selectedVehicleObject[key]" show-all :default-value="selectedVehicleObject.plateNumber" /> -->
60
65
  <v-col cols="12">
61
- <v-card flat outlined class="pa-2" border="sm black">
66
+ <v-card flat outlined class="pa-2" border="sm black" :loading="loadingPlateNumberDetails">
62
67
  <v-data-table :items="selectedVehicleObject[key]" :headers="plateHeaders" hide-default-footer>
63
68
  <template #item.status="{ value }">
64
69
  <v-chip :color="formatVehicleStatus(value).color" size="x-small" dark>
@@ -66,22 +71,33 @@
66
71
  </v-chip>
67
72
  </template>
68
73
  <template #item.action="{ item: plateNumberItem, value }">
69
- <v-btn v-if="canDeleteVehicle && (plateNumberItem as TPlateNumber)?.status == 'active'"
70
- text="Delete" color="error" flat size="x-small"
71
- @click="handleDeleteVehicleAction(plateNumberItem as TPlateNumber)" />
72
- <v-btn
73
- v-if="props.app === 'property_management_agency' && (plateNumberItem as TPlateNumber)?.status == 'pending'"
74
- text="Approve" color="success" flat size="x-small"
75
- @click="handleApproveVehicle(plateNumberItem as TPlateNumber)" />
76
- <v-btn v-if="(plateNumberItem as TPlateNumber)?.status == 'deleted'" text="Restore"
77
- color="orange" flat size="x-small"
78
- @click="handleRestoreVehicle(plateNumberItem as TPlateNumber)" />
79
- <v-btn
80
- v-if="((plateNumberItem as TPlateNumber)?.type === 'whitelist' || (plateNumberItem as TPlateNumber)?.type === 'blocklist') && plateNumberItem?.status == 'active'"
81
- variant="outlined"
82
- :text="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'Whitelist' : 'Blocklist'"
83
- :color="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'primary' : 'red'" flat
84
- size="x-small" class="ml-1" @click="handleUpdateType(plateNumberItem as TPlateNumber)" />
74
+ <v-menu>
75
+ <template #activator="{ props: menuProps }">
76
+ <v-btn icon="mdi-dots-vertical" v-bind="menuProps" flat size="x-small" />
77
+ </template>
78
+ <v-list density="compact">
79
+ <v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'active'"
80
+ title="Edit Vehicle" prepend-icon="mdi-pencil" @click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
81
+ <v-list-item
82
+ v-if="props.app === 'property_management_agency' && (plateNumberItem as TPlateNumber)?.status == 'pending'"
83
+ title="Approve" prepend-icon="mdi-check-circle" base-color="success"
84
+ @click="handleApproveVehicle(plateNumberItem as TPlateNumber)" />
85
+ <v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'deleted'" title="Restore"
86
+ prepend-icon="mdi-restore" base-color="orange"
87
+ @click="handleRestoreVehicle(plateNumberItem as TPlateNumber)" />
88
+ <v-list-item
89
+ v-if="((plateNumberItem as TPlateNumber)?.type === 'whitelist' || (plateNumberItem as TPlateNumber)?.type === 'blocklist') && plateNumberItem?.status == 'active'"
90
+ :title="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'Whitelist' : 'Blocklist'"
91
+ prepend-icon="mdi-swap-horizontal"
92
+ :base-color="(plateNumberItem as TPlateNumber)?.type === 'blocklist' ? 'primary' : 'red'"
93
+ @click="handleUpdateType(plateNumberItem as TPlateNumber)" />
94
+
95
+ <v-list-item
96
+ v-if="canDeleteVehicle && (plateNumberItem as TPlateNumber)?.status == 'active'"
97
+ title="Delete" prepend-icon="mdi-delete" base-color="error"
98
+ @click="handleDeleteVehicleAction(plateNumberItem as TPlateNumber)" />
99
+ </v-list>
100
+ </v-menu>
85
101
  </template>
86
102
  </v-data-table>
87
103
  <v-btn text="Add Vehicle Number" class="mt-6 text-capitalize" prepend-icon="mdi-plus"
@@ -94,7 +110,7 @@
94
110
  <v-col v-else-if="selectedVehicleObject[key]" cols="12">
95
111
  <span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong> {{ formatValues(key,
96
112
  selectedVehicleObject[key])
97
- }}</span>
113
+ }}</span>
98
114
  </v-col>
99
115
  </template>
100
116
  </v-row>
@@ -168,26 +184,30 @@ const plateHeaders = [
168
184
 
169
185
 
170
186
  const { formatCamelCaseToWords, formatDate, debounce } = useUtils();
171
- const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle } = useVehicle();
187
+ const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getVehicleById } = useVehicle();
172
188
 
173
189
  const items = ref<Array<Record<string, any>>>([]);
174
190
  const page = ref(1);
175
191
  const pages = ref(0);
176
192
  const pageRange = ref("-- - -- of --");
177
193
 
194
+ const mode = ref<"add" | "edit">("add");
195
+
178
196
  const searchInput = ref("")
179
197
 
180
198
  const loading = ref(false);
199
+ const loadingPlateNumberDetails = ref(false);
181
200
  const deletingVehicle = ref(false);
182
201
  const approvingVehicle = ref(false);
183
202
  const restoringVehicle = ref(false);
184
203
  const updatingType = ref(false);
185
204
 
186
205
  const selectedVehicleId = ref<string | null>(null)
187
- const vehicleType = ref<TVehicleType | null>(null);
206
+ const vehicleType = ref<TVehicleType | 'seasonpass' | null>(null);
188
207
  const vehicleTypeFilter = ref<TVehicleType | null>(null);
189
208
 
190
209
  const selectedPlateNumberObject = ref<TPlateNumber | null>(null);
210
+ const selectedPlateNumberId = ref<string | null>(null); // individual plate number
191
211
 
192
212
  const message = ref("");
193
213
  const messageColor = ref("");
@@ -255,6 +275,8 @@ const handleCloseAll = () => {
255
275
  dialog.createVehicle = false;
256
276
  dialog.updateVehicle = false;
257
277
  dialog.showSelection = false;
278
+ dialog.updateVehicleType = false;
279
+ mode.value = "add";
258
280
  vehicleType.value = null;
259
281
  }
260
282
 
@@ -294,12 +316,29 @@ function addNewPlateNumbers() {
294
316
  dialog.showSelection = true;
295
317
  }
296
318
 
297
- function handleEditVehicleAction() {
298
- vehicleType.value = selectedVehicleObject.value?.type || null;
299
- console.log("Selected Vehicle Object: ", selectedVehicleObject.value);
300
- dialog.showSelection = false;
301
- dialog.updateVehicle = false;
302
- dialog.updateVehicle = true;
319
+ async function handleEditVehicleAction(item: TPlateNumber) {
320
+
321
+ selectedPlateNumberId.value = item?._id as string;
322
+
323
+ try {
324
+ loadingPlateNumberDetails.value = true;
325
+ const res = await getVehicleById(selectedPlateNumberId.value as string)
326
+ if(res?.seasonPassType){
327
+ vehicleType.value = "seasonpass";
328
+ } else {
329
+ vehicleType.value = item?.type || null;
330
+ }
331
+ selectedPlateNumberObject.value = res || null;
332
+ dialog.showMoreActions = false;
333
+ mode.value = "edit";
334
+ dialog.updateVehicle = true;
335
+
336
+ } catch (error) {
337
+ console.error("Error fetching vehicle details:", error);
338
+ } finally {
339
+ loadingPlateNumberDetails.value = false;
340
+ }
341
+
303
342
  }
304
343
 
305
344
  function handleDeleteVehicleAction(item: TPlateNumber) {
@@ -354,6 +393,7 @@ function handleUpdatePage(newPageNum: number) {
354
393
  }
355
394
 
356
395
  function handleAddVehicleClick() {
396
+ mode.value = "add";
357
397
  dialog.showSelection = true;
358
398
  selectedVehicleId.value = null;
359
399
  }
@@ -15,6 +15,7 @@ export default function useCustomerSite() {
15
15
  );
16
16
  }
17
17
 
18
+
18
19
  async function getAll({
19
20
  page = 1,
20
21
  search = "",
@@ -47,10 +48,20 @@ export default function useCustomerSite() {
47
48
  );
48
49
  }
49
50
 
51
+ function getCustomerSites (org: string) {
52
+ return useNuxtApp().$api<Record<string, any>>(
53
+ `/api/customer-sites?page=1&search=&limit=10&sort=&order=asc&status=active&org=${org}`,
54
+ {
55
+ method: 'GET'
56
+ }
57
+ )
58
+ }
59
+
50
60
  return {
51
61
  add,
52
62
  getAll,
53
63
  addViaInvite,
54
64
  getBySiteAsServiceProvider,
65
+ getCustomerSites
55
66
  };
56
67
  }
@@ -130,15 +130,15 @@ export default function useFeedback() {
130
130
  });
131
131
  }
132
132
 
133
- function deleteFeedback(id: string) {
134
- return useNuxtApp().$api<Record<string, any>>(
135
- `/api/feedbacks/deleted/feedback/${id}`,
136
- {
137
- method: "PUT",
138
- // query: { id },
139
- }
140
- );
141
- }
133
+ // function deleteFeedback(id: string) {
134
+ // return useNuxtApp().$api<Record<string, any>>(
135
+ // `/api/feedbacks/deleted/feedback/${id}`,
136
+ // {
137
+ // method: "PUT",
138
+ // // query: { id },
139
+ // }
140
+ // );
141
+ // }
142
142
 
143
143
  function updateFeedbackServiceProvider(id: string, serviceProvider: string) {
144
144
  return useNuxtApp().$api<Record<string, any>>(
@@ -182,6 +182,12 @@ export default function useFeedback() {
182
182
  );
183
183
  }
184
184
 
185
+ function deleteFeedback(id: string) {
186
+ return useNuxtApp().$api<Record<string, any>>(`/api/feedbacks/${id}`, {
187
+ method: "DELETE",
188
+ });
189
+ }
190
+
185
191
  return {
186
192
  feedbacks,
187
193
  setFeedback,
@@ -0,0 +1,34 @@
1
+ type ProfilePhotoMessageLike = {
2
+ createdByProfile?: unknown;
3
+ };
4
+
5
+ export default function useProfilePhotoSrc() {
6
+ const { API_DO_STORAGE_ENDPOINT } = useRuntimeConfig().public;
7
+
8
+ const isProbablyUrl = (value: string) =>
9
+ /^(https?:\/\/|data:|blob:)/i.test(value);
10
+
11
+ const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
12
+
13
+ const joinUrl = (base: string, ...parts: string[]) => {
14
+ const safeBase = base.replace(/\/+$/g, "");
15
+ const safeParts = parts.map((p) => trimSlashes(p)).filter(Boolean);
16
+ return [safeBase, ...safeParts].join("/");
17
+ };
18
+
19
+ const getProfilePhotoSrc = (message: ProfilePhotoMessageLike) => {
20
+ const rawKey = message?.createdByProfile;
21
+ if (typeof rawKey !== "string") return "";
22
+
23
+ const key = rawKey.trim();
24
+ if (!key) return "";
25
+
26
+ if (isProbablyUrl(key)) return key;
27
+
28
+ return joinUrl(String(API_DO_STORAGE_ENDPOINT || ""), key);
29
+ };
30
+
31
+ return {
32
+ getProfilePhotoSrc,
33
+ };
34
+ }
@@ -14,6 +14,30 @@ export default function useUser() {
14
14
  });
15
15
  }
16
16
 
17
+ function inviteUserClient({
18
+ email = '',
19
+ app = '',
20
+ role = '',
21
+ name = '',
22
+ org = '',
23
+ site = '',
24
+ siteName = ''
25
+ } = {}) {
26
+ return useNuxtApp().$api<Record<string, any>>('/api/auth/invite/user', {
27
+ method: 'POST',
28
+ body: {
29
+ email,
30
+ app,
31
+ role,
32
+ name,
33
+ org,
34
+ ...(site && { siteId: site }),
35
+ ...(siteName && { siteName })
36
+ }
37
+ })
38
+ }
39
+
40
+
17
41
  function updateName(name = "") {
18
42
  return useNuxtApp().$api<Record<string, any>>("/api/users/name", {
19
43
  method: "PUT",
@@ -127,6 +151,7 @@ export default function useUser() {
127
151
  createUserByInvite,
128
152
  getById,
129
153
  createUserByVerification,
130
- getUserByEmail
154
+ getUserByEmail,
155
+ inviteUserClient
131
156
  };
132
157
  }
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.35",
5
+ "version": "1.11.37",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {