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