@iservice365/layer-common 1.1.0 → 1.3.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/components/CameraForm.vue +264 -0
  3. package/components/CameraMain.vue +352 -0
  4. package/components/Card/DeleteConfirmation.vue +51 -0
  5. package/components/Card/MemberInfoSummary.vue +44 -0
  6. package/components/Chat/Information.vue +28 -11
  7. package/components/Dialog/DeleteConfirmation.vue +51 -0
  8. package/components/Dialog/UpdateMoreAction.vue +99 -0
  9. package/components/Feedback/Form.vue +17 -3
  10. package/components/FeedbackDetail.vue +0 -11
  11. package/components/FeedbackMain.vue +21 -11
  12. package/components/Input/DateTimePicker.vue +10 -5
  13. package/components/Input/File.vue +1 -1
  14. package/components/Input/FileV2.vue +106 -63
  15. package/components/Input/InputPhoneNumberV2.vue +114 -0
  16. package/components/Input/NRICNumber.vue +41 -0
  17. package/components/Input/PhoneNumber.vue +1 -0
  18. package/components/Input/VehicleNumber.vue +49 -0
  19. package/components/NumberSettingField.vue +107 -0
  20. package/components/PeopleForm.vue +452 -0
  21. package/components/TableMain.vue +2 -1
  22. package/components/VehicleUpdateMoreAction.vue +84 -0
  23. package/components/VisitorForm.vue +712 -0
  24. package/components/VisitorFormSelection.vue +53 -0
  25. package/components/VisitorManagement.vue +569 -0
  26. package/components/WorkOrder/Create.vue +87 -49
  27. package/components/WorkOrder/Main.vue +17 -12
  28. package/composables/useBuilding.ts +250 -0
  29. package/composables/useBuildingUnit.ts +116 -0
  30. package/composables/useFeedback.ts +3 -3
  31. package/composables/useFile.ts +7 -9
  32. package/composables/useLocal.ts +67 -0
  33. package/composables/usePeople.ts +48 -0
  34. package/composables/useSecurityUtils.ts +18 -0
  35. package/composables/useSiteSettings.ts +111 -0
  36. package/composables/useUtils.ts +30 -1
  37. package/composables/useVisitor.ts +80 -0
  38. package/composables/useWorkOrder.ts +3 -3
  39. package/package.json +1 -1
  40. package/plugins/vuetify.ts +6 -1
  41. package/types/building.d.ts +19 -0
  42. package/types/camera.d.ts +31 -0
  43. package/types/people.d.ts +22 -0
  44. package/types/select.d.ts +4 -0
  45. package/types/site.d.ts +10 -7
  46. package/types/visitor.d.ts +42 -0
  47. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,53 @@
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 text-capitalize">
6
+ Add Visitor
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5">
12
+ <span class="text-subtitle-2 w-100 font-weight-medium d-flex justify-center mb-3">Please Select Visitor Type</span>
13
+ <template v-for="item in selection" :key="item.value">
14
+ <v-btn color="primary-button" block variant="flat" rounded="md" size="48" :text="item.label" @click="select(item.value)" class="my-2 text-capitalize text-subtitle-2" />
15
+ </template>
16
+ </v-card-text>
17
+
18
+ <v-toolbar density="compact">
19
+ <v-row no-gutters>
20
+ <v-col cols="12">
21
+ <v-btn
22
+ tile
23
+ block
24
+ variant="text"
25
+ class="text-none"
26
+ size="48"
27
+ @click="cancel"
28
+ >
29
+ Cancel
30
+ </v-btn>
31
+ </v-col>
32
+ </v-row>
33
+ </v-toolbar>
34
+ </v-card>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ const prop = defineProps({
39
+ });
40
+
41
+ const emit = defineEmits(['cancel', 'select']);
42
+ const { visitorSelection: selection} = useVisitor();
43
+
44
+
45
+
46
+ function cancel() {
47
+ emit("cancel");
48
+ }
49
+
50
+ function select(value: string) {
51
+ emit("select", value);
52
+ }
53
+ </script>
@@ -0,0 +1,569 @@
1
+ <template>
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
+ >
19
+ <template #extension>
20
+ <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
+ >
34
+ {{ tab.name }}
35
+ </v-tab>
36
+ </v-tabs>
37
+
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
+ >
57
+ <template #label>
58
+ <span class="text-caption">Not Checked Out</span>
59
+ </template>
60
+ </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
+ >
83
+ <template v-slot:selection="{ item, index }">
84
+ <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
+ />
93
+ </div>
94
+ </template>
95
+ </v-select>
96
+ </v-card>
97
+ </v-row>
98
+ </template>
99
+
100
+ <template v-slot:item.name="{ item }">
101
+ <span class="d-flex align-center ga-2">
102
+ <span>
103
+ <AvatarMain :name="item?.name" :size="20" :id="item?._id" />
104
+ </span>
105
+ <span class="text-capitalize">{{ item?.name }}</span>
106
+ </span>
107
+ </template>
108
+
109
+ <template v-slot:item.type-company="{ item }">
110
+ <span class="d-flex align-center ga-2">
111
+ <v-icon icon="mdi-user" size="15" />
112
+ <span v-if="item.type === 'contractor'" class="text-capitalize">{{
113
+ formatCamelCaseToWords(item.contractorType)
114
+ }}</span>
115
+ <span v-else class="text-capitalize">{{ formatType(item) }}</span>
116
+ </span>
117
+ <span class="d-flex align-center ga-2">
118
+ <v-icon icon="mdi-domain" size="15" />
119
+ <span class="text-capitalize">{{ item?.company || "N/A" }}</span>
120
+ </span>
121
+ </template>
122
+
123
+ <template v-slot:item.location="{ item }">
124
+ <span class="d-flex align-center ga-2">
125
+ <v-icon icon="mdi-storefront-outline" size="15" />
126
+ <span class="text-capitalize">{{
127
+ formatLocation({
128
+ block: item.block,
129
+ level: item.level,
130
+ unit: item.unitName,
131
+ })
132
+ }}</span>
133
+ </span>
134
+ </template>
135
+
136
+ <template v-slot:item.contact-vehicleNumber="{ item }">
137
+ <span class="d-flex align-center ga-2">
138
+ <v-icon icon="mdi-phone" size="15" />
139
+ <span class="text-capitalize">{{ item?.contact || "N/A" }}</span>
140
+ </span>
141
+ <span class="d-flex align-center ga-2">
142
+ <v-icon icon="mdi-car-back" size="15" />
143
+ <span class="text-capitalize">{{ item?.plateNumber || "N/A" }}</span>
144
+ </span>
145
+ </template>
146
+
147
+ <template v-slot:item.checkin-out="{ item }">
148
+ <span class="d-flex align-center ga-2">
149
+ <v-icon icon="mdi-clock-time-four-outline" color="green" size="20" />
150
+ <span class="text-capitalize">{{
151
+ UTCToLocalTIme(item.checkIn) || "-"
152
+ }}</span>
153
+ </span>
154
+ <span class="d-flex align-center ga-2">
155
+ <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" v-if="item.checkOut" />
156
+ <template v-if="item.checkOut">
157
+ <span class="text-capitalize">{{
158
+ UTCToLocalTIme(item.checkOut) || "_"
159
+ }}</span>
160
+ <span v-if="item?.manualCheckout">
161
+ <TooltipInfo
162
+ text="Manual Checkout"
163
+ density="compact"
164
+ size="x-small"
165
+ />
166
+ </span>
167
+ </template>
168
+ <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
+ />
178
+ </span>
179
+ </span>
180
+ </template>
181
+ </TableMain>
182
+
183
+ <v-dialog v-model="dialog.showSelection" width="450" persistent>
184
+ <VisitorFormSelection
185
+ @cancel="dialog.showSelection = false"
186
+ @select="handleSelectVisitorType"
187
+ />
188
+ </v-dialog>
189
+
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
+ />
205
+ </v-dialog>
206
+
207
+ <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
+ >
217
+ <template v-slot:content>
218
+ <v-row no-gutters class="mb-4">
219
+ <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
+ >
224
+ <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
+ />
233
+ </span>
234
+
235
+ <span
236
+ v-else-if="selectedVisitorObject[key]"
237
+ class="d-flex ga-3 align-center"
238
+ ><strong>{{ label }}:</strong>
239
+ {{ formatValues(key, selectedVisitorObject[key]) }}
240
+ <TooltipInfo
241
+ v-if="key === 'checkOut'"
242
+ text="Manual Checkout"
243
+ density="compact"
244
+ size="x-small"
245
+ />
246
+ </span>
247
+ </v-col>
248
+ </v-row>
249
+ </template>
250
+ </VehicleUpdateMoreAction>
251
+ </v-dialog>
252
+
253
+ <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
+ />
260
+ </v-dialog>
261
+
262
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
263
+ </v-row>
264
+ </template>
265
+
266
+ <script setup lang="ts">
267
+ definePageMeta({
268
+ middleware: ["01-auth", "02-org"],
269
+ });
270
+
271
+ const props = defineProps({
272
+ canAddVisitor: {
273
+ type: Boolean,
274
+ default: true,
275
+ },
276
+ canViewVisitor: {
277
+ type: Boolean,
278
+ default: true,
279
+ },
280
+ canUpdateVisitor: {
281
+ type: Boolean,
282
+ default: true,
283
+ },
284
+ canDeleteVisitor: {
285
+ type: Boolean,
286
+ default: true,
287
+ },
288
+ canCheckoutVisitor: {
289
+ type: Boolean,
290
+ default: true,
291
+ },
292
+ });
293
+
294
+ const headers = [
295
+ { title: "Name", value: "name" },
296
+ { title: "Type/Company", value: "type-company" },
297
+ { title: "Location", value: "location" },
298
+ { title: "Contact/Vehicle No.", value: "contact-vehicleNumber" },
299
+ { title: "Check In/Out", value: "checkin-out" },
300
+ ];
301
+
302
+ const {
303
+ getVisitors,
304
+ visitorSelection,
305
+ typeFieldMap,
306
+ deleteVisitor,
307
+ updateVisitor,
308
+ } = useVisitor();
309
+ const { debounce, formatCamelCaseToWords, formatDate, UTCToLocalTIme } =
310
+ useUtils();
311
+ const { formatLocation } = useSecurityUtils();
312
+ const { status: visitorStatus } = useRoute().query;
313
+ const { org: orgId, site: siteId } = useRoute().params as {
314
+ org: string;
315
+ site: string;
316
+ };
317
+ const routeName = useRoute().name;
318
+
319
+ const items = ref<Array<Record<string, any>>>([]);
320
+ const page = ref(1);
321
+ const pages = ref(0);
322
+ const pageRange = ref("-- - -- of --");
323
+ const activeTab = ref(visitorStatus ?? "registered");
324
+ const activeVisitorFormType = ref<TVisitorType | null>(null);
325
+ const selectedVisitorId = ref<string | null>(""); // selected visitor for viewing/actions
326
+
327
+ //filter states
328
+ const searchInput = ref("");
329
+ const dateFrom = ref("");
330
+ const dateTo = ref("");
331
+ const filterTypes = ref([]);
332
+ const displayCheckedOutOnly = ref<boolean>(false);
333
+
334
+ const message = ref("");
335
+ const messageColor = ref("");
336
+ const messageSnackbar = ref(false);
337
+
338
+ const loading = reactive({
339
+ deletingVisitor: false,
340
+ fetchingVisitors: false,
341
+ checkingOut: false,
342
+ });
343
+
344
+ const dialog = reactive({
345
+ showSelection: false,
346
+ addVisitor: false,
347
+ viewVisitor: false,
348
+ deleteConfirmation: false,
349
+ });
350
+
351
+ const tabOptions = [
352
+ { name: "Registered", status: "registered" },
353
+ { name: "Unregistered", status: "unregistered" },
354
+ ];
355
+
356
+ const formatType = (item: any) =>
357
+ (item.deliveryType ? item.deliveryType + "-" : "") + item.type;
358
+
359
+ const formattedFields = {
360
+ name: "Name",
361
+ nric: "NRIC",
362
+ contact: "Phone Number",
363
+ plateNumber: "Vehicle Number",
364
+ block: "Block",
365
+ level: "Level",
366
+ unitName: "Unit",
367
+ checkIn: "Check In",
368
+ checkOut: "Check Out",
369
+ remarks: "Remarks",
370
+ } as const;
371
+
372
+ function filterTypeSelectionLabel() {
373
+ const length = filterTypes.value.length;
374
+ return `${length} selected ${length === 1 ? "type" : "types"}` as string;
375
+ }
376
+
377
+ function toRoute(status: any) {
378
+ const obj = tabOptions.find((x) => x.status === status);
379
+ if (!obj) return;
380
+ navigateTo({
381
+ name: routeName,
382
+ params: {
383
+ org: orgId,
384
+ },
385
+ query: {
386
+ status: obj.status,
387
+ },
388
+ });
389
+ }
390
+
391
+ const {
392
+ data: getVisitorReq,
393
+ refresh: getVisitorRefresh,
394
+ pending: getVisitorPending,
395
+ } = await useLazyAsyncData(
396
+ `get-all-visitors-${visitorStatus}`,
397
+ () =>
398
+ getVisitors({
399
+ page: page.value,
400
+ org: orgId,
401
+ site: siteId,
402
+ search: searchInput.value,
403
+ dateTo: dateTo.value,
404
+ dateFrom: dateFrom.value,
405
+ type: filterTypes.value.filter(Boolean).join(","),
406
+ displayNoCheckOut: displayCheckedOutOnly.value,
407
+ status: activeTab.value as string
408
+ }),
409
+ {
410
+ watch: [page, activeTab],
411
+ }
412
+ );
413
+
414
+ watch(getVisitorReq, (newData: any) => {
415
+ if (newData) {
416
+ items.value = newData.items ?? [];
417
+ pages.value = newData.pages ?? 0;
418
+ pageRange.value = newData?.pageRange ?? "-- - -- of --";
419
+ }
420
+ });
421
+
422
+ const selectedVisitorObject = computed(() => {
423
+ const obj = items.value.find((x) => x?._id === selectedVisitorId.value);
424
+ if (!obj) return {};
425
+ const type = obj?.type;
426
+ if (!type) return {};
427
+ let includedKeys: string[] = ["checkIn", "checkOut"];
428
+ includedKeys.unshift(...(typeFieldMap[type] ?? []));
429
+ return Object.fromEntries(
430
+ Object.entries(obj).filter(([key]) => includedKeys.includes(key))
431
+ );
432
+ });
433
+
434
+ function formatValues(key: string, value: any) {
435
+ if (!value) return "";
436
+ switch (key) {
437
+ case "unit":
438
+ return value?.name;
439
+ case "checkIn":
440
+ return formatDate(value);
441
+ case "checkOut":
442
+ return formatDate(value);
443
+ }
444
+
445
+ return value;
446
+ }
447
+
448
+ function handleRowClick(data: any) {
449
+ selectedVisitorId.value = data?.item?._id;
450
+ dialog.viewVisitor = true;
451
+ }
452
+
453
+ function handleUpdatePage(newPageNum: number) {
454
+ page.value = newPageNum;
455
+ }
456
+
457
+ function handleSelectVisitorType(type: TVisitorType) {
458
+ dialog.showSelection = false;
459
+ dialog.addVisitor = true;
460
+ activeVisitorFormType.value = type;
461
+ }
462
+
463
+ function handleClickBack() {
464
+ dialog.showSelection = true;
465
+ dialog.addVisitor = false;
466
+ }
467
+
468
+ function handleVisitorFormDone() {
469
+ getVisitorRefresh();
470
+ dialog.showSelection = false;
471
+ dialog.addVisitor = false;
472
+ }
473
+
474
+ function handleVisitorFormCreateMore() {
475
+ getVisitorRefresh();
476
+ dialog.showSelection = true;
477
+ dialog.addVisitor = false;
478
+ }
479
+
480
+ function handleDeleteVisitor() {
481
+ dialog.deleteConfirmation = true;
482
+ dialog.viewVisitor = false;
483
+ }
484
+
485
+ function showMessage(msg: string, color: string) {
486
+ message.value = msg;
487
+ messageColor.value = color;
488
+ messageSnackbar.value = true;
489
+ }
490
+
491
+ async function handleProceedDeleteVisitor() {
492
+ try {
493
+ loading.deletingVisitor = true;
494
+ const userId = selectedVisitorId.value;
495
+ const res = await deleteVisitor(userId as string);
496
+ if (res) {
497
+ showMessage("Visitor successfully deleted!", "info");
498
+ await getVisitorRefresh();
499
+ dialog.deleteConfirmation = false;
500
+ }
501
+ } catch (error: any) {
502
+ const errorMessage = error?.response?._data?.message;
503
+ console.log("[ERROR]", error);
504
+ showMessage(
505
+ errorMessage || "Something went wrong. Please try again later.",
506
+ "error"
507
+ );
508
+ } finally {
509
+ loading.deletingVisitor = false;
510
+ }
511
+ }
512
+
513
+ async function handleCheckout(userId: string) {
514
+ if (!userId) {
515
+ showMessage("Invalid userId", "error");
516
+ return;
517
+ }
518
+ selectedVisitorId.value = userId;
519
+
520
+ try {
521
+ loading.checkingOut = true;
522
+ const res = await updateVisitor(userId as string, {
523
+ checkOut: new Date().toISOString(),
524
+ });
525
+ if (res) {
526
+ showMessage("Visitor successfully checked-out!", "info");
527
+ await getVisitorRefresh();
528
+ dialog.viewVisitor = false;
529
+ }
530
+ } catch (error: any) {
531
+ const errorMessage = error?.response?._data?.message;
532
+ console.log("[ERROR]", error);
533
+ showMessage(
534
+ errorMessage || "Something went wrong. Please try again later.",
535
+ "error"
536
+ );
537
+ } finally {
538
+ loading.checkingOut = false;
539
+ }
540
+ }
541
+
542
+ // get dates in ISO String (UTC Time)
543
+ function getUTCDates() {
544
+ const today = new Date();
545
+ const yesterday = new Date();
546
+ yesterday.setUTCDate(today.getUTCDate() - 1);
547
+
548
+ const dateFrom = ref(yesterday.toISOString()); // yesterday in UTC
549
+ const dateTo = ref(today.toISOString()); // today in UTC
550
+
551
+ const dateYesterday = yesterday.toISOString();
552
+ const dateToday = today.toISOString();
553
+
554
+ return { dateYesterday, dateToday };
555
+ }
556
+
557
+ // filter debounce search
558
+ const debounceSearch = debounce(getVisitorRefresh, 500);
559
+
560
+ watch(
561
+ [searchInput, dateTo, dateFrom, displayCheckedOutOnly, filterTypes],
562
+ ([]) => {
563
+ debounceSearch();
564
+ },
565
+ { immediate: false, deep: true }
566
+ );
567
+ </script>
568
+
569
+ <style scoped></style>