@7365admin1/layer-common 1.10.7 → 1.10.9

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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardAddForm.vue +1 -1
  3. package/components/AccessCardAssignToUnitForm.vue +1 -1
  4. package/components/AccessManagement.vue +1 -1
  5. package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
  6. package/components/BuildingManagement/units.vue +2 -2
  7. package/components/BuildingUnitFormAdd.vue +4 -4
  8. package/components/BuildingUnitFormEdit.vue +114 -68
  9. package/components/Carousel.vue +474 -0
  10. package/components/DrawImage.vue +172 -0
  11. package/components/EntryPassInformation.vue +283 -29
  12. package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +95 -87
  13. package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
  14. package/components/Feedback/Form.vue +4 -4
  15. package/components/FeedbackMain.vue +748 -145
  16. package/components/FileInput.vue +289 -0
  17. package/components/Input/DateTimePicker.vue +17 -11
  18. package/components/ManageChecklistMain.vue +379 -41
  19. package/components/StockCard.vue +11 -7
  20. package/components/TableHygiene.vue +42 -452
  21. package/components/UnitPersonCard.vue +74 -14
  22. package/components/VisitorForm.vue +193 -52
  23. package/components/VisitorFormSelection.vue +13 -2
  24. package/components/VisitorManagement.vue +83 -55
  25. package/composables/useAccessManagement.ts +41 -18
  26. package/composables/useCleaningPermission.ts +7 -7
  27. package/composables/useDashboardData.ts +2 -2
  28. package/composables/useEquipment.ts +63 -0
  29. package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
  30. package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
  31. package/composables/{useSupply.ts → useEquipmentManagement.ts} +1 -1
  32. package/composables/useEquipmentManagementPermission.ts +96 -0
  33. package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
  34. package/composables/useFeedback.ts +53 -21
  35. package/composables/useLocalAuth.ts +29 -1
  36. package/composables/useUploadFiles.ts +94 -0
  37. package/composables/useUtils.ts +152 -53
  38. package/composables/useVehicle.ts +21 -2
  39. package/composables/useVisitor.ts +9 -7
  40. package/composables/useWorkOrder.ts +25 -3
  41. package/package.json +2 -1
  42. package/types/building.d.ts +1 -1
  43. package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
  44. package/types/{supply.d.ts → equipment.d.ts} +2 -2
  45. package/types/feedback.d.ts +5 -2
  46. package/types/people.d.ts +3 -1
  47. package/types/user.d.ts +1 -0
  48. package/types/vehicle.d.ts +2 -0
  49. package/types/visitor.d.ts +2 -1
@@ -1,18 +1,7 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
3
  <v-col cols="12" class="mb-2">
4
- <v-row no-gutters align="center" justify="space-between">
5
- <v-btn
6
- class="text-none text-capitalize"
7
- rounded="pill"
8
- variant="tonal"
9
- size="large"
10
- @click="showCreateDialog = true"
11
- v-if="canCreateFeedback"
12
- >
13
- Create Feedback
14
- </v-btn>
15
-
4
+ <v-row no-gutters align="center">
16
5
  <v-text-field
17
6
  v-model="searchText"
18
7
  placeholder="Search feedback..."
@@ -20,8 +9,55 @@
20
9
  density="comfortable"
21
10
  clearable
22
11
  hide-details
12
+ />
13
+ <v-autocomplete
14
+ v-model="filterByStatus"
15
+ :items="['All', 'To-Do', 'In-Progress', 'For Review', 'Completed']"
16
+ label="Filter by status"
17
+ hide-details
18
+ class="ml-2"
19
+ style="max-width: 250px"
20
+ />
21
+ <v-text-field
22
+ v-model="startDate"
23
+ :value="standardFormatDate(startDate)"
24
+ label="Start Date"
25
+ readonly
26
+ hide-details
27
+ class="ml-2"
28
+ append-inner-icon="mdi-calendar"
29
+ style="max-width: 250px"
30
+ @click="startDateDialog = true"
31
+ />
32
+ <v-dialog v-model="startDateDialog" max-width="355">
33
+ <v-card>
34
+ <v-date-picker v-model="startDate" width="320" :locale="locale" />
35
+ </v-card>
36
+ </v-dialog>
37
+ <v-text-field
38
+ v-model="endDate"
39
+ :value="standardFormatDate(endDate)"
40
+ label="End Date"
41
+ readonly
42
+ hide-details
23
43
  class="ml-2"
44
+ append-inner-icon="mdi-calendar"
24
45
  style="max-width: 250px"
46
+ @click="endDateDialog = true"
47
+ />
48
+ <v-dialog v-model="endDateDialog" max-width="355">
49
+ <v-card>
50
+ <v-date-picker v-model="endDate" width="320" :locale="locale" />
51
+ </v-card>
52
+ </v-dialog>
53
+ <v-btn
54
+ v-if="canCreateFeedback"
55
+ text="create feedback"
56
+ class="text-none text-capitalize ml-2"
57
+ rounded="pill"
58
+ variant="tonal"
59
+ size="large"
60
+ @click="showCreateDialog = true"
25
61
  />
26
62
  </v-row>
27
63
  </v-col>
@@ -36,8 +72,13 @@
36
72
  >
37
73
  <v-toolbar density="compact" color="grey-lighten-4">
38
74
  <template #prepend>
39
- <v-btn fab icon density="comfortable" @click="updatePage(1)">
40
- <v-icon>mdi-refresh</v-icon>
75
+ <v-btn
76
+ fab
77
+ icon
78
+ density="comfortable"
79
+ @click="(page = 1), refreshFeedbacks()"
80
+ >
81
+ <v-icon icon="mdi-refresh" />
41
82
  </v-btn>
42
83
  </template>
43
84
 
@@ -66,12 +107,102 @@
66
107
  @click:row="tableRowClickHandler"
67
108
  style="max-height: calc(100vh - (200px))"
68
109
  >
110
+ <template #item.createdBy="{ item, index }">
111
+ <v-avatar
112
+ v-if="
113
+ item?.createdBy &&
114
+ Boolean(item?.createdBy) &&
115
+ typeof item?.createdBy === 'object' &&
116
+ Object.keys(item?.createdBy).length > 0
117
+ "
118
+ size="small"
119
+ :color="materialColors[index % materialColors.length]"
120
+ class="text-subtitle-2 mr-1 mr-md-4"
121
+ >
122
+ {{
123
+ getInitial(
124
+ `${item?.createdBy?.givenName ?? ""} ${
125
+ item?.createdBy?.surname ?? ""
126
+ }`
127
+ ) || ""
128
+ }}
129
+ </v-avatar>
130
+ {{
131
+ `${item?.createdBy?.givenName ?? ""} ${
132
+ item?.createdBy?.surname ?? ""
133
+ }`
134
+ }}
135
+ </template>
136
+ <template #item.subject="{ item }">
137
+ {{ item.subject || "N/A" }}
138
+ <v-tooltip text="High Priority" location="end">
139
+ <template v-slot:activator="{ props }">
140
+ <v-icon
141
+ v-if="item.highPriority"
142
+ icon="mdi-alert-circle-outline"
143
+ color="red"
144
+ class="mb-1"
145
+ size="large"
146
+ v-bind="props"
147
+ />
148
+ </template>
149
+ </v-tooltip>
150
+ </template>
151
+ <template #item.createdAt="{ value }">
152
+ <v-icon icon="mdi-calendar-month-outline" />
153
+ {{ standardFormatDateTime(value || "") }}
154
+ </template>
155
+ <template #item.status="{ value }">
156
+ <span class="d-inline-block text-truncate">
157
+ <v-chip
158
+ :color="getStatusColor(value ?? '')"
159
+ class="pa-5 text-capitalize text-center whitespace-nowrap text-body-1"
160
+ variant="flat"
161
+ >
162
+ {{ value }}
163
+ </v-chip>
164
+ </span>
165
+ </template>
166
+ <template #item.assigneeInfo="{ item, index }">
167
+ <!-- <span v-if="!item.assigneeInfo">
168
+ <v-btn
169
+ :block="$vuetify.display.xs"
170
+ text="accept"
171
+ variant="flat"
172
+ color="primary"
173
+ :class="`text-${$vuetify.display.xs ? 'h5' : 'body-1'}`"
174
+ :disabled="canAccept(item.raw) ? false : true"
175
+ @click.stop="accept(item.raw)"
176
+ />
177
+ </span> -->
178
+ <span v-if="item.assigneeInfo" class="mt-1 text-wrap">
179
+ <v-avatar
180
+ size="small"
181
+ :color="materialColors[index % materialColors.length]"
182
+ class="mr-2 text-white"
183
+ >
184
+ {{
185
+ getInitial(
186
+ `${item.assigneeInfo?.givenName ?? ""} ${
187
+ item.assigneeInfo?.surname ?? ""
188
+ }`
189
+ ) || ""
190
+ }}
191
+ </v-avatar>
192
+ {{
193
+ `${item.assigneeInfo?.givenName ?? ""} ${
194
+ item.assigneeInfo?.surname ?? ""
195
+ }` || "N/A"
196
+ }}
197
+ </span>
198
+ <span v-else class="mt-1 text-wrap"> Not Assigned </span>
199
+ </template>
69
200
  </v-data-table>
70
201
  </v-card>
71
202
  </v-col>
72
203
  </v-row>
73
204
 
74
- <FeedbackForm
205
+ <!-- <FeedbackForm
75
206
  v-model="showCreateDialog"
76
207
  :feedback="_feedback"
77
208
  :is-edit-mode="isEditMode"
@@ -84,7 +215,207 @@
84
215
  @submit="submitFeedback"
85
216
  @file-added="handleFileAdded"
86
217
  @file-deleted="deleteFile"
87
- />
218
+ /> -->
219
+
220
+ <!-- create feeback dialog -->
221
+ <v-dialog
222
+ v-model="showCreateDialog"
223
+ transition="dialog-bottom-transition"
224
+ style="max-width: 650px"
225
+ >
226
+ <v-card class="rounded-lg pa-0">
227
+ <v-toolbar flat class="rounded-tl-lg rounded-tr-lg">
228
+ <span class="ml-6 font-weight-bold text-1-4rem"> Create Feedback </span>
229
+ <v-spacer />
230
+ <v-btn @click="showCreateDialog = false">
231
+ <v-icon icon="mdi-close" size="40" />
232
+ </v-btn>
233
+ </v-toolbar>
234
+
235
+ <v-row no-gutters class="pa-8">
236
+ <v-col cols="12" class="text-center">
237
+ <Carousel
238
+ v-if="attachedFiles && attachedFiles.length > 0"
239
+ :data-files="
240
+ attachedFiles.map((file) => ({
241
+ name: file.data.name,
242
+ data: file.data,
243
+ progress: 100,
244
+ type: file.data.type,
245
+ url: file.url,
246
+ }))
247
+ "
248
+ :clickable="true"
249
+ :img-editable="true"
250
+ @on-file-edit="onImageEdited"
251
+ :img-delete="true"
252
+ @on-file-delete="removeFile"
253
+ />
254
+ <span v-else class="font-weight-bold text-subtitle-1">
255
+ No attached image(s) found.
256
+ </span>
257
+ </v-col>
258
+
259
+ <v-col cols="12" class="mt-6">
260
+ <div class="text-subtitle-1 text-medium-emphasis">
261
+ Attach image (Optional)
262
+ </div>
263
+ <FileInput
264
+ :init-files="attachedFiles.map((file) => file.data)"
265
+ @update:files="handleFileUpdate"
266
+ @on-clear="attachedFiles = []"
267
+ />
268
+ </v-col>
269
+
270
+ <v-col cols="12" class="my-2">
271
+ <div class="text-subtitle-1 text-medium-emphasis">
272
+ Subject
273
+ <span class="text-red">* (Required)</span>
274
+ </div>
275
+ <v-combobox
276
+ v-model="_feedback.subject"
277
+ :items="subjectOptions"
278
+ density="comfortable"
279
+ placeholder="Enter subject"
280
+ clearable
281
+ :filter-keys="['title', 'description']"
282
+ :persistent-hint="false"
283
+ hide-details
284
+ >
285
+ <template v-slot:item="{ props, item }">
286
+ <v-row no-gutters v-bind="props" class="px-3">
287
+ <v-col cols="12" class="pa-2">
288
+ <span class="font-weight-bold">
289
+ {{ item.raw.title }}
290
+ </span>
291
+ <span
292
+ class="d-flex align-center flex-wrap mx-auto px-4 text-caption"
293
+ >
294
+ {{ item.raw.description }}
295
+ </span>
296
+ </v-col>
297
+ </v-row>
298
+ </template>
299
+ </v-combobox>
300
+ </v-col>
301
+
302
+ <v-col cols="12" class="mb-2">
303
+ <div class="text-subtitle-1 text-medium-emphasis">
304
+ Location
305
+ <span class="text-red">* (Required)</span>
306
+ </div>
307
+ <v-text-field
308
+ v-model="_feedback.location"
309
+ density="comfortable"
310
+ placeholder="Enter location"
311
+ clearable
312
+ :persistent-hint="false"
313
+ />
314
+ </v-col>
315
+
316
+ <v-col cols="12">
317
+ <div class="text-subtitle-1 text-medium-emphasis">
318
+ Description
319
+ <span class="text-red">* (Required)</span>
320
+ </div>
321
+ <v-textarea
322
+ v-model="_feedback.description"
323
+ density="comfortable"
324
+ placeholder="Enter description"
325
+ clearable
326
+ :persistent-hint="false"
327
+ no-resize
328
+ ></v-textarea>
329
+ </v-col>
330
+
331
+ <!-- <v-col cols="12" class="mb-4">
332
+ <v-btn
333
+ color="primary"
334
+ height="auto"
335
+ class="mb-4"
336
+ variant="flat"
337
+ @click="attachWorkOrderDialog = true"
338
+ >
339
+ <div class="d-flex justify-space-between align-center w-100 py-2">
340
+ <v-icon
341
+ icon="mdi-check-circle-outline"
342
+ size="50"
343
+ :color="attachWorkOrderForm.attachWorkOrder && 'green'"
344
+ />
345
+ <span class="text-wrap ml-4"> Create and Attach Work Order </span>
346
+ </div>
347
+ </v-btn>
348
+ </v-col> -->
349
+
350
+ <v-col cols="12">
351
+ <v-btn
352
+ text="submit"
353
+ block
354
+ size="large"
355
+ class="font-weight-bold"
356
+ color="primary"
357
+ :disabled="isFileUploading || isSubmitting"
358
+ @click="submit()"
359
+ :loading="isFileUploading || isSubmitting"
360
+ />
361
+ </v-col>
362
+ </v-row>
363
+ </v-card>
364
+ </v-dialog>
365
+
366
+ <!-- attach work order dialog -->
367
+ <!-- <v-dialog
368
+ v-model="attachWorkOrderDialog"
369
+ transition="dialog-bottom-transition"
370
+ style="max-width: 650px"
371
+ >
372
+ <v-card class="rounded-lg">
373
+ <v-toolbar>
374
+ <span class="ml-6 font-weight-bold">
375
+ Create and Attach Work Order
376
+ </span>
377
+ <v-spacer />
378
+ <v-btn @click="attachWorkOrderDialog = false">
379
+ <v-icon icon="mdi-close" size="40" />
380
+ </v-btn>
381
+ </v-toolbar>
382
+
383
+ <v-col cols="12" class="px-4">
384
+ <v-textarea
385
+ v-model="attachWorkOrderForm.description"
386
+ label="Email"
387
+ autocomplete="email"
388
+ density="default"
389
+ class="pt-0 mt-0"
390
+ hide-details
391
+ clearable
392
+ >
393
+ <template v-slot:label>
394
+ Description
395
+ <span class="text-error"> (Required)*</span>
396
+ </template>
397
+ </v-textarea>
398
+ <v-checkbox
399
+ v-model="attachWorkOrderForm.isHighPriority"
400
+ label="Set Work Order as High Priority (Optional)"
401
+ class="mb-0 pb-1 mb-n1 ml-n2"
402
+ hide-details
403
+ />
404
+ </v-col>
405
+
406
+ <v-col cols="12" class="mb-2 px-4">
407
+ <v-btn
408
+ block
409
+ :text="attachWorkOrderForm.attachWorkOrder ? 'detach' : 'attach'"
410
+ class="rounded-lg"
411
+ color="primary"
412
+ height="40px"
413
+ elevation="0"
414
+ @click="attachWorkOrder()"
415
+ />
416
+ </v-col>
417
+ </v-card>
418
+ </v-dialog> -->
88
419
 
89
420
  <ConfirmDialog
90
421
  v-model="showDeleteDialog"
@@ -206,9 +537,44 @@
206
537
  </v-toolbar>
207
538
  </v-card></v-dialog
208
539
  >
540
+
541
+ <!-- accept dialog -->
542
+ <v-dialog
543
+ v-model="acceptDialog"
544
+ transition="dialog-bottom-transition"
545
+ style="max-width: 650px"
546
+ >
547
+ <v-card class="rounded-lg pa-0">
548
+ <v-toolbar flat class="rounded-tl-lg rounded-tr-lg">
549
+ <span class="ml-6 font-weight-bold"> Accept Feedback </span>
550
+ <v-spacer />
551
+ <v-btn @click="acceptDialog = false">
552
+ <v-icon icon="mdi-close" size="40" />
553
+ </v-btn>
554
+ </v-toolbar>
555
+
556
+ <v-card-text class="text-center text-h6">
557
+ Are you sure you want to accept this feedback?
558
+ </v-card-text>
559
+
560
+ <v-col cols="12" class="mb-2 px-4">
561
+ <v-btn
562
+ text="confirm"
563
+ class="rounded-lg"
564
+ color="primary"
565
+ block
566
+ size="x-large"
567
+ elevation="0"
568
+ @click="handleAccept()"
569
+ />
570
+ </v-col>
571
+ </v-card>
572
+ </v-dialog>
209
573
  </template>
210
574
 
211
575
  <script setup lang="ts">
576
+ import moment from "moment-timezone";
577
+
212
578
  const props = defineProps({
213
579
  detailRoute: {
214
580
  type: String,
@@ -265,7 +631,7 @@ const message = ref("");
265
631
  const messageColor = ref("");
266
632
  const messageSnackbar = ref(false);
267
633
 
268
- const { getUserFromCookie } = useLocal();
634
+ const { currentUser } = useLocalAuth();
269
635
 
270
636
  const serviceProviders = ref<
271
637
  Array<{ title: string; value: string; subtitle: string }>
@@ -301,29 +667,34 @@ const _feedback = ref({
301
667
  location: "",
302
668
  description: "",
303
669
  highPriority: false,
304
- attachments: [] as string[],
670
+ attachments: [] as Array<{ _id: string; name: string; type: string }>,
305
671
  serviceProvider: "",
306
672
  assignee: "",
307
673
  organization: "",
308
674
  site: "",
309
675
  });
310
676
 
311
- const selected = ref<string[]>([]);
312
-
313
- const { getColorStatus, debounce } = useUtils();
677
+ const {
678
+ getStatusColor,
679
+ debounce,
680
+ standardFormatDateTime,
681
+ standardFormatDate,
682
+ materialColors,
683
+ getInitial,
684
+ getImage,
685
+ } = useUtils();
314
686
 
315
687
  const headers: Array<Record<string, any>> = [
316
688
  { title: "Name", value: "createdByName", align: "start" },
317
689
  { title: "Subject", value: "subject", align: "start" },
318
- { title: "Category", value: "category", align: "start" },
690
+ { title: "Date", value: "createdAt", align: "start" },
691
+ { title: "App", value: "app", align: "start" },
319
692
  { title: "Status", value: "status", align: "start" },
693
+ { title: "Assignee", value: "assigneeInfo", align: "start" },
320
694
  { title: "", value: "action-table", align: "end" },
321
695
  ];
322
696
 
323
- // const loading = ref(false);
324
- const erroredImages = ref<string[]>([]);
325
697
  const route = useRoute();
326
- const { customers } = useCustomer();
327
698
 
328
699
  const dialogPreview = ref(false);
329
700
  const selectedFeedback = ref<TFeedback | null>(null);
@@ -342,49 +713,69 @@ const pages = ref(0);
342
713
  const pageRange = ref("-- - -- of --");
343
714
  const items = ref<Array<Record<string, any>>>([]);
344
715
 
716
+ const filterByStatus = ref("All");
717
+ const getPast30DaysDate = (): Date => {
718
+ const today = new Date();
719
+ const past30Days = new Date(today);
720
+ past30Days.setDate(today.getDate() - 30);
721
+ return past30Days;
722
+ };
723
+ const startDate = ref<any>(getPast30DaysDate());
724
+ const startDateDialog = ref(false);
725
+ const endDate = ref<any>(new Date());
726
+ const endDateDialog = ref(false);
727
+ const locale = "en";
728
+
345
729
  const {
346
- data: getAllFeedbackReq,
347
- status: getAllReqStatus,
348
- refresh: getAllReqRefresh,
349
- } = useLazyAsyncData("get-all-feedbacks-" + props.category, () =>
350
- _getFeedbacks({
351
- page: page.value,
352
- site: route.params.site as string,
353
- category: props.category,
354
- search: searchText.value || "",
355
- })
730
+ data: feedbacks,
731
+ status: getFeedbacksStatus,
732
+ refresh: refreshFeedbacks,
733
+ } = useLazyAsyncData(
734
+ "get-all-feedbacks",
735
+ () =>
736
+ _getFeedbacks({
737
+ page: page.value,
738
+ site: route.params.site as string,
739
+ provider: currentUser.value.serviceProvider,
740
+ ...(currentUser.value.type != "site" && {
741
+ userId: currentUser.value._id,
742
+ }),
743
+ service: "Security",
744
+ dateFrom: moment(startDate.value, "DD/MM/YYYY").startOf("day"),
745
+ dateTo: moment(endDate.value, "DD/MM/YYYY").endOf("day"),
746
+ search: searchText.value,
747
+ status: filterByStatus.value == "All" ? "" : filterByStatus.value,
748
+ }),
749
+ {
750
+ watch: [filterByStatus, searchText, page, startDate, endDate],
751
+ }
356
752
  );
357
753
 
358
- const loading = computed(() => getAllReqStatus.value === "pending");
359
- const onNextPrevPageLoading = ref(false);
360
-
361
754
  watchEffect(() => {
362
- if (getAllFeedbackReq.value) {
363
- items.value = getAllFeedbackReq.value.items;
364
- pages.value = getAllFeedbackReq.value.pages;
365
- pageRange.value = getAllFeedbackReq.value.pageRange;
755
+ if (feedbacks.value) {
756
+ items.value = feedbacks.value?.items;
757
+ pages.value = feedbacks.value?.pages;
758
+ pageRange.value = feedbacks.value?.pageRange;
759
+ }
760
+
761
+ if (startDate.value) {
762
+ startDateDialog.value = false;
763
+ }
764
+
765
+ if (endDate.value) {
766
+ endDateDialog.value = false;
366
767
  }
367
768
  });
368
769
 
770
+ const loading = computed(() => getFeedbacksStatus.value === "pending");
771
+
369
772
  async function updatePage(pageVal: any) {
370
- onNextPrevPageLoading.value = true;
371
773
  page.value = pageVal;
372
- const response = await _getFeedbacks({
373
- page: page.value,
374
- site: route.params.site as string,
375
- category: props.category,
376
- });
377
- if (response) {
378
- items.value = response.items;
379
- pages.value = response.pages;
380
- pageRange.value = response.pageRange;
381
- onNextPrevPageLoading.value = false;
382
- }
383
774
  }
384
775
 
385
- function onSelectedUpdate(newSelected: string[]) {
386
- selected.value = newSelected;
387
- }
776
+ // function onSelectedUpdate(newSelected: string[]) {
777
+ // selected.value = newSelected;
778
+ // }
388
779
 
389
780
  async function editFeedback(item: any) {
390
781
  try {
@@ -436,7 +827,7 @@ async function submitDelete() {
436
827
  try {
437
828
  const response = await deleteFeedback(feedbackToDelete.value._id);
438
829
  showMessage(response.message, "success");
439
- await getAllReqRefresh();
830
+ await refreshFeedbacks();
440
831
  } catch (error) {
441
832
  console.error("Failed to delete feedback:", error);
442
833
  showMessage("Failed to delete feedback!", "error");
@@ -450,7 +841,7 @@ async function submitDelete() {
450
841
 
451
842
  const resetFeedbackForm = () => {
452
843
  _feedback.value = {
453
- attachments: [] as string[],
844
+ attachments: [] as Array<{ _id: string; name: string; type: string }>,
454
845
  category: "",
455
846
  subject: "",
456
847
  location: "",
@@ -468,67 +859,68 @@ function handleCloseDialog() {
468
859
  isEditMode.value = false;
469
860
  showCreateDialog.value = false;
470
861
  }
471
- const _feedbackId = ref<string | null>(null);
472
-
473
- async function _createFeedback(organization: string, site: string) {
474
- const userId = getUserFromCookie();
475
-
476
- const createPayload: TFeedbackCreate = {
477
- subject: _feedback.value.subject,
478
- category: _feedback.value.category,
479
- location: _feedback.value.location,
480
- description: _feedback.value.description,
481
- highPriority: _feedback.value.highPriority ?? false,
482
- attachments: _feedback.value.attachments || [],
483
- serviceProvider: _feedback.value.serviceProvider || "",
484
- assignee: _feedback.value.assignee || "",
485
- organization,
486
- site,
487
- ...(userId !== null ? { createdBy: userId } : {}),
488
- };
489
862
 
490
- const response = await createFeedback(createPayload);
491
- showMessage(response?.message || "Feedback created successfully", "success");
492
- }
863
+ // const _feedbackId = ref<string | null>(null);
493
864
 
494
- async function _updateFeedback() {
495
- if (!_feedbackId.value) return;
865
+ // async function _createFeedback(organization: string, site: string) {
866
+ // const userId = getUserFromCookie();
496
867
 
497
- const updatePayload: TFeedbackUpdate = {
498
- subject: _feedback.value.subject,
499
- category: _feedback.value.category,
500
- location: _feedback.value.location,
501
- description: _feedback.value.description,
502
- attachments: _feedback.value.attachments || [],
503
- };
868
+ // const createPayload: TFeedbackCreate = {
869
+ // subject: _feedback.value.subject,
870
+ // category: _feedback.value.category,
871
+ // location: _feedback.value.location,
872
+ // description: _feedback.value.description,
873
+ // highPriority: _feedback.value.highPriority ?? false,
874
+ // attachments: _feedback.value.attachments || [],
875
+ // serviceProvider: _feedback.value.serviceProvider || "",
876
+ // assignee: _feedback.value.assignee || "",
877
+ // organization,
878
+ // site,
879
+ // ...(userId !== null ? { createdBy: userId } : {}),
880
+ // };
504
881
 
505
- const response = await updateFeedback(_feedbackId.value, updatePayload);
506
- showMessage(response?.message || "Feedback updated successfully", "success");
507
- }
882
+ // const response = await createFeedback(createPayload);
883
+ // showMessage(response?.message || "Feedback created successfully", "success");
884
+ // }
508
885
 
509
- const submitFeedback = async () => {
510
- submitting.value = true;
886
+ // async function _updateFeedback() {
887
+ // if (!_feedbackId.value) return;
511
888
 
512
- try {
513
- const organization = route.params.org as string;
514
- const site = route.params.site as string;
889
+ // const updatePayload: TFeedbackUpdate = {
890
+ // subject: _feedback.value.subject,
891
+ // category: _feedback.value.category,
892
+ // location: _feedback.value.location,
893
+ // description: _feedback.value.description,
894
+ // attachments: _feedback.value.attachments || [],
895
+ // };
515
896
 
516
- if (isEditMode.value) {
517
- await _updateFeedback();
518
- } else {
519
- await _createFeedback(organization, site);
520
- }
897
+ // const response = await updateFeedback(_feedbackId.value, updatePayload);
898
+ // showMessage(response?.message || "Feedback updated successfully", "success");
899
+ // }
521
900
 
522
- showCreateDialog.value = false;
523
- await getAllReqRefresh();
524
- resetFeedbackForm();
525
- } catch (error) {
526
- console.error(error);
527
- showMessage("Something went wrong", "error");
528
- } finally {
529
- submitting.value = false;
530
- }
531
- };
901
+ // const submitFeedback = async () => {
902
+ // submitting.value = true;
903
+
904
+ // try {
905
+ // const organization = route.params.org as string;
906
+ // const site = route.params.site as string;
907
+
908
+ // if (isEditMode.value) {
909
+ // await _updateFeedback();
910
+ // } else {
911
+ // await _createFeedback(organization, site);
912
+ // }
913
+
914
+ // showCreateDialog.value = false;
915
+ // await refreshFeedbacks();
916
+ // resetFeedbackForm();
917
+ // } catch (error) {
918
+ // console.error(error);
919
+ // showMessage("Something went wrong", "error");
920
+ // } finally {
921
+ // submitting.value = false;
922
+ // }
923
+ // };
532
924
 
533
925
  function showMessage(msg: string, color: string) {
534
926
  message.value = msg;
@@ -538,36 +930,36 @@ function showMessage(msg: string, color: string) {
538
930
 
539
931
  const { addFile, deleteFile: _deleteFile } = useFile();
540
932
 
541
- const API_DO_STORAGE_ENDPOINT =
542
- useRuntimeConfig().public.API_DO_STORAGE_ENDPOINT;
933
+ // const API_DO_STORAGE_ENDPOINT =
934
+ // useRuntimeConfig().public.API_DO_STORAGE_ENDPOINT;
543
935
 
544
- async function handleFileAdded(file: File) {
545
- try {
546
- const res = await addFile(file);
936
+ // async function handleFileAdded(file: File) {
937
+ // try {
938
+ // const res = await addFile(file);
547
939
 
548
- const uploadedId = res?.id;
549
- if (uploadedId) {
550
- const url = `${uploadedId}`;
551
- _feedback.value.attachments = _feedback.value.attachments ?? [];
552
- _feedback.value.attachments.push(url);
553
- }
554
- } catch (error) {
555
- console.error("Error uploading file:", error);
556
- showMessage("Failed to upload file", "error");
557
- }
558
- }
940
+ // const uploadedId = res?.id;
941
+ // if (uploadedId) {
942
+ // const url = `${uploadedId}`;
943
+ // _feedback.value.attachments = _feedback.value.attachments ?? [];
944
+ // _feedback.value.attachments.push(url);
945
+ // }
946
+ // } catch (error) {
947
+ // console.error("Error uploading file:", error);
948
+ // showMessage("Failed to upload file", "error");
949
+ // }
950
+ // }
559
951
 
560
- async function deleteFile(value: string) {
561
- try {
562
- await _deleteFile(value);
563
- _feedback.value.attachments = (_feedback.value.attachments ?? []).filter(
564
- (file) => file !== value
565
- );
566
- } catch (error) {
567
- console.log(error);
568
- showMessage("Failed to delete file", "error");
569
- }
570
- }
952
+ // async function deleteFile(value: string) {
953
+ // try {
954
+ // await _deleteFile(value);
955
+ // _feedback.value.attachments = (_feedback.value.attachments ?? []).filter(
956
+ // (file) => file !== value
957
+ // );
958
+ // } catch (error) {
959
+ // console.log(error);
960
+ // showMessage("Failed to delete file", "error");
961
+ // }
962
+ // }
571
963
 
572
964
  const { serviceProviderCategories, getServiceProviderCategories } =
573
965
  useServiceProvider();
@@ -587,12 +979,223 @@ function tableRowClickHandler(_: any, data: any) {
587
979
 
588
980
  getServiceProviderCategories();
589
981
 
590
- const debounceSearch = debounce(getAllReqRefresh, 500);
982
+ onMounted(() => {
983
+ _feedback.value.description = "";
984
+ _feedback.value.location = "";
985
+ _feedback.value.subject = "";
986
+ _feedback.value.attachments = [];
987
+ });
591
988
 
592
- watch(
593
- () => searchText.value,
594
- () => {
595
- debounceSearch();
989
+ const acceptDialog = ref(false);
990
+ const feedbackData = ref();
991
+
992
+ function canAccept(feedBack: any) {
993
+ return (
994
+ feedBack.provider === currentUser.value?.serviceProvider ||
995
+ (feedBack.subject &&
996
+ feedBack.subject.toLowerCase().includes("security") &&
997
+ !feedBack.provider)
998
+ );
999
+ }
1000
+ async function accept(feedBack: any) {
1001
+ acceptDialog.value = true;
1002
+ feedbackData.value = feedBack;
1003
+ }
1004
+ async function handleAccept() {
1005
+ if (
1006
+ feedbackData.value.provider === currentUser.value?.serviceProvider ||
1007
+ (feedbackData.value.subject &&
1008
+ feedbackData.value.subject.toLowerCase().includes("security") &&
1009
+ !feedbackData.value.provider)
1010
+ ) {
1011
+ try {
1012
+ const data: any = {
1013
+ _id: feedbackData.value._id,
1014
+ statusUpdate: {
1015
+ status: "In-Progress",
1016
+ updatedById: currentUser.value._id,
1017
+ updatedByName: `${currentUser.value.givenName} ${currentUser.value.surname}`,
1018
+ assignee: currentUser.value._id,
1019
+ provider: currentUser.value?.serviceProvider,
1020
+ },
1021
+ };
1022
+
1023
+ // await updateStatusFeedback(data);
1024
+
1025
+ acceptDialog.value = false;
1026
+ // changePath(feedbackData.value);
1027
+ } catch (error) {
1028
+ showMessage(error as string, "error");
1029
+ }
1030
+ } else {
1031
+ return showMessage(
1032
+ "Only assigned Provider can accept this feedback.",
1033
+ "error"
1034
+ );
596
1035
  }
597
- );
1036
+ }
1037
+
1038
+ const subjectOptions = computed(() => {
1039
+ return [
1040
+ {
1041
+ title: "Facilities",
1042
+ description:
1043
+ "Shared amenities like gym, pool, BBQ pits, and function rooms.",
1044
+ },
1045
+ {
1046
+ title: "Building Facade",
1047
+ description:
1048
+ "Exterior walls, paint, cladding, signage, and overall appearance.",
1049
+ },
1050
+ {
1051
+ title: "Security",
1052
+ description:
1053
+ "Guard services, access control, patrols, and visitor management.",
1054
+ },
1055
+ {
1056
+ title: "Cleaning",
1057
+ description:
1058
+ "Cleanliness of lobbies, corridors, lifts, bins, and common areas.",
1059
+ },
1060
+ {
1061
+ title: "Landscape",
1062
+ description: "Condition of plants, lawns, trees, and garden maintenance.",
1063
+ },
1064
+ {
1065
+ title: "Pest Control",
1066
+ description:
1067
+ "Effectiveness of measures against insects, rodents, and pests.",
1068
+ },
1069
+ {
1070
+ title: "Water Features",
1071
+ description:
1072
+ "Operation and cleanliness of fountains, ponds, and pools (non-swimming).",
1073
+ },
1074
+ {
1075
+ title: "Car Park",
1076
+ description:
1077
+ "Parking availability, markings, lighting, and barrier systems.",
1078
+ },
1079
+ {
1080
+ title: "Lift",
1081
+ description:
1082
+ "Lift reliability, speed, cleanliness, ventilation, and downtime.",
1083
+ },
1084
+ {
1085
+ title: "Security Systems",
1086
+ description: "CCTV, intercoms, access cards, and gate/door sensors.",
1087
+ },
1088
+ {
1089
+ title: "Others",
1090
+ description:
1091
+ "Any issues or feedback not covered by the categories above.",
1092
+ },
1093
+ ]
1094
+ .filter((item) => item.title !== "Others")
1095
+ .sort((a, b) => a.title.localeCompare(b.title))
1096
+ .concat({
1097
+ title: "Others",
1098
+ description:
1099
+ "Any issues or feedback not covered by the categories above.",
1100
+ });
1101
+ });
1102
+
1103
+ const isFileUploading = ref<boolean>(false);
1104
+ type TFile = {
1105
+ name: string;
1106
+ data: File;
1107
+ progress: number;
1108
+ url?: string;
1109
+ type?: string;
1110
+ id?: string;
1111
+ };
1112
+ const attachedFiles = ref<TFile[]>([]);
1113
+
1114
+ const handleFileUpdate = async (
1115
+ files: Array<{ data: File; name: string; url: string }>
1116
+ ) => {
1117
+ for (const file of files) {
1118
+ attachedFiles.value.push(file);
1119
+ }
1120
+ };
1121
+
1122
+ const onImageEdited = async (url: string, idx: number) => {
1123
+ const response = await getImage(url);
1124
+ if (!response) return;
1125
+ attachedFiles.value[idx].data = new File(
1126
+ [response],
1127
+ attachedFiles.value[idx].data?.name,
1128
+ { type: attachedFiles.value[idx].data?.type }
1129
+ );
1130
+ attachedFiles.value[idx].url = url;
1131
+ };
1132
+
1133
+ function removeFile(url: string) {
1134
+ const newFiles = attachedFiles.value.filter((item: any) => item.url !== url);
1135
+
1136
+ attachedFiles.value = [];
1137
+ nextTick(() => {
1138
+ attachedFiles.value = newFiles;
1139
+ });
1140
+ }
1141
+
1142
+ const attachWorkOrderDialog = ref(false);
1143
+ const attachWorkOrderForm = ref<any>({
1144
+ description: "",
1145
+ isHighPriority: false,
1146
+ attachWorkOrder: false,
1147
+ });
1148
+
1149
+ const attachWorkOrder = () => {
1150
+ attachWorkOrderForm.value.attachWorkOrder =
1151
+ !attachWorkOrderForm.value.attachWorkOrder;
1152
+ attachWorkOrderDialog.value = false;
1153
+ };
1154
+
1155
+ const isSubmitting = ref<boolean>(false);
1156
+
1157
+ async function submit() {
1158
+ if (attachedFiles.value && attachedFiles.value?.length) {
1159
+ isFileUploading.value = true;
1160
+ for (const file of attachedFiles.value) {
1161
+ const res = await addFile(file.data);
1162
+
1163
+ const uploadedId = res?.id;
1164
+ if (uploadedId) {
1165
+ _feedback.value.attachments = _feedback.value.attachments ?? [];
1166
+ _feedback.value.attachments.push(uploadedId);
1167
+ }
1168
+ }
1169
+ isFileUploading.value = false;
1170
+ }
1171
+
1172
+ try {
1173
+ isSubmitting.value = true;
1174
+
1175
+ const result = await createFeedback({
1176
+ createdBy: currentUser.value._id,
1177
+ description: _feedback.value.description,
1178
+ attachments: _feedback.value.attachments,
1179
+ site: route.params.site as string,
1180
+ organization: route.params.org as string,
1181
+ subject: _feedback.value.subject?.title || _feedback.value.subject,
1182
+ location: _feedback.value.location,
1183
+ app: "MA",
1184
+ ...(attachWorkOrderForm.value.attachWorkOrder && {
1185
+ provider: _feedback.value.serviceProvider,
1186
+ workOrder: {
1187
+ isHighPriority: attachWorkOrderForm.value.isHighPriority,
1188
+ description: attachWorkOrderForm.value.description,
1189
+ },
1190
+ }),
1191
+ });
1192
+ handleCloseDialog();
1193
+ await refreshFeedbacks();
1194
+ showMessage(result.message as string, "success");
1195
+ } catch (error) {
1196
+ showMessage(error as string, "error");
1197
+ } finally {
1198
+ isSubmitting.value = false;
1199
+ }
1200
+ }
598
1201
  </script>