@iservice365/layer-common 1.3.0 → 1.3.2

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.
@@ -1,95 +1,33 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
- <TableMain
4
- :headers="headers"
5
- :items="items"
6
- :loading="getVisitorPending"
7
- :page="page"
8
- :pages="pages"
9
- :extension-height="110"
10
- :pageRange="pageRange"
11
- :canCreate="canAddVisitor"
12
- @refresh="getVisitorRefresh"
13
- @update:page="handleUpdatePage"
14
- createLabel="Add Visitor"
15
- show-header
16
- @row-click="handleRowClick"
17
- @create="dialog.showSelection = true"
18
- >
3
+ <TableMain :headers="headers" :items="items" :loading="getVisitorPending" :page="page" :pages="pages"
4
+ :extension-height="110" :offset="300" :pageRange="pageRange" :canCreate="canAddVisitor"
5
+ @refresh="getVisitorRefresh" @update:page="handleUpdatePage" createLabel="Add Visitor" show-header
6
+ @row-click="handleRowClick" @create="dialog.showSelection = true">
19
7
  <template #extension>
20
8
  <v-row no-gutters class="w-100 d-flex flex-column">
21
- <v-tabs
22
- v-model="activeTab"
23
- color="primary"
24
- :height="40"
25
- @update:model-value="toRoute"
26
- class="w-100"
27
- >
28
- <v-tab
29
- v-for="tab in tabOptions"
30
- :value="tab.status"
31
- :key="tab.status"
32
- class="text-capitalize"
33
- >
9
+ <v-tabs v-model="activeTab" color="primary" :height="40" @update:model-value="toRoute" class="w-100">
10
+ <v-tab v-for="tab in tabOptions" :value="tab.status" :key="tab.status" class="text-capitalize">
34
11
  {{ tab.name }}
35
12
  </v-tab>
36
13
  </v-tabs>
37
14
 
38
- <v-card
39
- class="w-100 px-3 d-flex align-center ga-5 py-2"
40
- flat
41
- :height="60"
42
- >
43
- <v-text-field
44
- v-model="searchInput"
45
- density="compact"
46
- placeholder="Search"
47
- clearable
48
- max-width="300"
49
- append-inner-icon="mdi-magnify"
50
- hide-details
51
- />
52
- <v-checkbox
53
- v-model="displayCheckedOutOnly"
54
- class="text-subtitle-2"
55
- hide-details
56
- >
15
+ <v-card class="w-100 px-3 d-flex align-center ga-5 py-2" flat :height="60">
16
+ <v-text-field v-model="searchInput" density="compact" placeholder="Search" clearable max-width="300"
17
+ append-inner-icon="mdi-magnify" hide-details />
18
+ <v-checkbox v-model="displayCheckedOutOnly" class="text-subtitle-2" hide-details>
57
19
  <template #label>
58
20
  <span class="text-caption">Not Checked Out</span>
59
21
  </template>
60
22
  </v-checkbox>
61
- <InputDateTimePicker
62
- v-model:utc="dateFrom"
63
- density="compact"
64
- hide-details
65
- />
66
- <InputDateTimePicker
67
- v-model:utc="dateTo"
68
- density="compact"
69
- hide-details
70
- />
71
- <v-select
72
- v-model="filterTypes"
73
- label="Filter by types"
74
- item-title="label"
75
- item-value="value"
76
- :items="visitorSelection"
77
- density="compact"
78
- clearable
79
- multiple
80
- max-width="200"
81
- hide-details
82
- >
23
+ <InputDateTimePicker v-model:utc="dateFrom" density="compact" hide-details />
24
+ <InputDateTimePicker v-model:utc="dateTo" density="compact" hide-details />
25
+ <v-select v-model="filterTypes" label="Filter by types" item-title="label" item-value="value"
26
+ :items="visitorSelection" density="compact" clearable multiple max-width="200" hide-details>
83
27
  <template v-slot:selection="{ item, index }">
84
28
  <div class="d-flex align-center text-caption text-nowrap">
85
- <v-chip
86
- v-if="index === 0"
87
- color="error"
88
- :text="filterTypeSelectionLabel()"
89
- size="x-small"
90
- variant="tonal"
91
- class="ml-2"
92
- />
29
+ <v-chip v-if="index === 0" color="error" :text="filterTypeSelectionLabel()" size="x-small"
30
+ variant="tonal" class="ml-2" />
93
31
  </div>
94
32
  </template>
95
33
  </v-select>
@@ -111,7 +49,7 @@
111
49
  <v-icon icon="mdi-user" size="15" />
112
50
  <span v-if="item.type === 'contractor'" class="text-capitalize">{{
113
51
  formatCamelCaseToWords(item.contractorType)
114
- }}</span>
52
+ }}</span>
115
53
  <span v-else class="text-capitalize">{{ formatType(item) }}</span>
116
54
  </span>
117
55
  <span class="d-flex align-center ga-2">
@@ -149,100 +87,57 @@
149
87
  <v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
150
88
  <span class="text-capitalize">{{
151
89
  UTCToLocalTIme(item.checkIn) || "-"
152
- }}</span>
90
+ }}</span>
153
91
  </span>
154
92
  <span class="d-flex align-center ga-2">
155
93
  <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" v-if="item.checkOut" />
156
94
  <template v-if="item.checkOut">
157
95
  <span class="text-capitalize">{{
158
96
  UTCToLocalTIme(item.checkOut) || "_"
159
- }}</span>
97
+ }}</span>
160
98
  <span v-if="item?.manualCheckout">
161
- <TooltipInfo
162
- text="Manual Checkout"
163
- density="compact"
164
- size="x-small"
165
- />
99
+ <TooltipInfo text="Manual Checkout" density="compact" size="x-small" />
166
100
  </span>
167
101
  </template>
168
102
  <span v-else>
169
- <v-btn
170
- size="x-small"
171
- class="text-capitalize"
172
- color="red"
173
- text="Checkout"
174
- :loading="loading.checkingOut && item?._id === selectedVisitorId"
175
- @click.stop="handleCheckout(item._id)"
176
- v-if="canCheckoutVisitor"
177
- />
103
+ <v-btn size="x-small" class="text-capitalize" color="red" text="Checkout"
104
+ :loading="loading.checkingOut && item?._id === selectedVisitorId" @click.stop="handleCheckout(item._id)"
105
+ v-if="canCheckoutVisitor" />
178
106
  </span>
179
107
  </span>
180
108
  </template>
181
109
  </TableMain>
182
110
 
183
111
  <v-dialog v-model="dialog.showSelection" width="450" persistent>
184
- <VisitorFormSelection
185
- @cancel="dialog.showSelection = false"
186
- @select="handleSelectVisitorType"
187
- />
112
+ <VisitorFormSelection @cancel="dialog.showSelection = false" @select="handleSelectVisitorType" />
188
113
  </v-dialog>
189
114
 
190
- <v-dialog
191
- v-model="dialog.addVisitor"
192
- v-if="activeVisitorFormType"
193
- width="450"
194
- persistent
195
- >
196
- <VisitorForm
197
- mode="add"
198
- :org="orgId"
199
- :site="siteId"
200
- :type="activeVisitorFormType"
201
- @back="handleClickBack"
202
- @done="handleVisitorFormDone"
203
- @done:more="handleVisitorFormCreateMore"
204
- />
115
+ <v-dialog v-model="dialog.addVisitor" v-if="activeVisitorFormType" width="450" persistent>
116
+ <VisitorForm mode="add" :org="orgId" :site="siteId" :type="activeVisitorFormType" @back="handleClickBack"
117
+ @done="handleVisitorFormDone" @done:more="handleVisitorFormCreateMore" />
205
118
  </v-dialog>
206
119
 
207
120
  <v-dialog v-model="dialog.viewVisitor" width="450" persistent>
208
- <VehicleUpdateMoreAction
209
- title="Preview"
210
- :can-update="canUpdateVisitor"
211
- :can-delete="canDeleteVisitor"
212
- @close="dialog.viewVisitor = false"
213
- edit-button-label="Edit Visitor"
214
- delete-button-label="Delete Visitor"
215
- @delete="handleDeleteVisitor"
216
- >
121
+ <VehicleUpdateMoreAction title="Preview" :can-update="canUpdateVisitor" :can-delete="canDeleteVisitor"
122
+ @close="dialog.viewVisitor = false" edit-button-label="Edit Visitor" delete-button-label="Delete Visitor"
123
+ @delete="handleDeleteVisitor">
217
124
  <template v-slot:content>
218
125
  <v-row no-gutters class="mb-4">
219
126
  <v-col v-for="(label, key) in formattedFields" :key="key" cols="12">
220
- <span
221
- v-if="key === 'checkOut' && !selectedVisitorObject[key] && canCheckoutVisitor"
222
- class="d-flex align-center"
223
- >
127
+ <span v-if="
128
+ key === 'checkOut' &&
129
+ !selectedVisitorObject[key] &&
130
+ canCheckoutVisitor
131
+ " class="d-flex align-center">
224
132
  <strong>{{ label }}:</strong>
225
- <v-btn
226
- size="x-small"
227
- class="ml-3 text-capitalize"
228
- color="red"
229
- text="Checkout"
230
- :disabled="loading.checkingOut"
231
- @click="handleCheckout(selectedVisitorId as string)"
232
- />
133
+ <v-btn size="x-small" class="ml-3 text-capitalize" color="red" text="Checkout"
134
+ :disabled="loading.checkingOut" @click="handleCheckout(selectedVisitorId as string)" />
233
135
  </span>
234
136
 
235
- <span
236
- v-else-if="selectedVisitorObject[key]"
237
- class="d-flex ga-3 align-center"
238
- ><strong>{{ label }}:</strong>
137
+ <span v-else-if="selectedVisitorObject[key]" class="d-flex ga-3 align-center"><strong>{{ label
138
+ }}:</strong>
239
139
  {{ formatValues(key, selectedVisitorObject[key]) }}
240
- <TooltipInfo
241
- v-if="key === 'checkOut'"
242
- text="Manual Checkout"
243
- density="compact"
244
- size="x-small"
245
- />
140
+ <TooltipInfo v-if="key === 'checkOut'" text="Manual Checkout" density="compact" size="x-small" />
246
141
  </span>
247
142
  </v-col>
248
143
  </v-row>
@@ -251,12 +146,9 @@
251
146
  </v-dialog>
252
147
 
253
148
  <v-dialog v-model="dialog.deleteConfirmation" width="450" persistent>
254
- <CardDeleteConfirmation
255
- prompt-title="Are you sure want to delete this visitor?"
256
- :loading="loading.deletingVisitor"
257
- @close="dialog.deleteConfirmation = false"
258
- @delete="handleProceedDeleteVisitor"
259
- />
149
+ <CardDeleteConfirmation prompt-title="Are you sure want to delete this visitor?"
150
+ :loading="loading.deletingVisitor" @close="dialog.deleteConfirmation = false"
151
+ @delete="handleProceedDeleteVisitor" />
260
152
  </v-dialog>
261
153
 
262
154
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
@@ -309,18 +201,21 @@ const {
309
201
  const { debounce, formatCamelCaseToWords, formatDate, UTCToLocalTIme } =
310
202
  useUtils();
311
203
  const { formatLocation } = useSecurityUtils();
312
- const { status: visitorStatus } = useRoute().query;
313
- const { org: orgId, site: siteId } = useRoute().params as {
204
+ // const { status: visitorStatus, search } = useRoute().query as { status: string, search: string};
205
+
206
+ const route = useRoute()
207
+ const router = useRouter()
208
+ const { org: orgId, site: siteId } = route.params as {
314
209
  org: string;
315
210
  site: string;
316
211
  };
317
- const routeName = useRoute().name;
212
+ const routeName = route.name;
318
213
 
319
214
  const items = ref<Array<Record<string, any>>>([]);
320
215
  const page = ref(1);
321
216
  const pages = ref(0);
322
217
  const pageRange = ref("-- - -- of --");
323
- const activeTab = ref(visitorStatus ?? "registered");
218
+ const activeTab = ref("registered");
324
219
  const activeVisitorFormType = ref<TVisitorType | null>(null);
325
220
  const selectedVisitorId = ref<string | null>(""); // selected visitor for viewing/actions
326
221
 
@@ -328,7 +223,7 @@ const selectedVisitorId = ref<string | null>(""); // selected visitor for viewin
328
223
  const searchInput = ref("");
329
224
  const dateFrom = ref("");
330
225
  const dateTo = ref("");
331
- const filterTypes = ref([]);
226
+ const filterTypes = ref<TVisitorType[]>([]);
332
227
  const displayCheckedOutOnly = ref<boolean>(false);
333
228
 
334
229
  const message = ref("");
@@ -393,21 +288,20 @@ const {
393
288
  refresh: getVisitorRefresh,
394
289
  pending: getVisitorPending,
395
290
  } = await useLazyAsyncData(
396
- `get-all-visitors-${visitorStatus}`,
291
+ `get-all-visitors-${activeTab.value}-${page.value}`
292
+ ,
397
293
  () =>
398
294
  getVisitors({
399
295
  page: page.value,
400
- org: orgId,
401
296
  site: siteId,
402
297
  search: searchInput.value,
403
298
  dateTo: dateTo.value,
404
299
  dateFrom: dateFrom.value,
405
300
  type: filterTypes.value.filter(Boolean).join(","),
406
- displayNoCheckOut: displayCheckedOutOnly.value,
407
- status: activeTab.value as string
301
+ status: activeTab.value as string,
408
302
  }),
409
303
  {
410
- watch: [page, activeTab],
304
+ watch: [page, () => route.query],
411
305
  }
412
306
  );
413
307
 
@@ -420,9 +314,9 @@ watch(getVisitorReq, (newData: any) => {
420
314
  });
421
315
 
422
316
  const selectedVisitorObject = computed(() => {
423
- const obj = items.value.find((x) => x?._id === selectedVisitorId.value);
317
+ const obj = items.value.find((x: any) => x?._id === selectedVisitorId.value);
424
318
  if (!obj) return {};
425
- const type = obj?.type;
319
+ const type = obj?.type as TVisitorType | undefined;
426
320
  if (!type) return {};
427
321
  let includedKeys: string[] = ["checkIn", "checkOut"];
428
322
  includedKeys.unshift(...(typeFieldMap[type] ?? []));
@@ -554,16 +448,42 @@ function getUTCDates() {
554
448
  return { dateYesterday, dateToday };
555
449
  }
556
450
 
557
- // filter debounce search
558
- const debounceSearch = debounce(getVisitorRefresh, 500);
451
+
452
+ const updateRouteQuery = debounce(
453
+ (search: string, from: string, to: string, types: string[], status) => {
454
+ router.replace({
455
+ query: {
456
+ ...route.query,
457
+ search: search || undefined,
458
+ dateFrom: from || undefined,
459
+ dateTo: to || undefined,
460
+ type: types.filter(Boolean).join(",") || undefined,
461
+ status: status || undefined,
462
+ },
463
+ });
464
+ },
465
+ 500 // wait 500ms after last input
466
+ );
559
467
 
560
468
  watch(
561
- [searchInput, dateTo, dateFrom, displayCheckedOutOnly, filterTypes],
562
- ([]) => {
563
- debounceSearch();
469
+ [searchInput, dateFrom, dateTo, filterTypes, activeTab],
470
+ ([search, from, to, types, status]) => {
471
+ updateRouteQuery(search, from, to, types, status)
472
+
564
473
  },
565
- { immediate: false, deep: true }
474
+ { deep: true }
566
475
  );
476
+
477
+
478
+
479
+ onMounted(() => {
480
+ activeTab.value = (route.query.status as string) || "unregistered"
481
+ searchInput.value = (route.query.search as string) || "";
482
+ dateFrom.value = (route.query.dateFrom as string) || "";
483
+ dateTo.value = (route.query.dateTo as string) || "";
484
+ filterTypes.value = ((route.query.type as string)?.split(",") || []).filter(Boolean) as TVisitorType[];
485
+
486
+ })
567
487
  </script>
568
488
 
569
489
  <style scoped></style>
@@ -26,7 +26,11 @@
26
26
  variant="outlined"
27
27
  density="compact"
28
28
  class="mb-2"
29
- clearable
29
+ :clearable="
30
+ props.createdFrom === 'feedback' && workOrder.subject !== ''
31
+ ? false
32
+ : true
33
+ "
30
34
  :readonly="
31
35
  props.createdFrom === 'feedback' && workOrder.subject !== ''
32
36
  ? true
@@ -222,7 +222,7 @@ export default function useBuilding() {
222
222
 
223
223
  function updateById(
224
224
  id: string,
225
- data: Pick<TBuilding, "name" | "block" | "levels">
225
+ data: Pick<TBuilding, "name" | "block" | "levels" | "buildingFloorPlan">
226
226
  ) {
227
227
  return useNuxtApp().$api(`/api/buildings/id/${id}`, {
228
228
  method: "PATCH",
@@ -120,11 +120,88 @@ export function useCommonPermissions() {
120
120
  },
121
121
  };
122
122
 
123
+ const visitorManagementPermissions: Record<string, TPermission> = {
124
+ "add-visitor": {
125
+ check: true,
126
+ description: "Allows the user to add a new visitor to the system.",
127
+ },
128
+ "see-all-visitor": {
129
+ check: true,
130
+ description: "Allows the user to view the list of all visitors.",
131
+ },
132
+ "see-visitor-details": {
133
+ check: true,
134
+ description: "Allows the user to view the details of a specific visitor.",
135
+ },
136
+ "update-visitor": {
137
+ check: true,
138
+ description: "Allows the user to update visitor details.",
139
+ },
140
+ "delete-visitor": {
141
+ check: true,
142
+ description:
143
+ "Allows the user to remove a visitor from the system permanently.",
144
+ },
145
+ };
146
+
147
+ const buildingManagementPermissions: Record<string, TPermission> = {
148
+ "add-building": {
149
+ check: true,
150
+ description: "Allows the user to add a new building to the system.",
151
+ },
152
+ "see-all-buildings": {
153
+ check: true,
154
+ description: "Allows the user to view the list of all buildings.",
155
+ },
156
+ "see-building-details": {
157
+ check: true,
158
+ description:
159
+ "Allows the user to view the details of a specific building.",
160
+ },
161
+ "update-building": {
162
+ check: true,
163
+ description: "Allows the user to update building details.",
164
+ },
165
+ "delete-building": {
166
+ check: true,
167
+ description:
168
+ "Allows the user to remove a building from the system permanently.",
169
+ },
170
+ };
171
+
172
+ const buildingUnitManagementPermissions: Record<string, TPermission> = {
173
+ "add-building-unit": {
174
+ check: true,
175
+ description: "Allows the user to add a new building unit to the system.",
176
+ },
177
+ "see-all-building-units": {
178
+ check: true,
179
+ description: "Allows the user to view the list of all building units.",
180
+ },
181
+ "see-building-unit-details": {
182
+ check: true,
183
+ description:
184
+ "Allows the user to view the details of a specific building unit.",
185
+ },
186
+ "update-building-unit": {
187
+ check: true,
188
+ description: "Allows the user to update building unit details.",
189
+ },
190
+ "delete-building-unit": {
191
+ check: true,
192
+ description:
193
+ "Allows the user to remove a building unit from the system permanently.",
194
+ },
195
+ };
196
+
123
197
  return {
124
198
  invitationPermissions,
125
199
  memberPermissions,
126
200
  rolePermissions,
127
201
  feedbackPermissions,
128
202
  workOrderPermissions,
203
+ visitorManagementPermissions,
204
+ buildingManagementPermissions,
205
+ buildingUnitManagementPermissions
129
206
  };
130
207
  }
@@ -13,36 +13,44 @@ export default function(){
13
13
  type="",
14
14
  displayNoCheckOut=false
15
15
  } = {}) {
16
- return await $fetch<Record<string, any>>("/api/people", {
16
+ return await useNuxtApp().$api<Record<string, any>>("/api/people", {
17
17
  method: "GET",
18
18
  query: { page, limit, sort, search, org, site, dateTo, dateFrom, type }
19
19
  });
20
20
  }
21
21
 
22
+ async function findPersonByNRIC(nric: string): Promise<null | Partial<TPeople>> {
23
+ return await $fetch<Record<string, any>>(`/api/people/nric/${nric}`, {
24
+ method: "GET",
25
+ });
26
+ }
27
+
22
28
  async function create(payload: Partial<TPeoplePayload>){
23
- return await $fetch<Record<string, any>>("/api/people", {
29
+ return await useNuxtApp().$api<Record<string, any>>("/api/people", {
24
30
  method: "POST",
25
31
  body: payload,
26
32
  });
27
33
  }
28
34
 
29
35
  async function updateById(_id: string, payload: Partial<TPeoplePayload>){
30
- return await $fetch<Record<string, any>>(`/api/people/id/${_id}`, {
36
+ return await useNuxtApp().$api<Record<string, any>>(`/api/people/id/${_id}`, {
31
37
  method: "PUT",
32
38
  body: payload,
33
39
  });
34
40
  }
35
41
 
36
42
  async function deleteById(_id: string){
37
- return await $fetch<Record<string, any>>(`/api/people/id/${_id}`, {
43
+ return await useNuxtApp().$api<Record<string, any>>(`/api/people/id/${_id}`, {
38
44
  method: "DELETE",
39
45
  });
40
46
  }
41
47
 
48
+
42
49
  return {
43
50
  create,
44
51
  getAll,
45
52
  updateById,
46
- deleteById
53
+ deleteById,
54
+ findPersonByNRIC
47
55
  }
48
56
  }