@7365admin1/layer-common 1.10.9 → 1.11.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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardAddForm.vue +1 -1
  3. package/components/AccessCardAssignToUnitForm.vue +10 -13
  4. package/components/AccessCardQrTagging.vue +2 -2
  5. package/components/BulletinBoardManagement.vue +18 -8
  6. package/components/Chat/SkeletonLoader.vue +71 -0
  7. package/components/DashboardMain.vue +176 -0
  8. package/components/DeliveryCompany.vue +240 -0
  9. package/components/EntryPassInformation.vue +38 -8
  10. package/components/FeedbackMain.vue +4 -19
  11. package/components/FileInputWithList.vue +304 -0
  12. package/components/IncidentReport/Authorities.vue +189 -151
  13. package/components/IncidentReport/IncidentInformation.vue +28 -12
  14. package/components/IncidentReport/IncidentInformationDownload.vue +225 -0
  15. package/components/IncidentReport/affectedEntities.vue +13 -57
  16. package/components/Signature.vue +133 -0
  17. package/components/SiteSettings.vue +285 -0
  18. package/components/SlideCardGroup.vue +194 -0
  19. package/components/Tooltip/Info.vue +33 -0
  20. package/components/VisitorForm.vue +65 -3
  21. package/components/VisitorManagement.vue +23 -6
  22. package/composables/useAccessManagement.ts +44 -6
  23. package/composables/useBulletin.ts +8 -3
  24. package/composables/useBulletinBoardPermission.ts +48 -0
  25. package/composables/useCleaningPermission.ts +2 -0
  26. package/composables/useComment.ts +147 -0
  27. package/composables/useCommonPermission.ts +29 -1
  28. package/composables/useFeedback.ts +79 -29
  29. package/composables/useFile.ts +6 -0
  30. package/composables/usePDFDownload.ts +1 -1
  31. package/composables/useSiteSettings.ts +1 -1
  32. package/composables/useVisitor.ts +6 -5
  33. package/composables/useWorkOrder.ts +61 -26
  34. package/constants/app.ts +12 -0
  35. package/nuxt.config.ts +2 -0
  36. package/package.json +3 -1
  37. package/plugins/vue-draggable-next.client.ts +5 -0
  38. package/public/default-image.svg +4 -0
  39. package/public/placeholder-image.svg +6 -0
  40. package/types/comment.d.ts +38 -0
  41. package/types/dashboard.d.ts +12 -0
  42. package/types/feedback.d.ts +56 -20
  43. package/types/site.d.ts +2 -1
  44. package/types/work-order.d.ts +54 -18
  45. package/utils/data.ts +31 -0
@@ -0,0 +1,194 @@
1
+ <template>
2
+ <v-sheet class="mx-auto">
3
+ <v-slide-group
4
+ class="pa-1"
5
+ show-arrows
6
+ :style="{ backgroundColor: '#FFFF' }"
7
+ >
8
+ <v-slide-group-item
9
+ v-for="(data, idx) in localDataFiles"
10
+ :key="idx"
11
+ v-slot="{ isSelected }"
12
+ >
13
+ <v-card
14
+ :class="['ma-1', isSelected && 'selected']"
15
+ width="150"
16
+ height="100"
17
+ @click="clickable && emit('onClickCarousel', data.path)"
18
+ class="rounded-lg border-thin"
19
+ border=" opacity-50 thin "
20
+ >
21
+ <v-btn
22
+ icon
23
+ variant="text"
24
+ size="x-small"
25
+ class="remove-btn pa-0 ma-0"
26
+ @click.stop="removeFile(data)"
27
+ >
28
+ <v-icon color="red" class="pa-0 ma-0" size="x-large"
29
+ >mdi-close-circle</v-icon
30
+ >
31
+ </v-btn>
32
+ <!-- File Icons -->
33
+
34
+ <v-img :src="data.url" cover height="200">
35
+ <template #placeholder>
36
+ <v-skeleton-loader height="100%" width="100%" />
37
+ </template>
38
+ </v-img>
39
+ <!-- <template v-if="url.type !== 'image'">
40
+ <div
41
+ class="d-flex fill-height align-center justify-center flex-column"
42
+ >
43
+ <v-icon
44
+ :icon="getFileIcon(url.type)"
45
+ size="64"
46
+ :color="getFileColor(url.type)"
47
+ ></v-icon>
48
+ </div>
49
+ </template>
50
+
51
+
52
+
53
+ <template v-else>
54
+ <v-img :src="url.path" cover height="200">
55
+ <template #placeholder>
56
+ <v-skeleton-loader height="100%" width="100%" />
57
+ </template>
58
+ </v-img>
59
+ </template> -->
60
+
61
+ <!-- Edit Button -->
62
+ <!-- <div v-if="imgEditable" class="edit-btn">
63
+ <v-btn
64
+ color="primary"
65
+ size="small"
66
+ @click.stop="onImageEdit(url.path, idx)"
67
+ >
68
+ <v-icon start>mdi-pencil</v-icon>
69
+ Edit
70
+ </v-btn>
71
+ </div> -->
72
+ </v-card>
73
+ </v-slide-group-item>
74
+ </v-slide-group>
75
+ </v-sheet>
76
+
77
+ <DrawImage
78
+ v-if="isShowImageEdit"
79
+ :is-show-dialog="isShowImageEdit"
80
+ :image-url="imageUrl"
81
+ :image-idx="imageIdx"
82
+ @on-submit="onImageSubmitEdit"
83
+ @on-close-dialog="isShowImageEdit = false"
84
+ />
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ const { getImage } = useUtils();
89
+ const { attachedFiles } = useUploadFiles();
90
+
91
+ const emit = defineEmits<{
92
+ (event: "onClickCarousel", url: string): void;
93
+ (event: "onImageEdit", url: string, idx: number): void;
94
+ (event: "onFileRemove"): void;
95
+ }>();
96
+
97
+ const props = defineProps<{
98
+ clickable?: boolean;
99
+ imgEditable?: boolean;
100
+ dataFiles?: File[];
101
+ }>();
102
+ const localDataFiles = ref([...props.dataFiles]);
103
+ const isShowImageEdit = ref(false);
104
+ const imageUrl = ref("");
105
+ const imageIdx = ref(0);
106
+
107
+ const getFileType = (url: string) => {
108
+ const extension = url.split(".").pop()?.toLowerCase() || "";
109
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) return "image";
110
+ if (["pdf"].includes(extension)) return "pdf";
111
+ if (["doc", "docx"].includes(extension)) return "word";
112
+ return "other";
113
+ };
114
+
115
+ const getFileIcon = (type: string) => {
116
+ switch (type) {
117
+ case "pdf":
118
+ return "mdi-file-pdf-box";
119
+ case "word":
120
+ return "mdi-file-word-box";
121
+ default:
122
+ return "mdi-file-outline";
123
+ }
124
+ };
125
+
126
+ const getFileColor = (type: string) => {
127
+ switch (type) {
128
+ case "pdf":
129
+ return "red";
130
+ case "word":
131
+ return "blue";
132
+ default:
133
+ return "grey";
134
+ }
135
+ };
136
+
137
+ const onImageEdit = (url: string, idx: number) => {
138
+ isShowImageEdit.value = true;
139
+ imageUrl.value = url;
140
+ imageIdx.value = idx;
141
+ };
142
+
143
+ const onImageSubmitEdit = async (url: string, idx: number) => {
144
+ const response = await getImage(url);
145
+ if (!response) return;
146
+ attachedFiles.value[idx].data = new File(
147
+ [response],
148
+ attachedFiles.value[idx].data?.name
149
+ );
150
+ attachedFiles.value[idx].url = url;
151
+ isShowImageEdit.value = false;
152
+ };
153
+
154
+ const isImageFile = (type: string) => {
155
+ return type?.startsWith("image/");
156
+ };
157
+
158
+ watch(
159
+ () => props.dataFiles,
160
+ (newDataFiles) => {
161
+ localDataFiles.value = [...newDataFiles];
162
+ }
163
+ );
164
+
165
+ const removeFile = (file) => {
166
+ const index = localDataFiles.value.findIndex(
167
+ (dataFile) => dataFile.id === file.id
168
+ );
169
+
170
+ if (index !== -1) {
171
+ localDataFiles.value.splice(index, 1);
172
+ emit("onFileRemove", file);
173
+ }
174
+ };
175
+ </script>
176
+
177
+ <style scoped>
178
+ .edit-btn {
179
+ position: absolute;
180
+ bottom: 8px;
181
+ right: 8px;
182
+ }
183
+
184
+ .selected {
185
+ border: 2px solid var(--v-theme-primary);
186
+ }
187
+
188
+ .remove-btn {
189
+ position: absolute;
190
+ top: 0px;
191
+ right: 0px;
192
+ z-index: 2;
193
+ }
194
+ </style>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <v-tooltip v-bind="$attrs" :text="text" @click.stop>
3
+ <template v-slot:activator="{ props: activatorProps }">
4
+ <v-btn icon="mdi-information-symbol" v-bind="activatorProps" class="p-0 m-0 d-flex align-center"
5
+ :density="density" :size="size" color="primary-button" />
6
+ </template>
7
+ </v-tooltip>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+
12
+ import type { VTooltip} from "vuetify/components"
13
+
14
+ type TTooltipDensity = VTooltip["$props"]["density"]
15
+
16
+ const props = defineProps({
17
+ text: {
18
+ type: String,
19
+ default: ""
20
+ },
21
+ size: {
22
+ type: String,
23
+ default: ""
24
+ },
25
+ density: {
26
+ type: String as PropType<TTooltipDensity>,
27
+ default: "default"
28
+ }
29
+ })
30
+
31
+ </script>
32
+
33
+ <style scoped></style>
@@ -110,6 +110,36 @@
110
110
  </v-col>
111
111
  </template>
112
112
 
113
+ <template v-if="shouldShowField('delivery-company')">
114
+ <v-col cols="12">
115
+ <InputLabel class="text-capitalize" title="Delivery Company" />
116
+ <v-combobox v-model="deliveryCompany" v-model:search="deliveryCompanyInput" ref="companyCombo"
117
+ autocomplete="off" :hide-no-data="false" :items="deliveryCompanyList" item-value="value"
118
+ :loading="siteDataPending" variant="outlined"
119
+ density="comfortable" persistent-hint small-chips>
120
+ <template v-slot:no-data>
121
+ <v-list-item>
122
+ <v-list-item-title v-if="fetchCompanyListPending">
123
+ <v-progress-circular indeterminate size="20" class="mr-3" />
124
+ Searching companies…
125
+ </v-list-item-title>
126
+ <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
127
+ class="d-flex align-center ga-1">
128
+ <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{ companyNameInput }}</strong>" as new
129
+ company.
130
+ </v-list-item-title>
131
+ <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
132
+ Start typing to search for companies.
133
+ </v-list-item-title>
134
+ <v-list-item-title v-else>
135
+ No companies available. Start typing to add a new one.
136
+ </v-list-item-title>
137
+ </v-list-item>
138
+ </template>
139
+ </v-combobox>
140
+ </v-col>
141
+ </template>
142
+
113
143
 
114
144
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
115
145
  <v-row>
@@ -292,6 +322,7 @@ const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
292
322
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
293
323
  const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
294
324
  const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
325
+ const { createVisitorPass } = useAccessManagement();
295
326
  const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
296
327
  const { getById: getUnitDataById } = useBuildingUnit()
297
328
 
@@ -324,7 +355,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
324
355
 
325
356
  const passType = ref("");
326
357
  const passQuantity = ref<number | null>(1);
327
- const passCards = ref<string[]>([]);
358
+ const passCards = ref<{ _id: string; cardNo: string }[]>([]);
328
359
 
329
360
  const registeredUnitCompanyName = ref('N/A')
330
361
 
@@ -356,13 +387,17 @@ const blocksArray = ref<TDefaultOptionObj[]>([]);
356
387
  const levelsArray = ref<TDefaultOptionObj[]>([]);
357
388
  const unitsArray = ref<TDefaultOptionObj[]>([]);
358
389
 
390
+ const deliveryCompany = ref("");
391
+ const deliveryCompanyInput = ref("");
392
+ const deliveryCompanyList = ref<string[]>([]);
393
+
359
394
  const matchingPlateNumberNonCheckedOutArr = ref<TVisitor[]>([])
360
395
 
361
396
 
362
397
  const vehicleNumberUserItems = ref<TPeople[]>([])
363
398
 
364
399
 
365
- const shouldShowField = (fieldKey: keyof TVisitorPayload) => {
400
+ const shouldShowField = (fieldKey: keyof TVisitorPayload | 'delivery-company') => {
366
401
  if (prop.type !== "contractor" || contractorStep.value === 1) {
367
402
  const visibleFields = typeFieldMap[prop.type];
368
403
  return visibleFields?.includes(fieldKey);
@@ -634,6 +669,7 @@ const {
634
669
  data: siteData,
635
670
  refresh: refreshSiteData,
636
671
  status: blockStatus,
672
+ pending: siteDataPending,
637
673
  } = useLazyAsyncData(`fetch-site-data-${prop.site}`, () =>
638
674
  getSiteById(prop.site)
639
675
  );
@@ -665,7 +701,7 @@ const {
665
701
  watch(
666
702
  siteData,
667
703
  (newVal) => {
668
- const siteDataValue = newVal as any;
704
+ const siteDataValue = newVal as TSite;
669
705
  if (siteDataValue) {
670
706
  const numberOfBlocks = siteDataValue.metadata?.block || 0;
671
707
  for (let i = 1; i <= numberOfBlocks; i++) {
@@ -674,6 +710,9 @@ watch(
674
710
  value: i,
675
711
  });
676
712
  }
713
+
714
+ deliveryCompanyList.value = Array.isArray(siteDataValue?.deliveryCompanyList) ? siteDataValue.deliveryCompanyList : [];
715
+
677
716
  } else {
678
717
  blocksArray.value = [];
679
718
  }
@@ -857,9 +896,32 @@ async function submit() {
857
896
  members: visitor.members,
858
897
  };
859
898
  }
899
+
900
+ if(prop.type === "delivery"){
901
+ payload = {
902
+ ...payload,
903
+ company: deliveryCompany.value
904
+ }
905
+ }
906
+
860
907
  try {
861
908
  const res = await createVisitor(payload);
862
909
  if (res) {
910
+ if (prop.type === "contractor" && passType.value) {
911
+ const visitorId = res?._id;
912
+ const acmUrl = entryPassSettings.value?.data?.settings?.acm_url;
913
+ if (visitorId && acmUrl) {
914
+ await createVisitorPass({
915
+ site: prop.site,
916
+ unitId: visitor.unit!,
917
+ quantity: passQuantity.value ?? 1,
918
+ type: passType.value === "QR" ? "QRCODE" : passType.value,
919
+ nfcCards: passCards.value,
920
+ acm_url: acmUrl,
921
+ visitorId,
922
+ });
923
+ }
924
+ }
863
925
  if (createMore.value) {
864
926
  emit("done:more");
865
927
  } else emit("done");
@@ -89,13 +89,19 @@
89
89
  <span class="text-capitalize">{{
90
90
  UTCToLocalTIme(item.checkIn) || "-"
91
91
  }}</span>
92
+ <span>
93
+ <v-icon v-if="item?.snapshotEntryImage" size="17" icon="mdi-image" @click.stop="handleViewImage(item.snapshotEntryImage)" />
94
+ </span>
92
95
  </span>
93
96
  <span class="d-flex align-center ga-2">
94
97
  <v-icon icon="mdi-clock-time-eight-outline" color="red" size="20" />
95
98
  <template v-if="item.checkOut">
96
99
  <span class="text-capitalize">{{
97
- UTCToLocalTIme(item.checkOut) || "_"
100
+ UTCToLocalTIme(item.checkOut) || "-"
98
101
  }}</span>
102
+ <span>
103
+ <v-icon v-if="item?.snapshotExitImage" size="17" icon="mdi-image" @click.stop="handleViewImage(item.snapshotExitImage)" />
104
+ </span>
99
105
  <span v-if="item?.manualCheckout">
100
106
  <TooltipInfo text="Manual Checkout" density="compact" size="x-small" />
101
107
  </span>
@@ -120,8 +126,9 @@
120
126
  </v-dialog>
121
127
 
122
128
  <v-dialog v-model="dialog.showForm" v-if="activeVisitorFormType" width="450" persistent>
123
- <VisitorForm :mode="mode" :org="orgId" :site="siteId" :visitor-data="selectedVisitorDataObject" :type="activeVisitorFormType" @back="handleClickBack"
124
- @done="handleVisitorFormDone" @done:more="handleVisitorFormCreateMore" @close:all="handleCloseAll" />
129
+ <VisitorForm :mode="mode" :org="orgId" :site="siteId" :visitor-data="selectedVisitorDataObject"
130
+ :type="activeVisitorFormType" @back="handleClickBack" @done="handleVisitorFormDone"
131
+ @done:more="handleVisitorFormCreateMore" @close:all="handleCloseAll" />
125
132
  </v-dialog>
126
133
 
127
134
  <v-dialog v-model="dialog.viewVisitor" width="450" persistent>
@@ -200,6 +207,7 @@ const {
200
207
  const { debounce, formatCamelCaseToWords, formatDate, UTCToLocalTIme } =
201
208
  useUtils();
202
209
  const { formatLocation } = useSecurityUtils();
210
+ const { getFileUrlAnpr } = useFile();
203
211
  // const { status: visitorStatus, search } = useRoute().query as { status: string, search: string};
204
212
 
205
213
  const route = useRoute()
@@ -276,8 +284,11 @@ const formattedFields = {
276
284
  level: "Level",
277
285
  unitName: "Unit",
278
286
  checkIn: "Check In",
287
+ snapshotEntryImage: "Entry Image",
279
288
  checkOut: "Check Out",
289
+ snapshotExitImage: "Exit Image",
280
290
  remarks: "Remarks",
291
+
281
292
  } as const;
282
293
 
283
294
  function filterTypeSelectionLabel() {
@@ -326,7 +337,7 @@ const {
326
337
  params.status = activeTab.value
327
338
  } else if (activeTab.value === "guests") {
328
339
  params.type = "guest"
329
- params.status = undefined
340
+ params.status = "pending"
330
341
  }
331
342
 
332
343
  return await getVisitors(params)
@@ -348,11 +359,11 @@ watch(getVisitorReq, (newData: any) => {
348
359
  });
349
360
 
350
361
  const selectedVisitorObject = computed(() => {
362
+
351
363
  const obj = items.value.find((x: any) => x?._id === selectedVisitorId.value);
352
364
  if (!obj) return {};
353
365
  const type = obj?.type as TVisitorType | undefined;
354
- if (!type) return {};
355
- let includedKeys: string[] = ["checkIn", "checkOut"];
366
+ let includedKeys: string[] = ["checkIn", "checkOut", "plateNumber", "snapshotEntryImage", "snapshotExitImage"];
356
367
  includedKeys.unshift(...(typeFieldMap[type] ?? []));
357
368
  return Object.fromEntries(
358
369
  Object.entries(obj).filter(([key]) => includedKeys.includes(key))
@@ -434,6 +445,12 @@ function handleRegistrationUnregisteredVisitor(item: Partial<TVisitor>) {
434
445
  mode.value = "register";
435
446
  }
436
447
 
448
+ function handleViewImage(imageId: string) {
449
+ const imageEndpoint = `${siteId}/${imageId}`;
450
+ const imageUrl = getFileUrlAnpr(imageEndpoint);
451
+ window.open(imageUrl, "_blank");
452
+ }
453
+
437
454
 
438
455
  async function handleProceedDeleteVisitor() {
439
456
  try {
@@ -111,8 +111,8 @@ export default function useAccessManagement() {
111
111
  site: string;
112
112
  userType: string;
113
113
  type: string;
114
- accessLevel: string;
115
- liftAccessLevel: string;
114
+ accessLevel: string | null;
115
+ liftAccessLevel: string | null;
116
116
  }) {
117
117
  return useNuxtApp().$api<Record<string, any>>(
118
118
  `/api/access-management/access-and-lift-cards`,
@@ -122,8 +122,8 @@ export default function useAccessManagement() {
122
122
  site: params.site,
123
123
  userType: params.userType,
124
124
  type: params.type,
125
- accessLevel: params.accessLevel,
126
- liftAccessLevel: params.liftAccessLevel,
125
+ ...(params.accessLevel != null ? { accessLevel: params.accessLevel } : {}),
126
+ ...(params.liftAccessLevel != null ? { liftAccessLevel: params.liftAccessLevel } : {}),
127
127
  },
128
128
  }
129
129
  );
@@ -135,8 +135,8 @@ export default function useAccessManagement() {
135
135
  type: string;
136
136
  site: string;
137
137
  userType: string;
138
- accessLevel: string;
139
- liftAccessLevel: string;
138
+ accessLevel: string | null;
139
+ liftAccessLevel: string | null;
140
140
  }) {
141
141
  return useNuxtApp().$api<Record<string, any>>(
142
142
  `/api/access-management/assign-access-card`,
@@ -243,6 +243,42 @@ export default function useAccessManagement() {
243
243
  );
244
244
  }
245
245
 
246
+ function generateQrVms(params: {
247
+ site: string;
248
+ unitId: string;
249
+ quantity: number;
250
+ }) {
251
+ return useNuxtApp().$api<Record<string, any>>(
252
+ `/api/access-management/generate-qr-vms`,
253
+ {
254
+ method: "POST",
255
+ body: {
256
+ site: params.site,
257
+ unitId: params.unitId,
258
+ quantity: params.quantity,
259
+ },
260
+ }
261
+ );
262
+ }
263
+
264
+ function createVisitorPass(payload: {
265
+ site: string;
266
+ unitId: string;
267
+ quantity: number;
268
+ type: string;
269
+ nfcCards: { _id: string; cardNo: string }[];
270
+ acm_url: string;
271
+ visitorId: string;
272
+ }) {
273
+ return useNuxtApp().$api<Record<string, any>>(
274
+ `/api/access-management/visitor`,
275
+ {
276
+ method: "POST",
277
+ body: payload,
278
+ }
279
+ );
280
+ }
281
+
246
282
  return {
247
283
  getDoorAccessLevels,
248
284
  getLiftAccessLevels,
@@ -261,5 +297,7 @@ export default function useAccessManagement() {
261
297
  saveVisitorAccessCardQrTag,
262
298
  getAllVisitorAccessCardsQrTags,
263
299
  getAvailableContractorCards,
300
+ generateQrVms,
301
+ createVisitorPass,
264
302
  };
265
303
  }
@@ -1,12 +1,17 @@
1
+ import type { APP_CONSTANTS } from "../constants/app";
2
+
1
3
  export default function(){
2
4
 
3
- const recipientList: { title: string, value: TAnnouncementRecipients }[] = [
4
- // resident | security_agency | cleaning_services | mechanical_electrical | property_management_agency
5
+ const recipientList: { title: string, value: typeof APP_CONSTANTS[keyof typeof APP_CONSTANTS] }[] = [
5
6
  { title: "Security Agency", value: "security_agency" },
6
7
  { title: "Cleaning Services", value: "cleaning_services" },
7
8
  { title: "Mechanical & Electrical", value: "mechanical_electrical" },
8
9
  { title: "Property Management Agency", value: "property_management_agency" },
9
- { title: "Resident", value: "resident" }
10
+ { title: "Resident", value: "resident" },
11
+ { title: "Pest Control", value: "pest_control_services" },
12
+ { title: "Landscaping", value: "landscaping_services" },
13
+ { title: "Pool Maintenance", value: "pool_maintenance_services" },
14
+
10
15
  ]
11
16
 
12
17
  async function add(payload: Partial<TCreateAnnouncementPayload>) {
@@ -0,0 +1,48 @@
1
+ import { useCommonPermissions } from "./useCommonPermission";
2
+
3
+ export function useBulletinBoardPermission() {
4
+ const { hasPermission } = usePermission();
5
+ const { bulletinBoardPermissions: permissions } = useCommonPermissions();
6
+
7
+ const { userAppRole } = useLocalSetup();
8
+
9
+ const canViewBulletinBoard = computed(() => {
10
+ if (!userAppRole.value) return true;
11
+ if (userAppRole.value.permissions.includes("*")) return true;
12
+ return hasPermission(userAppRole.value, permissions, "bulletin-board", "see-all-bulletin-boards");
13
+ });
14
+
15
+ const canCreateBulletinBoard = computed(() => {
16
+ if (!userAppRole.value) return true;
17
+ if (userAppRole.value.permissions.includes("*")) return true;
18
+ return hasPermission(userAppRole.value, permissions, "bulletin-board", "add-bulletin-board");
19
+ });
20
+
21
+ const canUpdateBulletinBoard = computed(() => {
22
+ if (!userAppRole.value) return true;
23
+ if (userAppRole.value.permissions.includes("*")) return true;
24
+ return hasPermission(userAppRole.value, permissions, "bulletin-board", "update-bulletin-board");
25
+ });
26
+
27
+ const canViewBulletinBoardDetails = computed(() => {
28
+ if (!userAppRole.value) return true;
29
+ if (userAppRole.value.permissions.includes("*")) return true;
30
+ return hasPermission(userAppRole.value, permissions, "bulletin-board", "see-bulletin-board-details");
31
+ });
32
+
33
+
34
+
35
+ const canDeleteBulletinBoard = computed(() => {
36
+ if (!userAppRole.value) return true;
37
+ if (userAppRole.value.permissions.includes("*")) return true;
38
+ return hasPermission(userAppRole.value, permissions, "bulletin-board", "delete-bulletin-board");
39
+ });
40
+
41
+ return {
42
+ canViewBulletinBoard,
43
+ canCreateBulletinBoard,
44
+ canUpdateBulletinBoard,
45
+ canViewBulletinBoardDetails,
46
+ canDeleteBulletinBoard,
47
+ };
48
+ }
@@ -5,6 +5,7 @@ export default function useCleaningPermission() {
5
5
  feedbackPermissions,
6
6
  workOrderPermissions,
7
7
  invitationPermissions,
8
+ bulletinBoardPermissions,
8
9
  } = useCommonPermissions();
9
10
  const permissions: TPermissions = {
10
11
  members: memberPermissions,
@@ -12,6 +13,7 @@ export default function useCleaningPermission() {
12
13
  work_order: workOrderPermissions,
13
14
  roles: rolePermissions,
14
15
  invitations: invitationPermissions,
16
+ "bulletin-board": bulletinBoardPermissions,
15
17
  inventory: {
16
18
  "view-inventory": {
17
19
  check: true,