@7365admin1/layer-common 3.0.30 → 3.0.31-staging.10

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 (26) hide show
  1. package/components/AddPassKeyToVisitor.vue +115 -53
  2. package/components/DashboardMain.vue +29 -33
  3. package/components/InvitationMain.vue +4 -3
  4. package/components/ManageChecklistMain.vue +32 -25
  5. package/components/PhotoUpload.vue +19 -4
  6. package/components/VisitorForm.vue +11 -3
  7. package/components/VisitorManagement.vue +109 -30
  8. package/composables/useCleaningSchedulePermission.ts +16 -51
  9. package/package.json +1 -1
  10. package/types/customer.d.ts +2 -1
  11. /package/components/{NFC → Nfc}/NFCPatrolReportMain.vue +0 -0
  12. /package/components/{NFC → Nfc}/NFCPatrolRouteForm.vue +0 -0
  13. /package/components/{NFC → Nfc}/NFCPatrolRouteMain.vue +0 -0
  14. /package/components/{NFC → Nfc}/NFCPatrolSettings.vue +0 -0
  15. /package/components/{NFC → Nfc}/NFCTagForm.vue +0 -0
  16. /package/components/{NFC → Nfc}/NFCTagMain.vue +0 -0
  17. /package/components/{NFC → Nfc}/PatrolReport/DailyReportTab.vue +0 -0
  18. /package/components/{NFC → Nfc}/PatrolReport/MonthlyReportTab.vue +0 -0
  19. /package/components/{NFC → Nfc}/PatrolReport/PatrolActivityTable.vue +0 -0
  20. /package/components/{NFC → Nfc}/PatrolReport/PatrolReportTab.vue +0 -0
  21. /package/components/{NFC → Nfc}/PatrolReport/PatrolRouteSummary.vue +0 -0
  22. /package/components/{NFC → Nfc}/PatrolReport/RemarksDialog.vue +0 -0
  23. /package/components/{NFC → Nfc}/PatrolReport/ReportEmptyState.vue +0 -0
  24. /package/components/{NFC → Nfc}/PatrolReport/ReportFilters.vue +0 -0
  25. /package/components/{NFC → Nfc}/PatrolReport/ReportLocationHeader.vue +0 -0
  26. /package/components/{NFC → Nfc}/PatrolReport/SummaryReportTable.vue +0 -0
@@ -32,11 +32,6 @@
32
32
  color="blue"
33
33
  variant="tonal"
34
34
  size="small"
35
- closable
36
- @click:close="
37
- existingPassToBeReplaced = existingPass;
38
- existingPass = null;
39
- "
40
35
  >
41
36
  {{ existingPass.prefixAndName }}
42
37
  </v-chip>
@@ -45,9 +40,9 @@
45
40
 
46
41
  <!-- New pass selector -->
47
42
  <v-autocomplete
48
- v-if="!existingPass"
49
43
  v-model="selectedPass"
50
44
  v-model:search="passInput"
45
+ :label="existingPass?.keyId ? 'Change pass' : undefined"
51
46
  :hide-no-data="false"
52
47
  :items="passItems"
53
48
  item-title="prefixAndName"
@@ -85,20 +80,20 @@
85
80
  class="d-flex flex-wrap ga-1 mb-2"
86
81
  >
87
82
  <v-chip
88
- v-for="key in existingKeys"
83
+ v-for="key in existingKeys.filter((k) => k.status != 'Removed')"
89
84
  :key="key.keyId"
90
85
  prepend-icon="mdi-key"
91
86
  color="orange"
92
87
  variant="tonal"
93
88
  size="small"
94
89
  closable
95
- @click:close="removeExistingKey(key.keyId)"
90
+ @click:close="removeExistingKey(key)"
96
91
  >
97
92
  {{ key.prefixAndName }}
98
93
  </v-chip>
99
- <span class="text-caption text-medium-emphasis align-self-center"
100
- >(current)</span
101
- >
94
+ <span class="text-caption text-medium-emphasis align-self-center">
95
+ (current)
96
+ </span>
102
97
  </div>
103
98
 
104
99
  <!-- New keys selector -->
@@ -116,6 +111,8 @@
116
111
  density="compact"
117
112
  small-chips
118
113
  :loading="fetchKeysPending"
114
+ clearable
115
+ @update:model-value="selectKeys"
119
116
  >
120
117
  <template v-slot:chip="{ props: chipProps, item }">
121
118
  <v-chip
@@ -205,7 +202,7 @@ const processing = ref(false);
205
202
  const errorMessage = ref("");
206
203
 
207
204
  // New selections
208
- const selectedPass = ref<string>("");
205
+ const selectedPass = ref<string | null>(null);
209
206
  const selectedKeys = ref<string[]>([]);
210
207
  const passInput = ref("");
211
208
  const keyInput = ref("");
@@ -213,12 +210,15 @@ const passItems = ref<TPassKey[]>([]);
213
210
  const keyItems = ref<TPassKey[]>([]);
214
211
 
215
212
  // Existing pass/keys (editable)
216
- const existingPass = ref<{ keyId: string; prefixAndName: string } | null>(null);
217
- const existingPassToBeReplaced = ref<{
213
+ const existingPass = ref<{
218
214
  keyId: string;
219
215
  prefixAndName: string;
216
+ status?: string;
220
217
  } | null>(null);
221
- const existingKeys = ref<{ keyId: string; prefixAndName: string }[]>([]);
218
+
219
+ const existingKeys = ref<
220
+ { keyId: string; prefixAndName: string; status?: string }[]
221
+ >([]);
222
222
 
223
223
  const isEditMode = computed(() => prop.mode === "edit");
224
224
  const showKeys = computed(() => prop.visitor.type === "contractor");
@@ -237,28 +237,82 @@ const passTypesComputed = computed(() => {
237
237
  // Initialise existing values in edit mode
238
238
  onMounted(() => {
239
239
  if (isEditMode.value) {
240
- const vPass = prop.visitor.visitorPass;
240
+ const vPass = prop.visitor.visitorPass?.filter(
241
+ (i) => i.status != "Removed"
242
+ );
241
243
  if (Array.isArray(vPass) && vPass.length > 0) {
242
244
  const p = vPass[0] as any;
243
- existingPass.value = { keyId: p.keyId, prefixAndName: p.prefixAndName };
245
+ existingPass.value = {
246
+ keyId: p.keyId,
247
+ prefixAndName: p.prefixAndName,
248
+ status: p.status,
249
+ };
244
250
  }
245
- const vKeys = prop.visitor.passKeys;
251
+ const vKeys = prop.visitor.passKeys?.filter((i) => i.status != "Removed");
246
252
  if (Array.isArray(vKeys) && vKeys.length > 0) {
247
253
  existingKeys.value = (vKeys as any[]).map((k) => ({
248
254
  keyId: k.keyId,
249
255
  prefixAndName: k.prefixAndName,
256
+ status: k.status,
250
257
  }));
251
258
  }
252
259
  }
253
260
  });
254
261
 
255
- function removeExistingKey(keyId: string) {
256
- existingKeys.value = existingKeys.value.filter((k) => k.keyId !== keyId);
262
+ const previouslyRemovedKeys = ref<any>([]);
263
+
264
+ function removeExistingKey(key: Record<string, any>) {
265
+ const originalKey = existingKeys.value.find((k) => k.keyId === key.keyId);
266
+
267
+ if (
268
+ originalKey &&
269
+ !previouslyRemovedKeys.value.some((k: any) => k.keyId === key.keyId)
270
+ ) {
271
+ previouslyRemovedKeys.value.push({ ...originalKey });
272
+ }
273
+
274
+ existingKeys.value = existingKeys.value.map((i) => {
275
+ if (i.keyId === key.keyId) {
276
+ return {
277
+ ...i,
278
+ status: "Removed",
279
+ };
280
+ }
281
+ return i;
282
+ });
283
+ }
284
+
285
+ function selectKeys() {
286
+ const currentSelections = [...selectedKeys.value];
287
+
288
+ currentSelections.forEach((selectedId) => {
289
+ const removedMatch = previouslyRemovedKeys.value.find(
290
+ (k: any) => k.keyId === selectedId
291
+ );
292
+
293
+ if (removedMatch) {
294
+ existingKeys.value = existingKeys.value.map((i) => {
295
+ if (i.keyId === selectedId) {
296
+ return {
297
+ ...i,
298
+ status: removedMatch.status,
299
+ };
300
+ }
301
+ return i;
302
+ });
303
+
304
+ previouslyRemovedKeys.value = previouslyRemovedKeys.value.filter(
305
+ (k: any) => k.keyId !== selectedId
306
+ );
307
+
308
+ selectedKeys.value = selectedKeys.value.filter((id) => id !== selectedId);
309
+ }
310
+ });
257
311
  }
258
312
 
259
313
  // Exclude already-held pass/keys from selectable lists
260
314
  const existingPassId = computed(() => existingPass.value?.keyId);
261
- const existingKeyIds = computed(() => existingKeys.value.map((k) => k.keyId));
315
+ // const existingKeyIds = computed(() => existingKeys.value.map((k) => k.keyId));
262
316
 
263
317
  const {
264
318
  data: passesData,
@@ -294,43 +348,50 @@ const {
294
348
  })
295
349
  );
296
350
 
297
- watch(passesData, (data: any) => {
298
- const all: TPassKey[] = Array.isArray(data?.items) ? data.items : [];
299
- passItems.value = all.filter((p) => p._id !== existingPassId.value);
300
- });
351
+ // watch(passesData, (data: any) => {
352
+ // const all: TPassKey[] = Array.isArray(data?.items) ? data.items : [];
353
+ // passItems.value = all.filter((p) => p._id !== existingPassId.value);
354
+ // });
301
355
 
302
- watch(keysData, (data: any) => {
303
- const all: TPassKey[] = Array.isArray(data?.items) ? data.items : [];
304
- keyItems.value = all.filter((k) => !existingKeyIds.value.includes(k._id!));
305
- });
356
+ // watch(keysData, (data: any) => {
357
+ // const all: TPassKey[] = Array.isArray(data?.items) ? data.items : [];
358
+ // keyItems.value = all.filter((k) => !existingKeyIds.value.includes(k._id!));
359
+ // });
306
360
 
307
361
  // Re-filter when existing items change
308
362
  watch(
309
- [existingPassId, existingKeyIds],
363
+ [existingPassId, existingKeys, passesData, keysData],
310
364
  () => {
311
365
  const rawPasses = (passesData.value as any)?.items ?? [];
312
366
  passItems.value = rawPasses.filter(
313
367
  (p: TPassKey) => p._id !== existingPassId.value
314
368
  );
369
+
315
370
  const rawKeys = (keysData.value as any)?.items ?? [];
316
- keyItems.value = rawKeys.filter(
317
- (k: TPassKey) => !existingKeyIds.value.includes(k._id!)
371
+ const manuallyRemovedKeys = existingKeys.value
372
+ .filter((k) => k.status === "Removed")
373
+ .map((k) => ({
374
+ _id: k.keyId,
375
+ prefixAndName: k.prefixAndName,
376
+ }));
377
+
378
+ const allExistingKeyIds = existingKeys.value.map((k) => k.keyId);
379
+
380
+ const filteredRawKeys = rawKeys.filter(
381
+ (k: TPassKey) => !allExistingKeyIds.includes(k._id!)
318
382
  );
383
+
384
+ keyItems.value = [...filteredRawKeys, ...manuallyRemovedKeys];
385
+ console.log("keyItems: ", keyItems.value);
319
386
  },
320
- { deep: true }
387
+ { deep: true, immediate: true }
321
388
  );
322
389
 
323
390
  const canSubmit = computed(() => {
324
- if (isEditMode.value) {
325
- // allow save if anything changed: existing items modified OR new items selected
326
- return (
327
- existingPass.value !== null ||
328
- selectedPass.value ||
329
- existingKeys.value.length > 0 ||
330
- selectedKeys.value.length > 0
331
- );
332
- }
333
- return !!selectedPass.value || selectedKeys.value.length > 0;
391
+ const hasRemovedKeys = existingKeys.value.some((k) => k.status === "Removed");
392
+ return (
393
+ !!selectedPass.value || selectedKeys.value.length > 0 || hasRemovedKeys
394
+ );
334
395
  });
335
396
 
336
397
  async function handleSubmit() {
@@ -343,16 +404,13 @@ async function handleSubmit() {
343
404
 
344
405
  // Build final pass list: kept existing + newly selected
345
406
  const finalPasses: { keyId: string; status?: string; add?: boolean }[] = [];
346
- if (existingPass.value) {
347
- finalPasses.push({ keyId: existingPass.value.keyId, status: "Removed" });
348
- }
349
- if (existingPassToBeReplaced.value) {
350
- finalPasses.push({
351
- keyId: existingPassToBeReplaced.value.keyId,
352
- status: "Removed",
353
- });
354
- }
355
407
  if (selectedPass.value) {
408
+ if (existingPass.value) {
409
+ finalPasses.push({
410
+ keyId: existingPass.value.keyId,
411
+ status: "Removed",
412
+ });
413
+ }
356
414
  finalPasses.push({
357
415
  keyId: selectedPass.value,
358
416
  status: "Not Returned",
@@ -364,13 +422,17 @@ async function handleSubmit() {
364
422
  // Build final keys list: kept existing + newly selected
365
423
  if (showKeys.value) {
366
424
  const finalKeys: { keyId: string; status?: string; add?: boolean }[] = [
367
- ...existingKeys.value.map((k) => ({ keyId: k.keyId })),
425
+ ...existingKeys.value.map((k) => ({
426
+ keyId: k.keyId,
427
+ status: k.status,
428
+ })),
368
429
  ...selectedKeys.value.map((keyId) => ({
369
430
  keyId,
370
431
  status: "Not Returned",
371
432
  add: true,
372
433
  })),
373
434
  ];
435
+
374
436
  payload.passKeys = finalKeys;
375
437
  }
376
438
 
@@ -1545,30 +1545,16 @@ const { userAppRole } = useLocalSetup();
1545
1545
  const { downloadPDF } = usePDFDownload();
1546
1546
  const route = useRoute();
1547
1547
 
1548
- const dashboardWidgetPermissions: Record<string, string> = {
1549
- activity: "dashboard:view-activity",
1550
- recentActivity: "dashboard:view-activity",
1551
- taskSchedule: "dashboard:view-task-schedule",
1552
- staffStatus: "dashboard:view-attendance-widget",
1553
- attendance: "dashboard:view-attendance-widget",
1554
- feedbacks: "dashboard:view-feedbacks-widget",
1555
- workOrderStatus: "dashboard:view-work-order-status",
1556
- activePatrol: "dashboard:view-active-patrol",
1557
- upcomingEvents: "dashboard:view-upcoming-events",
1558
- todayReminders: "dashboard:view-today-reminders",
1559
- todayAttentions: "dashboard:view-today-attentions",
1560
- };
1561
-
1562
1548
  const propertyWidgetPermissions: Record<string, string[]> = {
1563
1549
  workOrders: [
1564
- "workOrder:see-all-work-orders",
1550
+ "work_orders:see-all-work-orders",
1565
1551
  "work-order:see-all-work-orders",
1566
1552
  ],
1567
1553
  incidents: ["incident-reports:see-incident-reports"],
1568
1554
  visitors: ["visitorManagement:see-all-visitor"],
1569
1555
  facilityBookings: ["facility-booking-mgmt:see-all-facility-booking"],
1570
1556
  activity: [
1571
- "workOrder:see-all-work-orders",
1557
+ "work_orders:see-all-work-orders",
1572
1558
  "work-order:see-all-work-orders",
1573
1559
  ],
1574
1560
  upcomingEvents: ["event-mgmt:see-all-event"],
@@ -1578,7 +1564,7 @@ const propertyWidgetPermissions: Record<string, string[]> = {
1578
1564
  "facility-booking-mgmt:see-all-facility-booking",
1579
1565
  ],
1580
1566
  workOrderStatus: [
1581
- "workOrder:see-all-work-orders",
1567
+ "work_orders:see-all-work-orders",
1582
1568
  "work-order:see-all-work-orders",
1583
1569
  ],
1584
1570
  feedbacks: ["feedbacks:see-all-feedback"],
@@ -1587,27 +1573,30 @@ const propertyWidgetPermissions: Record<string, string[]> = {
1587
1573
 
1588
1574
  const moduleWidgetPermissions: Record<string, string[]> = {
1589
1575
  openWorkOrder: [
1576
+ "work_orders:see-all-work-orders",
1590
1577
  "work_order:see-all-work-orders",
1591
1578
  "work-order:see-all-work-orders",
1592
1579
  "work-orders:see-all-work-orders",
1593
1580
  ],
1594
1581
  workOrders: [
1582
+ "work_orders:see-all-work-orders",
1595
1583
  "work_order:see-all-work-orders",
1596
1584
  "work-order:see-all-work-orders",
1597
1585
  "work-orders:see-all-work-orders",
1598
1586
  ],
1599
1587
  activity: [
1588
+ "work_orders:see-all-work-orders",
1600
1589
  "work_order:see-all-work-orders",
1601
1590
  "work-order:see-all-work-orders",
1602
1591
  "work-orders:see-all-work-orders",
1592
+ "work_orders:see-all-work-orders",
1603
1593
  ],
1604
- taskSchedule: [
1605
- "cleaning-schedule-mgmt:see-all-schedules",
1606
- "schedule-task-mgmt:see-all-schedule-tasks",
1607
- ],
1608
- taskCompleted: [
1609
- "cleaning-schedule-mgmt:see-all-schedules",
1610
- "schedule-task-mgmt:see-all-schedule-tasks",
1594
+ recentActivity: [
1595
+ "work_orders:see-all-work-orders",
1596
+ "work_order:see-all-work-orders",
1597
+ "work-order:see-all-work-orders",
1598
+ "work-orders:see-all-work-orders",
1599
+ "work_orders:see-all-work-orders",
1611
1600
  ],
1612
1601
  staffStatus: ["attendance-mgmt:see-all-attendance"],
1613
1602
  attendance: ["attendance-mgmt:see-all-attendance"],
@@ -1630,9 +1619,14 @@ const moduleWidgetPermissions: Record<string, string[]> = {
1630
1619
  activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs"],
1631
1620
  };
1632
1621
 
1622
+ function hasSchedulePermission(permissions: string[], action: string) {
1623
+ return permissions.some((permission) =>
1624
+ permission.endsWith(`-schedule-mgmt:${action}`)
1625
+ );
1626
+ }
1627
+
1633
1628
  function canViewWidget(key: string) {
1634
1629
  const permissions = userAppRole.value?.permissions;
1635
- const dashboardPermission = dashboardWidgetPermissions[key];
1636
1630
  const modulePermissions = isPropertyMode.value
1637
1631
  ? propertyWidgetPermissions[key]
1638
1632
  : moduleWidgetPermissions[key];
@@ -1640,20 +1634,22 @@ function canViewWidget(key: string) {
1640
1634
  if (!permissions) return true;
1641
1635
  if (permissions.includes("*")) return true;
1642
1636
 
1637
+ if (
1638
+ !isPropertyMode.value &&
1639
+ ["taskSchedule", "taskCompleted"].includes(key)
1640
+ ) {
1641
+ return hasSchedulePermission(permissions, "see-all-schedules");
1642
+ }
1643
+
1643
1644
  if (isPropertyMode.value && modulePermissions) {
1644
1645
  return modulePermissions.some((permission) =>
1645
1646
  permissions.includes(permission)
1646
1647
  );
1647
1648
  }
1648
1649
 
1649
- if (dashboardPermission || modulePermissions) {
1650
- return (
1651
- (dashboardPermission && permissions.includes(dashboardPermission)) ||
1652
- Boolean(
1653
- modulePermissions?.some((permission) =>
1654
- permissions.includes(permission)
1655
- )
1656
- )
1650
+ if (modulePermissions) {
1651
+ return modulePermissions.some((permission) =>
1652
+ permissions.includes(permission)
1657
1653
  );
1658
1654
  }
1659
1655
 
@@ -221,7 +221,7 @@ const headers = computed(() => {
221
221
  title: "Status",
222
222
  value: "status",
223
223
  },
224
-
224
+
225
225
  ];
226
226
 
227
227
  if (props.status === "pending" && props.cancelInvitation) {
@@ -270,10 +270,11 @@ const {
270
270
  page: page.value,
271
271
  status: props.status,
272
272
  search: headerSearch.value,
273
- type: "user-invite,member-invite",
273
+ // type: "user-invite,member-invite",
274
+ type: "member-invite",
274
275
  app: props.app,
275
276
  orgId: props.org,
276
- siteId: props.siteId,
277
+ // siteId: props.siteId,
277
278
  }),
278
279
  {
279
280
  watch: [() => props.status]
@@ -305,31 +305,38 @@
305
305
 
306
306
  <v-divider />
307
307
 
308
- <v-card-actions class="pa-0">
309
- <v-btn
310
- color="grey"
311
- variant="flat"
312
- rounded="0"
313
- class="flex-grow-1 text-none"
314
- height="48"
315
- :disabled="rejectModal.submitting"
316
- @click="closeRejectDialog"
317
- >
318
- Cancel
319
- </v-btn>
320
- <v-btn
321
- color="error"
322
- variant="flat"
323
- rounded="0"
324
- class="flex-grow-1 text-none"
325
- height="48"
326
- :loading="rejectModal.submitting"
327
- :disabled="!isRejectModalValid || rejectModal.submitting"
328
- @click="submitRejectDialog"
329
- >
330
- Confirm
331
- </v-btn>
332
- </v-card-actions>
308
+ <v-toolbar class="pa-0" density="compact">
309
+ <v-row no-gutters>
310
+ <v-col cols="6" class="pa-0">
311
+ <v-btn
312
+ block
313
+ variant="text"
314
+ class="text-none"
315
+ size="large"
316
+ height="48"
317
+ :disabled="rejectModal.submitting"
318
+ @click="closeRejectDialog"
319
+ >
320
+ Cancel
321
+ </v-btn>
322
+ </v-col>
323
+
324
+ <v-col cols="6" class="pa-0">
325
+ <v-btn
326
+ block
327
+ variant="flat"
328
+ color="black"
329
+ class="text-none font-weight-bold rounded-0"
330
+ height="48"
331
+ :loading="rejectModal.submitting"
332
+ :disabled="!isRejectModalValid || rejectModal.submitting"
333
+ @click="submitRejectDialog"
334
+ >
335
+ Confirm
336
+ </v-btn>
337
+ </v-col>
338
+ </v-row>
339
+ </v-toolbar>
333
340
  </v-card>
334
341
  </v-dialog>
335
342
 
@@ -59,6 +59,21 @@
59
59
  </v-col>
60
60
  </v-row>
61
61
 
62
+ <v-dialog v-model="showPhotoPreview" max-width="900">
63
+ <v-card>
64
+ <v-card-actions class="pa-2 justify-end">
65
+ <v-btn
66
+ icon="mdi-close"
67
+ variant="text"
68
+ @click="showPhotoPreview = false"
69
+ />
70
+ </v-card-actions>
71
+ <v-card-text class="pa-2 pt-0">
72
+ <v-img :src="selectedPhoto" contain max-height="80vh" />
73
+ </v-card-text>
74
+ </v-card>
75
+ </v-dialog>
76
+
62
77
  <v-dialog v-model="photoOptionsDialog" max-width="420">
63
78
  <v-card>
64
79
  <v-card-title class="d-flex align-center pa-4">
@@ -196,6 +211,8 @@ const photos = ref<string[]>([...props.modelValue]);
196
211
  const photosIds = ref<string[]>([...props.photoIds]);
197
212
  const photoOptionsDialog = ref(false);
198
213
  const showCameraDialog = ref(false);
214
+ const showPhotoPreview = ref(false);
215
+ const selectedPhoto = ref("");
199
216
  const videoRef = ref<HTMLVideoElement | null>(null);
200
217
  const canvasRef = ref<HTMLCanvasElement | null>(null);
201
218
  const fileInputRef = ref<HTMLInputElement | null>(null);
@@ -205,10 +222,8 @@ const uploading = ref(false);
205
222
  const { addFile } = useFile();
206
223
 
207
224
  function viewPhoto(photoData: string) {
208
- const link = document.createElement("a");
209
- link.href = photoData;
210
- link.target = "_blank";
211
- link.click();
225
+ selectedPhoto.value = photoData;
226
+ showPhotoPreview.value = true;
212
227
  }
213
228
 
214
229
  watch(
@@ -66,7 +66,7 @@
66
66
  <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
67
67
  @click="selectNricSearchResult(item)" class="cursor-pointer">
68
68
  <v-list-item-title>{{ item.name || 'Unknown' }}</v-list-item-title>
69
- <v-list-item-subtitle>{{ item.contact }}</v-list-item-subtitle>
69
+ <v-list-item-subtitle>NRIC: {{ item.nric }} | Contact No: {{ item.contact }} </v-list-item-subtitle>
70
70
  </v-list-item>
71
71
  </v-list>
72
72
  </v-menu>
@@ -155,9 +155,13 @@
155
155
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
156
156
  <v-row>
157
157
  <v-col cols="12">
158
- <InputLabel class="text-capitalize" title="Vehicle Number" required />
158
+ <InputLabel class="text-capitalize" title="Vehicle Number" :required="isVehicleRequired" />
159
159
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
160
- <InputVehicleNumber v-model="visitor.plateNumber" density="comfortable" />
160
+ <InputVehicleNumber
161
+ v-model="visitor.plateNumber"
162
+ :rules="[isVehicleRequired ? requiredRule : () => true]"
163
+ density="comfortable"
164
+ />
161
165
  </v-col>
162
166
  </v-row>
163
167
  </v-col>
@@ -635,6 +639,10 @@ const requireNRIC = computed(() => {
635
639
  return prop.type !== "walk-in" && prop.type !== "guest";
636
640
  });
637
641
 
642
+ const isVehicleRequired = computed(() => {
643
+ return ["guest", "drop-off", "pick-up"].includes(prop.type);
644
+ });
645
+
638
646
  const hidQrCodePassConfig = computed(() => {
639
647
  return (siteData.value as any)?.metadata?.hidQrCodePass || {};
640
648
  });
@@ -315,7 +315,9 @@
315
315
  style="cursor: pointer"
316
316
  >
317
317
  <v-chip
318
- v-for="pass in item.visitorPass"
318
+ v-for="pass in item.visitorPass.filter(
319
+ (i) => i.status != 'Removed'
320
+ )"
319
321
  :key="(pass as any)._id ?? (pass as any).keyId"
320
322
  prepend-icon="mdi-card-bulleted-outline"
321
323
  size="x-small"
@@ -325,7 +327,9 @@
325
327
  {{ (pass as any)?.prefixAndName }}
326
328
  </v-chip>
327
329
  <v-chip
328
- v-for="key in item.passKeys"
330
+ v-for="key in item.passKeys.filter(
331
+ (i) => i.status != 'Removed'
332
+ )"
329
333
  :key="(key as any)._id ?? (key as any).keyId"
330
334
  prepend-icon="mdi-key"
331
335
  size="x-small"
@@ -404,9 +408,16 @@
404
408
  </v-menu>
405
409
 
406
410
  <!-- QRCODE identifier -->
407
- <div v-if="item.accessCards?.type === 'QRCODE' && item.accessCards?.cardNo" class="d-flex align-center ga-1 mt-1">
411
+ <div
412
+ v-if="
413
+ item.accessCards?.type === 'QRCODE' && item.accessCards?.cardNo
414
+ "
415
+ class="d-flex align-center ga-1 mt-1"
416
+ >
408
417
  <v-icon size="15" icon="mdi-qrcode" color="teal" />
409
- <v-chip size="x-small" variant="tonal" color="teal">QR · ({{ item.cards?.flat().length ?? 1 }})</v-chip>
418
+ <v-chip size="x-small" variant="tonal" color="teal"
419
+ >QR · ({{ item.cards?.flat().length ?? 1 }})</v-chip
420
+ >
410
421
  </div>
411
422
  </v-row>
412
423
  </template>
@@ -814,7 +825,9 @@
814
825
  item-title="label"
815
826
  item-value="value"
816
827
  v-model="pass.status"
817
- :disabled="selectedVisitorObject.checkOut"
828
+ :disabled="
829
+ selectedVisitorObject.checkOut || pass.status == 'Removed'
830
+ "
818
831
  ></v-select>
819
832
  <v-textarea
820
833
  v-if="pass.status === 'Lost' || pass.status === 'Damaged'"
@@ -851,7 +864,9 @@
851
864
  item-title="label"
852
865
  item-value="value"
853
866
  v-model="key.status"
854
- :disabled="selectedVisitorObject.checkOut"
867
+ :disabled="
868
+ selectedVisitorObject.checkOut || key.status == 'Removed'
869
+ "
855
870
  ></v-select>
856
871
  <v-textarea
857
872
  v-if="key.status === 'Lost' || key.status === 'Damaged'"
@@ -911,37 +926,87 @@
911
926
  <v-dialog v-model="dialog.returnNfcCard" max-width="450" persistent>
912
927
  <v-card>
913
928
  <v-toolbar density="compact" color="">
914
- <v-row no-gutters class="d-flex fill-height justify-space-between align-center px-4">
929
+ <v-row
930
+ no-gutters
931
+ class="d-flex fill-height justify-space-between align-center px-4"
932
+ >
915
933
  <span class="font-weight-bold">Return NFC Card</span>
916
- <v-btn icon="mdi-close" variant="text" @click="dialog.returnNfcCard = false" />
934
+ <v-btn
935
+ icon="mdi-close"
936
+ variant="text"
937
+ @click="dialog.returnNfcCard = false"
938
+ />
917
939
  </v-row>
918
940
  </v-toolbar>
919
941
  <v-card-text class="px-4 pb-2">
920
- <p class="text-body-2 mb-3">Please indicate the status of the NFC card before checking out.</p>
942
+ <p class="text-body-2 mb-3">
943
+ Please indicate the status of the NFC card before checking out.
944
+ </p>
921
945
  <div>
922
946
  <InputLabel class="text-capitalize" title="Full Name" />
923
- <v-text-field v-model="selectedVisitorObject.name" density="comfortable" readonly />
947
+ <v-text-field
948
+ v-model="selectedVisitorObject.name"
949
+ density="comfortable"
950
+ readonly
951
+ />
924
952
  </div>
925
953
  <div>
926
954
  <InputLabel class="text-capitalize" title="Location" />
927
- <v-text-field v-model="selectedVisitorObject.location" density="comfortable" readonly />
955
+ <v-text-field
956
+ v-model="selectedVisitorObject.location"
957
+ density="comfortable"
958
+ readonly
959
+ />
928
960
  </div>
929
- <div v-for="(card, idx) in nfcCardReturnStatuses" :key="card.cardId" class="mb-4">
961
+ <div
962
+ v-for="(card, idx) in nfcCardReturnStatuses"
963
+ :key="card.cardId"
964
+ class="mb-4"
965
+ >
930
966
  <div class="d-flex align-center ga-2 mb-2">
931
967
  <v-icon size="18" icon="mdi-credit-card-outline" color="purple" />
932
- <v-chip size="small" variant="tonal" color="purple">{{ card.cardNo }}</v-chip>
968
+ <v-chip size="small" variant="tonal" color="purple">{{
969
+ card.cardNo
970
+ }}</v-chip>
933
971
  </div>
934
- <v-select v-model="nfcCardReturnStatuses[idx].status" :items="nfcPassStatusOptions" item-title="label" item-value="value" density="comfortable" hide-details />
935
- <v-textarea v-if="card.status === 'Lost' || card.status === 'Damage'" v-model="nfcCardReturnStatuses[idx].remarks" label="Remarks (required)" no-resize rows="3" class="mt-2" density="compact" />
972
+ <v-select
973
+ v-model="nfcCardReturnStatuses[idx].status"
974
+ :items="nfcPassStatusOptions"
975
+ item-title="label"
976
+ item-value="value"
977
+ density="comfortable"
978
+ hide-details
979
+ />
980
+ <v-textarea
981
+ v-if="card.status === 'Lost' || card.status === 'Damage'"
982
+ v-model="nfcCardReturnStatuses[idx].remarks"
983
+ label="Remarks (required)"
984
+ no-resize
985
+ rows="3"
986
+ class="mt-2"
987
+ density="compact"
988
+ />
936
989
  </div>
937
990
  </v-card-text>
938
991
  <v-toolbar class="pa-0" density="compact">
939
992
  <v-row no-gutters>
940
993
  <v-col cols="6">
941
- <v-btn variant="text" block @click="dialog.returnNfcCard = false">Close</v-btn>
994
+ <v-btn variant="text" block @click="dialog.returnNfcCard = false"
995
+ >Close</v-btn
996
+ >
942
997
  </v-col>
943
998
  <v-col cols="6">
944
- <v-btn color="red" variant="flat" height="48" rounded="0" block :loading="loading.checkingOut" :disabled="!canConfirmNfcCheckout" @click="handleNfcCheckout">Confirm Checkout</v-btn>
999
+ <v-btn
1000
+ color="red"
1001
+ variant="flat"
1002
+ height="48"
1003
+ rounded="0"
1004
+ block
1005
+ :loading="loading.checkingOut"
1006
+ :disabled="!canConfirmNfcCheckout"
1007
+ @click="handleNfcCheckout"
1008
+ >Confirm Checkout</v-btn
1009
+ >
945
1010
  </v-col>
946
1011
  </v-row>
947
1012
  </v-toolbar>
@@ -971,7 +1036,7 @@
971
1036
  @close="dialog.editPassKey = false"
972
1037
  @done="
973
1038
  () => {
974
- dialog.editPassKey = false;
1039
+ // dialog.editPassKey = false;
975
1040
  getVisitorRefresh();
976
1041
  }
977
1042
  "
@@ -1826,7 +1891,8 @@ const nfcCardReturnStatuses = ref<
1826
1891
  const canConfirmCheckout = computed(() => {
1827
1892
  const allEntries = [...passReturnStatuses.value, ...keyReturnStatuses.value];
1828
1893
  return allEntries.every((entry) => {
1829
- if (!entry.status || ["In Use","Not Returned"].includes(entry.status)) return false;
1894
+ if (!entry.status || ["In Use", "Not Returned"].includes(entry.status))
1895
+ return false;
1830
1896
  if (
1831
1897
  (entry.status === "Lost" || entry.status === "Damaged") &&
1832
1898
  !entry.remarks.trim()
@@ -1840,7 +1906,8 @@ const canConfirmNfcCheckout = computed(() => {
1840
1906
  if (nfcCardReturnStatuses.value.length === 0) return false;
1841
1907
  return nfcCardReturnStatuses.value.every(({ status, remarks }) => {
1842
1908
  if (!status || status === "In Use") return false;
1843
- if ((status === "Lost" || status === "Damage") && !remarks.trim()) return false;
1909
+ if ((status === "Lost" || status === "Damage") && !remarks.trim())
1910
+ return false;
1844
1911
  return true;
1845
1912
  });
1846
1913
  });
@@ -1883,17 +1950,28 @@ function handleCheckout(userId: string) {
1883
1950
  return;
1884
1951
  }
1885
1952
 
1886
- const hasNfc = visitor?.accessCards?.type === "NFC" && !!visitor?.accessCards?.cardNo;
1953
+ const hasNfc =
1954
+ visitor?.accessCards?.type === "NFC" && !!visitor?.accessCards?.cardNo;
1887
1955
  if (hasNfc) {
1888
1956
  const allCards: any[] = visitor.cards?.flat() ?? [];
1889
1957
  nfcCardReturnStatuses.value = allCards.map((card: any) => ({
1890
1958
  cardId: card._id,
1891
- cardNo: card._id === visitor.accessCards._id ? visitor.accessCards.cardNo : card._id,
1959
+ cardNo:
1960
+ card._id === visitor.accessCards._id
1961
+ ? visitor.accessCards.cardNo
1962
+ : card._id,
1892
1963
  status: "In Use",
1893
1964
  remarks: "",
1894
1965
  }));
1895
1966
  if (nfcCardReturnStatuses.value.length === 0) {
1896
- nfcCardReturnStatuses.value = [{ cardId: visitor.accessCards._id, cardNo: visitor.accessCards.cardNo, status: "In Use", remarks: "" }];
1967
+ nfcCardReturnStatuses.value = [
1968
+ {
1969
+ cardId: visitor.accessCards._id,
1970
+ cardNo: visitor.accessCards.cardNo,
1971
+ status: "In Use",
1972
+ remarks: "",
1973
+ },
1974
+ ];
1897
1975
  }
1898
1976
  dialog.returnNfcCard = true;
1899
1977
  return;
@@ -1961,7 +2039,9 @@ async function handleNfcCheckout() {
1961
2039
  })),
1962
2040
  });
1963
2041
  if (res) {
1964
- await updateVisitor(userId as string, { checkOut: new Date().toISOString() });
2042
+ await updateVisitor(userId as string, {
2043
+ checkOut: new Date().toISOString(),
2044
+ });
1965
2045
  showMessage("Visitor successfully checked-out!", "info");
1966
2046
  await getVisitorRefresh();
1967
2047
  dialog.returnNfcCard = false;
@@ -1969,7 +2049,10 @@ async function handleNfcCheckout() {
1969
2049
  }
1970
2050
  } catch (error: any) {
1971
2051
  const errorMessage = error?.response?._data?.message;
1972
- showMessage(errorMessage || "Something went wrong. Please try again later.", "error");
2052
+ showMessage(
2053
+ errorMessage || "Something went wrong. Please try again later.",
2054
+ "error"
2055
+ );
1973
2056
  } finally {
1974
2057
  loading.checkingOut = false;
1975
2058
  }
@@ -2241,11 +2324,7 @@ const syncVisitorFiltersFromRoute = () => {
2241
2324
 
2242
2325
  onMounted(syncVisitorFiltersFromRoute);
2243
2326
 
2244
- watch(
2245
- () => route.query,
2246
- syncVisitorFiltersFromRoute,
2247
- { deep: true }
2248
- );
2327
+ watch(() => route.query, syncVisitorFiltersFromRoute, { deep: true });
2249
2328
 
2250
2329
  const { socketUnregisteredVisitorTrigger, visitorSocketData } =
2251
2330
  useVisitorSocket();
@@ -1,58 +1,23 @@
1
1
  export function useCleaningSchedulePermission() {
2
- const { hasPermission } = usePermission();
3
- const { permissions } = useCleaningPermission();
4
-
5
2
  const { userAppRole } = useLocalSetup();
6
3
 
7
- const canViewSchedules = computed(() => {
8
- if (!userAppRole.value) return false;
9
- if (userAppRole.value.permissions.includes("*")) return true;
10
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "see-all-schedules");
11
- });
12
-
13
- const canViewScheduleDetails = computed(() => {
14
- if (!userAppRole.value) return false;
15
- if (userAppRole.value.permissions.includes("*")) return true;
16
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "see-schedule-details");
17
- });
18
-
19
- const canDownloadSchedule = computed(() => {
20
- if (!userAppRole.value) return false;
21
- if (userAppRole.value.permissions.includes("*")) return true;
22
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "download-schedule");
23
- });
24
-
25
- const canManageScheduleTasks = computed(() => {
26
- if (!userAppRole.value) return false;
27
- if (userAppRole.value.permissions.includes("*")) return true;
28
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "manage-schedule-tasks");
29
- });
30
-
31
- const canGenerateChecklist = computed(() => {
32
- if (!userAppRole.value) return false;
33
- if (userAppRole.value.permissions.includes("*")) return true;
34
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "generate-checklist");
35
- });
36
-
37
- const canViewHistory = computed(() => {
38
- if (!userAppRole.value) return false;
39
- if (userAppRole.value.permissions.includes("*")) return true;
40
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "view-history");
41
- });
42
-
43
- const canAddRemarks = computed(() => {
44
- if (!userAppRole.value) return false;
45
- if (userAppRole.value.permissions.includes("*")) return true;
46
- return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "add-remarks");
47
- });
4
+ const can = (action: string) =>
5
+ computed(() => {
6
+ const permissions = userAppRole.value?.permissions;
7
+ if (!permissions) return false;
8
+ if (permissions.includes("*")) return true;
9
+ return permissions.some((permission) =>
10
+ permission.endsWith(`-schedule-mgmt:${action}`),
11
+ );
12
+ });
48
13
 
49
14
  return {
50
- canViewSchedules,
51
- canViewScheduleDetails,
52
- canDownloadSchedule,
53
- canManageScheduleTasks,
54
- canGenerateChecklist,
55
- canViewHistory,
56
- canAddRemarks,
15
+ canViewSchedules: can("see-all-schedules"),
16
+ canViewScheduleDetails: can("see-schedule-details"),
17
+ canDownloadSchedule: can("download-schedule"),
18
+ canManageScheduleTasks: can("manage-schedule-tasks"),
19
+ canGenerateChecklist: can("generate-checklist"),
20
+ canViewHistory: can("view-history"),
21
+ canAddRemarks: can("add-remarks"),
57
22
  };
58
23
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.30",
5
+ "version": "3.0.31-staging.10",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -41,4 +41,5 @@ type TSiteCategory =
41
41
  | "shopping_mall"
42
42
  | "mix_development"
43
43
  | "industrial"
44
- | "co_working_co_living";
44
+ | "co_working"
45
+ | "co_living";
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes