@iservice365/layer-common 1.0.9 → 1.0.11

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 368e98f: Invitation - Add key to API request
8
+
9
+ ## 1.0.10
10
+
11
+ ### Patch Changes
12
+
13
+ - 4cdf32b: Invitation form - revise app selection
14
+
3
15
  ## 1.0.9
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <v-avatar :color="getColor(name || id)" :size="size">
3
+ <v-img v-if="imageSrc" alt="John" :src="imageUrl" />
4
+ <span v-else :style="{
5
+ fontSize: `${fontSize}px`,
6
+ lineHeight: `${size}px`
7
+ }">{{ initials }}</span>
8
+ </v-avatar>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ const props = defineProps({
13
+ name: {
14
+ type: String,
15
+ default: ""
16
+ },
17
+ color: {
18
+ type: String,
19
+ default: 'red'
20
+ },
21
+ size: {
22
+ type: Number,
23
+ default: 40
24
+ },
25
+ id: {
26
+ type: String, //fallback for constant default color without name
27
+ default: ""
28
+ },
29
+ imageSrc:{
30
+ type: String,
31
+ default: ""
32
+ }
33
+
34
+ })
35
+
36
+
37
+ const { getNameInitials } = useUtils();
38
+ const { getFileUrl } = useFile();
39
+
40
+ const initials = computed(() => {
41
+ return getNameInitials(props.name)
42
+ })
43
+
44
+ const fontSize = computed(() => Math.round(props.size * 0.5))
45
+
46
+ function getColor(str?: string): string {
47
+ if(!str) return "grey"
48
+
49
+ let hash = 0
50
+ for (let i = 0; i < str.length; i++) {
51
+ hash = (hash << 5) - hash + str.charCodeAt(i)
52
+ hash |= 0 // Convert to 32-bit integer
53
+ }
54
+
55
+ const hue = Math.abs(hash) % 360
56
+ const saturation = 60 + (Math.abs((hash >> 3) % 20)) // 60–80%
57
+ const lightness = 45 + (Math.abs((hash >> 5) % 15)) // 45–60%
58
+
59
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`
60
+ }
61
+
62
+ const imageUrl = computed(() => {
63
+ return getFileUrl(props?.imageSrc || '')
64
+ })
65
+
66
+ </script>
67
+
68
+ <style scoped></style>
@@ -8,51 +8,19 @@
8
8
  <v-row class="pa-4">
9
9
  <v-sheet
10
10
  class="overflow-y-auto flex-grow-1"
11
- :style="{ height: 'calc(100vh - 180px)', overflowX: 'hidden' }"
11
+ :style="{ height: 'calc(100vh - 250px)', overflowX: 'hidden' }"
12
12
  >
13
13
  <v-col
14
14
  cols="12"
15
15
  class="mb-2"
16
16
  style="position: sticky; top: 0; background-color: white; z-index: 1"
17
17
  >
18
- <h3 class="text-h6">Information</h3>
18
+ <h3 class="text-h6">
19
+ {{ type === "workOrder" ? "Work Order" : "Feedback" }} Information
20
+ </h3>
19
21
  </v-col>
20
22
 
21
- <v-col cols="12" class="text-center mb-3" v-if="item">
22
- <!-- <v-avatar
23
- size="48"
24
- class="mb-1"
25
- v-if="!item.attachments || !item.attachments.length"
26
- >
27
- <v-img v-if="currentUser?.profile" width="15" height="15" />
28
- <span v-else class="text-h5">{{
29
- getNameInitials(senderName)
30
- }}</span>
31
- </v-avatar>
32
-
33
- <v-img
34
- v-else-if="item.attachments.length === 1"
35
- :src="item.attachments[0]"
36
- height="200"
37
- cover
38
- class="rounded mb-2"
39
- />
40
-
41
- <v-carousel
42
- v-else
43
- hide-delimiter-background
44
- height="200"
45
- class="rounded mb-2"
46
- show-arrows
47
- cycle
48
- >
49
- <v-carousel-item
50
- v-for="(attachment, index) in item.attachments"
51
- :key="index"
52
- >
53
- <v-img :src="`/api/public/${attachment}`" cover class="w-100 h-100" />
54
- </v-carousel-item>
55
- </v-carousel> -->
23
+ <v-col cols="12" class="text-center mb-0" v-if="item">
56
24
  <v-img
57
25
  v-if="item.attachments.length === 1"
58
26
  :src="`/api/public/${item.attachments[0]}`"
@@ -61,7 +29,7 @@
61
29
  class="rounded mb-2"
62
30
  />
63
31
  <v-carousel
64
- v-else
32
+ v-else-if="item.attachments.length > 1"
65
33
  hide-delimiter-background
66
34
  height="200"
67
35
  class="rounded mb-2"
@@ -82,10 +50,19 @@
82
50
  </v-col>
83
51
 
84
52
  <v-col cols="12" v-if="item">
85
- <v-row dense class="my-1">
53
+ <v-row dense class="my-1" v-if="type === 'feedback'">
54
+ <v-col cols="12" class="py-1"><strong>Subject:</strong></v-col>
55
+ <v-col cols="12" class="py-1">
56
+ {{ item.subject }}
57
+ </v-col>
58
+ </v-row>
59
+
60
+ <v-divider />
61
+
62
+ <v-row dense class="my-1 pr-4">
86
63
  <v-col cols="6" class="py-1"><strong>Category:</strong></v-col>
87
64
  <v-col cols="6" class="py-1 text-right text-capitalize">{{
88
- item.category
65
+ item.categoryInfo
89
66
  }}</v-col>
90
67
  </v-row>
91
68
 
@@ -97,7 +74,7 @@
97
74
  </v-row>
98
75
  <v-divider />
99
76
 
100
- <v-row dense class="my-1">
77
+ <v-row dense class="my-1" v-if="type === 'feedback'">
101
78
  <v-col cols="12" class="py-1"
102
79
  ><strong>Description Feedback:</strong></v-col
103
80
  >
@@ -107,22 +84,45 @@
107
84
  </v-row>
108
85
  <v-divider />
109
86
 
110
- <v-row dense class="my-1">
87
+ <v-row dense class="my-1" v-if="type === 'workOrder'">
111
88
  <v-col cols="12" class="py-1"
112
89
  ><strong>Description Work Order:</strong></v-col
113
90
  >
114
91
  <v-col cols="12" class="py-1">
115
- {{
116
- item.workOrderDescription ||
117
- "No work order description available"
118
- }}
92
+ {{ item.description || "No work order description available" }}
119
93
  </v-col>
120
94
  </v-row>
121
95
  <v-divider />
122
96
 
123
- <v-row dense class="my-1">
124
- <v-col cols="6" class="py-1"><strong>Workorder:</strong></v-col>
125
- <v-col cols="6" class="py-1 text-right">{{ item.workOrder }}</v-col>
97
+ <v-row dense class="my-1" v-if="type === 'feedback'">
98
+ <v-col cols="6" class="py-1"
99
+ ><strong>Work Order No.:</strong></v-col
100
+ >
101
+ <v-col v-if="item.workOrderNo" cols="6" class="py-1 text-right">
102
+ <NuxtLink :to="showWorkOrderInfo(item.workOrderId)">{{
103
+ item.workOrderNo
104
+ }}</NuxtLink>
105
+ </v-col>
106
+ <v-col v-else cols="6" class="py-1 text-right">
107
+ <v-btn
108
+ class="text-none text-capitalize text-blue"
109
+ rounded="pill"
110
+ variant="flat"
111
+ size="small"
112
+ @click="openWorOrderCreateDialog()"
113
+ >
114
+ Create WorkOrder
115
+ </v-btn>
116
+ </v-col>
117
+ </v-row>
118
+
119
+ <v-row dense class="my-1" v-if="type === 'workOrder'">
120
+ <v-col cols="6" class="py-1"
121
+ ><strong>Work Order Number:</strong></v-col
122
+ >
123
+ <v-col v-if="item.workOrderNo" cols="6" class="py-1 text-right"
124
+ >{{ item.workOrderNo }}
125
+ </v-col>
126
126
  </v-row>
127
127
 
128
128
  <v-divider />
@@ -149,22 +149,19 @@
149
149
  }}</v-col>
150
150
  </v-row>
151
151
  </v-col>
152
-
153
- <v-col cols="12">
154
- <v-btn
155
- block
156
- color="primary"
157
- variant="flat"
158
- class="mb-2 text-capitalize"
159
- @click="$emit('edit')"
160
- >
161
- Edit
162
- </v-btn>
163
- </v-col>
164
152
  </v-sheet>
165
153
  </v-row>
166
154
 
167
155
  <v-sheet class="px-4 py-2">
156
+ <v-btn
157
+ block
158
+ color="primary"
159
+ variant="flat"
160
+ class="mb-2 text-capitalize"
161
+ @click="$emit('edit')"
162
+ >
163
+ Edit
164
+ </v-btn>
168
165
  <v-btn
169
166
  v-if="item?.status !== 'Completed'"
170
167
  block
@@ -186,13 +183,34 @@
186
183
  Delete
187
184
  </v-btn>
188
185
  </v-sheet>
186
+
187
+ <WorkOrderCreate
188
+ v-model="showCreateDialog"
189
+ :created-from="'feedback'"
190
+ :work-order="_workOrder"
191
+ @update:work-order="(val: TWorkOrderCreate) => (_workOrder = val)"
192
+ :is-edit-mode="isEditMode"
193
+ :loading="isSubmitting"
194
+ :categories="serviceProviders"
195
+ :theme="theme"
196
+ :errored-images="erroredImages"
197
+ :max-files="5"
198
+ :message-fn="showMessage"
199
+ @close="handleCloseDialog"
200
+ @file-added="handleFileAdded"
201
+ @file-deleted="deleteFile"
202
+ @submit="submitWorkOrder"
203
+ />
204
+
205
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
189
206
  </v-sheet>
190
207
  </template>
191
208
 
192
209
  <script setup lang="ts">
210
+ import { useTheme } from "vuetify";
193
211
  const { getColorStatus, formatDate } = useUtils();
194
212
 
195
- defineProps({
213
+ const props = defineProps({
196
214
  item: {
197
215
  type: Object,
198
216
  default: () => ({}),
@@ -201,15 +219,181 @@ defineProps({
201
219
  type: String,
202
220
  default: "",
203
221
  },
222
+ type: {
223
+ type: String,
224
+ default: "feedback",
225
+ },
204
226
  });
205
227
 
206
- const emit = defineEmits([
207
- "update:provider",
208
- "mark-complete-request",
209
- "delete",
210
- "edit",
211
- ]);
228
+ const emit = defineEmits<{
229
+ (e: "update:provider", serviceProvider: string): void;
230
+ (e: "mark-complete-request"): void;
231
+ (e: "delete"): void;
232
+ (e: "edit"): void;
233
+ (e: "updateWorkOrderId", workOrderId: string): void;
234
+ }>();
212
235
 
213
236
  const { currentUser } = useLocalAuth();
214
237
  const { getNameInitials } = useUtils();
238
+
239
+ const showCreateDialog = ref(false);
240
+ const isEditMode = ref(false);
241
+ const isSubmitting = ref(false);
242
+ const theme = useTheme().name;
243
+ const erroredImages = ref<string[]>([]);
244
+
245
+ const message = ref("");
246
+ const messageColor = ref("");
247
+ const messageSnackbar = ref(false);
248
+
249
+ const route = useRoute();
250
+
251
+ const { getWorkOrders: _getWorkOrders, createWorkOrder } = useWorkOrder();
252
+
253
+ const _workOrder = ref<TWorkOrderCreate>({
254
+ attachments: [],
255
+ category: "",
256
+ subject: "",
257
+ location: "",
258
+ description: "",
259
+ highPriority: false,
260
+ block: "",
261
+ level: "",
262
+ unit: "",
263
+ serviceProvider: "",
264
+ assignee: "",
265
+ organization: "",
266
+ site: "",
267
+ });
268
+
269
+ const serviceProviders = ref<
270
+ Array<{ title: string; value: string; subtitle: string }>
271
+ >([]);
272
+ const { getAll: getAllServiceProvider } = useServiceProvider();
273
+
274
+ const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
275
+ getAllServiceProvider({
276
+ siteId: useRoute().params.site as string,
277
+ })
278
+ );
279
+
280
+ watchEffect(() => {
281
+ if (getAllReq.value) {
282
+ serviceProviders.value = getAllReq.value.items.map((i: any) => ({
283
+ title: i.nature.replace(/_/g, " "),
284
+ value: i.serviceProviderOrgId,
285
+ subtitle: i.name,
286
+ }));
287
+ }
288
+ });
289
+
290
+ function showMessage(msg: string, color: string) {
291
+ message.value = msg;
292
+ messageColor.value = color;
293
+ messageSnackbar.value = true;
294
+ }
295
+
296
+ function handleCloseDialog() {
297
+ resetWorkOrderForm();
298
+ isEditMode.value = false;
299
+ showCreateDialog.value = false;
300
+ }
301
+
302
+ function openWorOrderCreateDialog() {
303
+ let newWorkOrder = {
304
+ ..._workOrder.value,
305
+ attachments:
306
+ props.item.attachments.length > 0 ? props.item.attachments : [],
307
+ subject: props.item.subject ? props.item.subject : "",
308
+ category: props.item.category ? props.item.category : "",
309
+ location: props.item.location ? props.item.location : "",
310
+ };
311
+ _workOrder.value = newWorkOrder;
312
+ showCreateDialog.value = true;
313
+ }
314
+
315
+ function resetWorkOrderForm() {
316
+ _workOrder.value = {
317
+ attachments: [],
318
+ category: "",
319
+ subject: "",
320
+ location: "",
321
+ description: "",
322
+ highPriority: false,
323
+ block: "",
324
+ level: "",
325
+ unit: "",
326
+ serviceProvider: "",
327
+ assignee: "",
328
+ organization: "",
329
+ site: "",
330
+ };
331
+ }
332
+
333
+ const { addFile, deleteFile: _deleteFile } = useFile();
334
+
335
+ const API_DO_STORAGE_ENDPOINT =
336
+ useRuntimeConfig().public.API_DO_STORAGE_ENDPOINT;
337
+
338
+ async function handleFileAdded(file: File) {
339
+ try {
340
+ const res = await addFile(file);
341
+ const uploadedId = res?.id;
342
+ if (uploadedId) {
343
+ const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
344
+ _workOrder.value.attachments = _workOrder.value.attachments ?? [];
345
+ _workOrder.value.attachments.push(url);
346
+ }
347
+ } catch (error) {
348
+ console.error("Error uploading file:", error);
349
+ showMessage("Failed to upload file", "error");
350
+ }
351
+ }
352
+
353
+ async function deleteFile(value: string) {
354
+ try {
355
+ await _deleteFile(value);
356
+ _workOrder.value.attachments = (_workOrder.value.attachments ?? []).filter(
357
+ (file) => file !== value
358
+ );
359
+ } catch (error) {
360
+ console.log(error);
361
+ showMessage("Failed to delete file", "error");
362
+ }
363
+ }
364
+
365
+ async function submitWorkOrder() {
366
+ try {
367
+ isSubmitting.value = true;
368
+
369
+ const payload = {
370
+ ..._workOrder.value,
371
+ organization: route.params.org as string,
372
+ site: route.params.site as string,
373
+ };
374
+
375
+ const res = await createWorkOrder(payload);
376
+
377
+ showMessage(res.message, "success");
378
+ showCreateDialog.value = false;
379
+ resetWorkOrderForm();
380
+ emit("updateWorkOrderId", res.workOrderId);
381
+ // getAllReqRefresh();
382
+ } catch (err) {
383
+ showMessage((err as Error).message, "error");
384
+ } finally {
385
+ isSubmitting.value = false;
386
+ }
387
+ }
388
+
389
+ function showWorkOrderInfo(workOrderId: string) {
390
+ const route = useRoute();
391
+ const org = route.params.org;
392
+ const site = route.params.site;
393
+ const id = workOrderId;
394
+ return {
395
+ name: "org-site-work-orders-id",
396
+ params: { org, site, id },
397
+ };
398
+ }
215
399
  </script>
@@ -6,7 +6,9 @@
6
6
  style="border-right: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0"
7
7
  >
8
8
  <div style="border-bottom: 1px solid #e0e0e0" class="px-4 pt-4 pb-2 mb-2">
9
- <h3 class="text-h6">Chat</h3>
9
+ <h3 class="text-h6">
10
+ {{ type === "workOrder" ? "Work Order" : "Feedback" }} Chat
11
+ </h3>
10
12
  </div>
11
13
 
12
14
  <v-sheet
@@ -94,6 +96,13 @@
94
96
  </template>
95
97
 
96
98
  <script setup lang="ts">
99
+ defineProps({
100
+ type: {
101
+ type: String,
102
+ default: "feedback",
103
+ },
104
+ });
105
+
97
106
  const messages = [
98
107
  {
99
108
  isCurrentUser: true,
@@ -1,8 +1,7 @@
1
1
  <template>
2
2
  <div class="feedback-detail-wrapper">
3
3
  <v-row no-gutters class="fill-height">
4
-
5
- <v-col cols="12" xl="3" lg="4" md="4" class="fill-height">
4
+ <!-- <v-col cols="12" xl="3" lg="4" md="4" class="fill-height">
6
5
  <div class="panel-container border-e">
7
6
  <ChatNavigation
8
7
  :title="'Feedbacks'"
@@ -11,24 +10,24 @@
11
10
  @search="_getFeedbacks"
12
11
  />
13
12
  </div>
14
- </v-col>
15
-
13
+ </v-col> -->
16
14
 
17
- <v-col cols="12" xl="6" lg="5" md="5" class="fill-height">
15
+ <v-col cols="12" xl="7" lg="7" md="7" class="fill-height">
18
16
  <div class="panel-container border-e">
19
- <ChatMessage />
17
+ <ChatMessage :type="'feedback'" />
20
18
  </div>
21
19
  </v-col>
22
20
 
23
-
24
- <v-col cols="12" xl="3" lg="3" md="3" class="fill-height">
21
+ <v-col cols="12" xl="5" lg="5" md="5" class="fill-height">
25
22
  <div class="panel-container">
26
23
  <ChatInformation
27
24
  :item="feedback"
28
25
  :service-providers="_serviceProviders"
26
+ :type="'feedback'"
29
27
  @edit="openEditDialog"
30
28
  @mark-complete-request="showCompleteDialog = true"
31
29
  @delete="showDeleteDialog = true"
30
+ @update-work-order-id="updateWorkOrderId"
32
31
  />
33
32
  </div>
34
33
  </v-col>
@@ -473,6 +472,25 @@ async function submitFeedback(payload: TFeedbackUpdate) {
473
472
  submitting.value = false;
474
473
  }
475
474
  }
475
+
476
+ async function updateWorkOrderId(workOrderId: string) {
477
+ const payload = {
478
+ category: feedback.value.category,
479
+ subject: feedback.value.subject,
480
+ location: feedback.value.location,
481
+ description: feedback.value.description,
482
+ workOrderId: workOrderId,
483
+ };
484
+ const response = await updateFeedback(feedback.value._id, payload);
485
+ const message =
486
+ response?.message || "Feedback updated with Work Order Number successfully";
487
+ showMessage(message, "success");
488
+
489
+ showCreateDialog.value = false;
490
+ await _getFeedbackById();
491
+
492
+ resetFeedbackForm();
493
+ }
476
494
  </script>
477
495
 
478
496
  <style scoped>
@@ -532,12 +550,13 @@ async function submitFeedback(payload: TFeedbackUpdate) {
532
550
  height: calc(100vh - 64px); /* Mobile header height */
533
551
  max-height: calc(100vh - 64px);
534
552
  }
535
-
553
+
536
554
  .border-e {
537
555
  border-right: none;
538
- border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
556
+ border-bottom: 1px solid
557
+ rgba(var(--v-border-color), var(--v-border-opacity));
539
558
  }
540
-
559
+
541
560
  .v-col {
542
561
  height: auto;
543
562
  min-height: 300px;
@@ -21,7 +21,7 @@
21
21
  :items="items"
22
22
  :pages="pages"
23
23
  :page-range="pageRange"
24
- :loading="loading"
24
+ :loading="loading || onNextPrevPageLoading"
25
25
  :height="'calc(100vh - 175px)'"
26
26
  v-model:page="page"
27
27
  :selected="selected"
@@ -31,6 +31,7 @@
31
31
  :viewPage="{ name: 'org-site-feedbacks-id' }"
32
32
  :clickable-rows="true"
33
33
  :length="pages"
34
+ @update:pagination="updatePage"
34
35
  >
35
36
  <template #title>
36
37
  <span class="text-h6 font-weight-regular">Feedbacks</span>
@@ -252,6 +253,7 @@ const {
252
253
  );
253
254
 
254
255
  const loading = computed(() => getAllReqStatus.value === "pending");
256
+ const onNextPrevPageLoading = ref(false);
255
257
 
256
258
  watchEffect(() => {
257
259
  if (getAllFeedbackReq.value) {
@@ -261,6 +263,22 @@ watchEffect(() => {
261
263
  }
262
264
  });
263
265
 
266
+ async function updatePage(pageVal: any) {
267
+ onNextPrevPageLoading.value = true;
268
+ page.value = pageVal;
269
+ const response = await _getFeedbacks({
270
+ page: page.value,
271
+ organization: route.params.org as string,
272
+ site: route.params.site as string,
273
+ });
274
+ if (response) {
275
+ items.value = response.items;
276
+ pages.value = response.pages;
277
+ pageRange.value = response.pageRange;
278
+ onNextPrevPageLoading.value = false;
279
+ }
280
+ }
281
+
264
282
  function onSelectedUpdate(newSelected: string[]) {
265
283
  selected.value = newSelected;
266
284
  }
@@ -347,7 +365,6 @@ function handleCloseDialog() {
347
365
  const _feedbackId = ref<string | null>(null);
348
366
 
349
367
  async function _createFeedback(organization: string, site: string) {
350
-
351
368
  const userId = getUserFromCookie();
352
369
 
353
370
  const createPayload: TFeedbackCreate = {
@@ -361,7 +378,7 @@ async function _createFeedback(organization: string, site: string) {
361
378
  assignee: _feedback.value.assignee || "",
362
379
  organization,
363
380
  site,
364
- ...(userId !== null ? {createdBy: userId } : {}),
381
+ ...(userId !== null ? { createdBy: userId } : {}),
365
382
  };
366
383
 
367
384
  const response = await createFeedback(createPayload);