@7365admin1/layer-common 3.1.3 → 3.1.4-staging.47

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.
@@ -4,6 +4,7 @@
4
4
  <v-row no-gutters class="mb-4" align="center">
5
5
  <v-col cols="12" md="6">
6
6
  <v-btn
7
+ v-if="props.canCreateNfcPatrol"
7
8
  rounded="pill"
8
9
  variant="outlined"
9
10
 
@@ -82,7 +83,12 @@
82
83
  </v-col>
83
84
 
84
85
  <!-- Add Dialog -->
85
- <v-dialog v-model="dialogAdd" persistent max-width="900">
86
+ <v-dialog
87
+ v-if="props.canCreateNfcPatrol"
88
+ v-model="dialogAdd"
89
+ persistent
90
+ max-width="900"
91
+ >
86
92
  <NFCPatrolRouteForm
87
93
  title="Add Route"
88
94
  :site="props.site"
@@ -94,7 +100,12 @@
94
100
  </v-dialog>
95
101
 
96
102
  <!-- Edit Dialog -->
97
- <v-dialog v-model="dialogEdit" persistent max-width="900">
103
+ <v-dialog
104
+ v-if="props.canUpdateNfcPatrol"
105
+ v-model="dialogEdit"
106
+ persistent
107
+ max-width="900"
108
+ >
98
109
  <NFCPatrolRouteForm
99
110
  title="Edit Route"
100
111
  :site="props.site"
@@ -190,7 +201,10 @@
190
201
 
191
202
  <v-toolbar class="pa-0" density="compact">
192
203
  <v-row no-gutters>
193
- <v-col cols="6" class="pa-0">
204
+ <v-col
205
+ :cols="props.canUpdateNfcPatrol || props.canDeleteNfcPatrol ? 6 : 12"
206
+ class="pa-0"
207
+ >
194
208
  <v-btn
195
209
  block
196
210
  variant="text"
@@ -203,7 +217,11 @@
203
217
  </v-btn>
204
218
  </v-col>
205
219
 
206
- <v-col cols="6" class="pa-0">
220
+ <v-col
221
+ v-if="props.canUpdateNfcPatrol || props.canDeleteNfcPatrol"
222
+ cols="6"
223
+ class="pa-0"
224
+ >
207
225
  <v-menu>
208
226
  <template #activator="{ props }">
209
227
  <v-btn
@@ -220,13 +238,17 @@
220
238
  </template>
221
239
 
222
240
  <v-list class="pa-0">
223
- <v-list-item @click="openDialogEdit()">
241
+ <v-list-item v-if="props.canUpdateNfcPatrol" @click="openDialogEdit()">
224
242
  <v-list-item-title class="text-subtitle-2">
225
243
  Edit Route
226
244
  </v-list-item-title>
227
245
  </v-list-item>
228
246
 
229
- <v-list-item @click="openDialogDelete()" class="text-red">
247
+ <v-list-item
248
+ v-if="props.canDeleteNfcPatrol"
249
+ @click="openDialogDelete()"
250
+ class="text-red"
251
+ >
230
252
  <v-list-item-title class="text-subtitle-2">
231
253
  Delete Route
232
254
  </v-list-item-title>
@@ -240,7 +262,12 @@
240
262
  </v-dialog>
241
263
 
242
264
  <!-- Delete Dialog -->
243
- <v-dialog v-model="dialogDelete" persistent width="540">
265
+ <v-dialog
266
+ v-if="props.canDeleteNfcPatrol"
267
+ v-model="dialogDelete"
268
+ persistent
269
+ width="540"
270
+ >
244
271
  <v-card width="100%">
245
272
  <v-card-text
246
273
  style="max-height: 100vh; overflow-y: auto"
@@ -304,6 +331,18 @@ const props = defineProps({
304
331
  type: String,
305
332
  required: true,
306
333
  },
334
+ canCreateNfcPatrol: {
335
+ type: Boolean,
336
+ default: false,
337
+ },
338
+ canUpdateNfcPatrol: {
339
+ type: Boolean,
340
+ default: false,
341
+ },
342
+ canDeleteNfcPatrol: {
343
+ type: Boolean,
344
+ default: false,
345
+ },
307
346
  headers: {
308
347
  type: Array as PropType<Array<any>>,
309
348
  default: () => [
@@ -406,6 +445,8 @@ const messageSnackbar = ref(false);
406
445
  const messageColor = ref("");
407
446
 
408
447
  async function openDialogEdit() {
448
+ if (!props.canUpdateNfcPatrol) return;
449
+
409
450
  try {
410
451
  // Fetch the full route details from the API
411
452
  const response = await getRouteById(selectedRoute.value._id);
@@ -431,6 +472,8 @@ async function openDialogEdit() {
431
472
  }
432
473
 
433
474
  function openDialogDelete() {
475
+ if (!props.canDeleteNfcPatrol) return;
476
+
434
477
  dialogDelete.value = true;
435
478
 
436
479
  if (dialogPreview.value) dialogPreview.value = false;
@@ -502,6 +545,8 @@ function handleRowClick(_: any, data: any) {
502
545
  }
503
546
 
504
547
  async function submitDelete() {
548
+ if (!props.canDeleteNfcPatrol) return;
549
+
505
550
  try {
506
551
  await deleteRouteById(selectedRoute.value._id ?? "");
507
552
  await getRoutesRefresh();
@@ -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(
@@ -19,7 +19,7 @@
19
19
  :error-messages="errorMessages"
20
20
  :readonly="!edit"
21
21
  :hint="
22
- edit ? 'Select permissions' : 'Permissions are not editable'
22
+ edit && 'Select permissions'
23
23
  "
24
24
  persistent-hint
25
25
  />
@@ -734,7 +734,7 @@ const {
734
734
  updateStatus: updateServiceProviderStatus,
735
735
  invite,
736
736
  } = useServiceProvider();
737
- const { getAllByUserId } = useMember();
737
+ const { getAll: getAllMembers, getAllByUserId } = useMember();
738
738
 
739
739
  const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
740
740
  useVerification();
@@ -757,6 +757,9 @@ const createInviteDialog = ref(false);
757
757
  // const loadingInviteRoles = ref(false);
758
758
  // const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
759
759
 
760
+ const { getByUserType } = useMember();
761
+ const { getOrgs } = useOrg();
762
+ const { getByEmail } = useServiceProvider();
760
763
 
761
764
  async function checkExistingAccount(email: string) {
762
765
  existingAccount.value = null;
@@ -767,40 +770,54 @@ async function checkExistingAccount(email: string) {
767
770
  checkingExistingAccount.value = true;
768
771
 
769
772
  try {
773
+ // Check if user exists
770
774
  const user = await getUserByEmail(email);
771
775
 
772
776
  if (!user?._id) return;
773
777
 
774
- const members = await getAllByUserId(user._id);
778
+ // Check if this service provider already exists in current org/site
779
+ const serviceProviders = await getByEmail({
780
+ email,
781
+ orgId: props.orgId,
782
+ siteId: props.siteId,
783
+ });
775
784
 
776
- const appMember = (members.items ?? []).find(
777
- (member: any) => member.type === serviceProviderInvite.value.app
778
- );
785
+ console.log("serviceProviders", serviceProviders);
779
786
 
780
- if (!appMember) {
781
- existingAccount.value = null;
782
- return;
783
- }
787
+ if (serviceProviders?.length) {
788
+ messageServiceProvider.value =
789
+ "This service provider already exists.";
790
+ return;
791
+ }
784
792
 
785
- if (
786
- appMember.org === props.orgId &&
787
- (appMember.siteId || "") === (props.siteId || "")
788
- ) {
789
- existingAccount.value = null;
790
- messageServiceProvider.value =
791
- "This user is already a member of this service provider.";
792
- return;
793
- }
793
+ // Check existing memberships
794
+ const members = await getAllByUserId(user._id);
794
795
 
795
- existingAccount.value = {
796
- _id: appMember.org,
797
- text: appMember.orgName,
798
- org: appMember.org,
799
- orgName: appMember.orgName,
800
- siteId: appMember.siteId,
801
- siteName: appMember.siteName,
802
- };
796
+ const appMember = (members.items ?? []).find(
797
+ (member: any) => member.type === serviceProviderInvite.value.app
798
+ );
799
+
800
+ if (!appMember) return;
803
801
 
802
+ // Already belongs to the same org/site
803
+ if (
804
+ appMember.org === props.orgId &&
805
+ (appMember.siteId || "") === (props.siteId || "")
806
+ ) {
807
+ messageServiceProvider.value =
808
+ "This user is already a member of this service provider.";
809
+ return;
810
+ }
811
+
812
+ // Existing account in another org/site
813
+ existingAccount.value = {
814
+ _id: appMember.org,
815
+ text: appMember.orgName,
816
+ org: appMember.org,
817
+ orgName: appMember.orgName,
818
+ siteId: appMember.siteId,
819
+ siteName: appMember.siteName,
820
+ };
804
821
  } catch (error) {
805
822
  existingAccount.value = null;
806
823
  } finally {
@@ -36,10 +36,17 @@
36
36
  </template>
37
37
 
38
38
  <v-list v-if="nricSearchResults.length" class="pa-1 phone-search-results">
39
- <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
40
- @click="selectNricSearchResult(item)" class="cursor-pointer nric-search-item">
41
- <VisitorSearchResultSummary :item="item" />
42
- </v-list-item>
39
+ <div class="search-results-scroll">
40
+ <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
41
+ @click="selectNricSearchResult(item)" class="cursor-pointer nric-search-item">
42
+ <VisitorSearchResultSummary :item="item" />
43
+ </v-list-item>
44
+ </div>
45
+ <div class="search-results-close pa-1">
46
+ <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('nric')">
47
+ Close
48
+ </v-btn>
49
+ </div>
43
50
  </v-list>
44
51
  </v-menu>
45
52
  </v-col>
@@ -58,10 +65,17 @@
58
65
  </template>
59
66
 
60
67
  <v-list v-if="phoneSearchResults.length" class="pa-1 phone-search-results">
61
- <v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
62
- @click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
63
- <VisitorSearchResultSummary :item="item" />
64
- </v-list-item>
68
+ <div class="search-results-scroll">
69
+ <v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
70
+ @click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
71
+ <VisitorSearchResultSummary :item="item" />
72
+ </v-list-item>
73
+ </div>
74
+ <div class="search-results-close pa-1">
75
+ <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('contact')">
76
+ Close
77
+ </v-btn>
78
+ </div>
65
79
  </v-list>
66
80
  </v-menu>
67
81
  </v-col>
@@ -77,15 +91,23 @@
77
91
  <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
78
92
  <template #activator="{ props }">
79
93
  <InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
80
- @update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true"
94
+ @update:model-value="handleUpdateVehicleNumber" @focus="handleFocusPlateNumber"
81
95
  @blur="isPlateNumberFieldFocused = false" />
82
96
  </template>
83
97
 
84
98
  <v-list v-if="vehicleSearchResults.length" class="pa-1 phone-search-results">
85
- <v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
86
- @click="selectPlateNumberSearchResult(item)" class="cursor-pointer nric-search-item">
87
- <VisitorSearchResultSummary :item="item" />
88
- </v-list-item>
99
+ <div class="search-results-scroll">
100
+ <v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
101
+ @click="selectPlateNumberSearchResult(item)" class="cursor-pointer nric-search-item">
102
+ <VisitorSearchResultSummary :item="item" />
103
+ </v-list-item>
104
+ </div>
105
+ <div class="search-results-close pa-1">
106
+ <v-btn block variant="flat" color="red"
107
+ @click.stop="closeAutofillSearchResults('vehicleNumber')">
108
+ Close
109
+ </v-btn>
110
+ </div>
89
111
  </v-list>
90
112
  </v-menu>
91
113
  </v-col>
@@ -308,10 +330,10 @@
308
330
 
309
331
  <div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
310
332
  <span>{{ selectedNricSearchResult.name || "Unknown" }}</span>
311
- <span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
333
+ <span v-if="!visitor.nric || currentAutofillSource === 'nric'" class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
312
334
  </div>
313
335
 
314
- <template v-if="!visitor.contact && selectedNricContactOptions.length >= 1">
336
+ <template v-if="(!visitor.contact || currentAutofillSource === 'contact') && selectedNricContactOptions.length >= 1">
315
337
  <h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
316
338
  <v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
317
339
  <v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
@@ -319,7 +341,7 @@
319
341
  </v-radio-group>
320
342
  </template>
321
343
 
322
- <template v-if="!visitor.plateNumber && selectedNricPlateOptions.length >= 1">
344
+ <template v-if="(!visitor.plateNumber || currentAutofillSource === 'vehicleNumber') && selectedNricPlateOptions.length >= 1">
323
345
  <h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
324
346
  <v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
325
347
  <v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
@@ -1044,8 +1066,8 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
1044
1066
  const selectedPlate =
1045
1067
  findMatchingPlateOption(plateOptions, visitor.plateNumber) || plateOptions[0] || "";
1046
1068
 
1047
- const needsContactChoice = !visitor.contact?.trim() && contactOptions.length > 1;
1048
- const needsPlateChoice = !visitor.plateNumber?.trim() && plateOptions.length > 1;
1069
+ const needsContactChoice = (!visitor.contact?.trim() || source === 'contact') && contactOptions.length > 1;
1070
+ const needsPlateChoice = (!visitor.plateNumber?.trim() || source === 'vehicleNumber') && plateOptions.length > 1;
1049
1071
 
1050
1072
  if (needsContactChoice || needsPlateChoice) {
1051
1073
  nricSearchMenu.value = false;
@@ -1066,6 +1088,23 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
1066
1088
  });
1067
1089
  }
1068
1090
 
1091
+ function closeAutofillSearchResults(source: Exclude<AutofillSource, null>) {
1092
+ if (source === "nric") {
1093
+ nricSearchMenu.value = false;
1094
+ nricSearchResults.value = [];
1095
+ }
1096
+
1097
+ if (source === "contact") {
1098
+ phoneSearchMenu.value = false;
1099
+ phoneSearchResults.value = [];
1100
+ }
1101
+
1102
+ if (source === "vehicleNumber") {
1103
+ vehicleSearchMenu.value = false;
1104
+ vehicleSearchResults.value = [];
1105
+ }
1106
+ }
1107
+
1069
1108
  function closeNricAutofillOptions() {
1070
1109
  dialog.nricAutofillOptions = false;
1071
1110
  selectedNricSearchResult.value = null;
@@ -1503,6 +1542,15 @@ function handleUpdateVehicleNumber() {
1503
1542
  debounceFetchPlateNumber();
1504
1543
  }
1505
1544
 
1545
+ const handleFocusPlateNumber = () => {
1546
+ isPlateNumberFieldFocused.value = true;
1547
+ if (prop.mode === 'register') {
1548
+
1549
+ handleUpdateVehicleNumber();
1550
+ }
1551
+ };
1552
+
1553
+
1506
1554
  function backToSelection() {
1507
1555
  emit("back");
1508
1556
  message.value = "";
@@ -2108,10 +2156,22 @@ onMounted(() => {
2108
2156
  }
2109
2157
 
2110
2158
  .phone-search-results {
2159
+ display: flex;
2160
+ flex-direction: column;
2111
2161
  max-height: 400px;
2162
+ overflow: hidden;
2163
+ }
2164
+
2165
+ .search-results-scroll {
2166
+ flex: 1 1 auto;
2167
+ min-height: 0;
2112
2168
  overflow-y: auto;
2113
2169
  }
2114
2170
 
2171
+ .search-results-close {
2172
+ flex: 0 0 auto;
2173
+ }
2174
+
2115
2175
  .nric-options-card {
2116
2176
  border-radius: 16px !important;
2117
2177
  }
@@ -675,6 +675,24 @@
675
675
  />
676
676
  </div>
677
677
  </span>
678
+ <span
679
+ v-else-if="
680
+ key === 'snapshotEntryImage' || key === 'snapshotExitImage'
681
+ "
682
+ class="d-flex flex-column"
683
+ >
684
+ <strong class="w-full">{{ label }}:</strong>
685
+ <div class="d-flex flex-wrap ga-2">
686
+ <AttachmentsViewer
687
+ :files="
688
+ mappedSnapshots([selectedVisitorObject.snapshotEntryImage])
689
+ "
690
+ anpr
691
+ :site="siteId"
692
+ title="Snapshot"
693
+ />
694
+ </div>
695
+ </span>
678
696
 
679
697
  <span
680
698
  v-else-if="selectedVisitorObject[key]"
@@ -825,10 +843,8 @@
825
843
  item-title="label"
826
844
  item-value="value"
827
845
  v-model="pass.status"
828
- :disabled="
829
- selectedVisitorObject.checkOut || pass.status == 'Removed'
830
- "
831
- ></v-select>
846
+ :disabled="pass.status == 'Removed'"
847
+ />
832
848
  <v-textarea
833
849
  v-if="pass.status === 'Lost' || pass.status === 'Damaged'"
834
850
  no-resize
@@ -836,8 +852,7 @@
836
852
  class="w-100"
837
853
  density="compact"
838
854
  v-model="pass.remarks"
839
- :disabled="selectedVisitorObject.checkOut"
840
- ></v-textarea>
855
+ />
841
856
  </v-row>
842
857
  </div>
843
858
  <div v-if="keyReturnStatuses.length > 0" class="mb-2">
@@ -864,10 +879,8 @@
864
879
  item-title="label"
865
880
  item-value="value"
866
881
  v-model="key.status"
867
- :disabled="
868
- selectedVisitorObject.checkOut || key.status == 'Removed'
869
- "
870
- ></v-select>
882
+ :disabled="key.status == 'Removed'"
883
+ />
871
884
  <v-textarea
872
885
  v-if="key.status === 'Lost' || key.status === 'Damaged'"
873
886
  no-resize
@@ -875,8 +888,7 @@
875
888
  class="w-100"
876
889
  density="compact"
877
890
  v-model="key.remarks"
878
- :disabled="selectedVisitorObject.checkOut"
879
- ></v-textarea>
891
+ />
880
892
  </v-row>
881
893
  </div>
882
894
 
@@ -886,11 +898,11 @@
886
898
  color="blue"
887
899
  density="comfortable"
888
900
  class="text-capitalize"
889
- :disabled="selectedVisitorObject.checkOut"
890
901
  :loading="loading.updatingPassKeys"
891
902
  @click.stop="handleUpdatePassKeys"
892
- >Update Pass/Keys</v-btn
893
903
  >
904
+ Update Pass/Keys
905
+ </v-btn>
894
906
  </v-row>
895
907
  </v-card-text>
896
908
  <v-toolbar class="pa-0" density="compact">
@@ -900,8 +912,9 @@
900
912
  variant="text"
901
913
  block
902
914
  @click="dialog.returnPassesKeys = false"
903
- >Close</v-btn
904
915
  >
916
+ Close
917
+ </v-btn>
905
918
  </v-col>
906
919
  <v-col cols="6">
907
920
  <v-btn
@@ -915,8 +928,9 @@
915
928
  !canConfirmCheckout || selectedVisitorObject.checkOut
916
929
  "
917
930
  @click="proceedCheckout"
918
- >Confirm Checkout</v-btn
919
931
  >
932
+ Confirm Checkout
933
+ </v-btn>
920
934
  </v-col>
921
935
  </v-row>
922
936
  </v-toolbar>
@@ -1442,6 +1456,15 @@ function mappedAttachments(attachments: string[]) {
1442
1456
  });
1443
1457
  }
1444
1458
 
1459
+ function mappedSnapshots(attachments: string[]) {
1460
+ return attachments.map((x, index) => {
1461
+ return {
1462
+ id: x,
1463
+ name: `Snapshot - ${x}`,
1464
+ };
1465
+ });
1466
+ }
1467
+
1445
1468
  function normalizeDateOnly(value: string) {
1446
1469
  if (!value) return "";
1447
1470
  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
@@ -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
  }