@7365admin1/layer-common 1.10.5 → 1.10.6

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,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 49fefa8: Update layer-common package for March 9,2026
8
+
3
9
  ## 1.10.5
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <v-dialog :model-value="modelValue" width="480" persistent>
3
+ <v-card width="100%">
4
+ <v-toolbar density="compact" color="black">
5
+ <v-toolbar-title class="text-subtitle-1 font-weight-medium">
6
+ Card Details — {{ card?.cardNo ?? "N/A" }}
7
+ </v-toolbar-title>
8
+ <v-btn icon @click="emit('update:modelValue', false)">
9
+ <v-icon>mdi-close</v-icon>
10
+ </v-btn>
11
+ </v-toolbar>
12
+
13
+ <v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
14
+ <div v-if="pending" class="d-flex justify-center align-center py-8">
15
+ <v-progress-circular indeterminate color="black" />
16
+ </div>
17
+
18
+ <div v-else-if="!details" class="text-center text-grey py-8">
19
+ No details available.
20
+ </div>
21
+
22
+ <div v-else class="d-flex flex-column ga-3">
23
+ <div>
24
+ <div class="text-caption text-grey">Card No</div>
25
+ <div class="text-body-2 font-weight-medium">{{ details.cardNo ?? "N/A" }}</div>
26
+ </div>
27
+
28
+ <div>
29
+ <div class="text-caption text-grey">Type</div>
30
+ <div class="text-body-2 font-weight-medium">{{ details.type ?? "N/A" }}</div>
31
+ </div>
32
+
33
+ <div>
34
+ <div class="text-caption text-grey">Status</div>
35
+ <v-chip
36
+ :color="statusColor(details.status)"
37
+ variant="flat"
38
+ size="x-small"
39
+ class="text-capitalize font-weight-medium mt-1"
40
+ >
41
+ {{ details.status ?? "N/A" }}
42
+ </v-chip>
43
+ </div>
44
+
45
+ <div>
46
+ <div class="text-caption text-grey">Activated</div>
47
+ <div class="text-body-2 font-weight-medium">{{ details.isActivated ? "Yes" : "No" }}</div>
48
+ </div>
49
+
50
+ <div>
51
+ <div class="text-caption text-grey">Site</div>
52
+ <div class="text-body-2 font-weight-medium">{{ details.site?.name ?? "N/A" }}</div>
53
+ </div>
54
+
55
+ <div>
56
+ <div class="text-caption text-grey">User</div>
57
+ <div class="text-body-2 font-weight-medium">{{ details.user?.name ?? "N/A" }} ({{ details.user?.email ?? "N/A" }})</div>
58
+ </div>
59
+
60
+ <div>
61
+ <div class="text-caption text-grey">Created At</div>
62
+ <div class="text-body-2 font-weight-medium">{{ formatDate(details.createdAt) }}</div>
63
+ </div>
64
+
65
+ <div>
66
+ <div class="text-caption text-grey">Updated At</div>
67
+ <div class="text-body-2 font-weight-medium">{{ formatDate(details.updatedAt) }}</div>
68
+ </div>
69
+
70
+ <div v-if="details.remarks">
71
+ <div class="text-caption text-grey">Remarks</div>
72
+ <div class="text-body-2 font-weight-medium">{{ details.remarks }}</div>
73
+ </div>
74
+ </div>
75
+ </v-card-text>
76
+ </v-card>
77
+ </v-dialog>
78
+ </template>
79
+
80
+ <script setup lang="ts">
81
+ const props = defineProps({
82
+ modelValue: {
83
+ type: Boolean,
84
+ default: false,
85
+ },
86
+ card: {
87
+ type: Object as PropType<Record<string, any> | null>,
88
+ default: null,
89
+ },
90
+ siteId: {
91
+ type: String,
92
+ default: "",
93
+ },
94
+ });
95
+
96
+ const emit = defineEmits<{
97
+ "update:modelValue": [value: boolean];
98
+ }>();
99
+
100
+ const { getCardDetails } = useAccessManagement();
101
+
102
+ const details = ref<Record<string, any> | null>(null);
103
+ const pending = ref(false);
104
+
105
+ watch(
106
+ () => props.modelValue,
107
+ async (val) => {
108
+ if (val && props.card?._id && props.siteId) {
109
+ pending.value = true;
110
+ try {
111
+ const res = await getCardDetails({ siteId: props.siteId, cardId: props.card._id });
112
+ details.value = res?.data ?? null;
113
+ } finally {
114
+ pending.value = false;
115
+ }
116
+ } else {
117
+ details.value = null;
118
+ }
119
+ }
120
+ );
121
+
122
+ function statusColor(status: string) {
123
+ const map: Record<string, string> = {
124
+ active: "success",
125
+ assigned: "primary",
126
+ replaced: "orange",
127
+ deleted: "error",
128
+ available: "grey",
129
+ };
130
+ return map[status] ?? "grey";
131
+ }
132
+
133
+ function formatDate(date: string) {
134
+ if (!date) return "N/A";
135
+ return new Intl.DateTimeFormat("en-GB", {
136
+ day: "2-digit",
137
+ month: "short",
138
+ year: "numeric",
139
+ hour: "2-digit",
140
+ minute: "2-digit",
141
+ hour12: true,
142
+ }).format(new Date(date));
143
+ }
144
+ </script>
@@ -1,7 +1,8 @@
1
1
  <template>
2
- <AccessCardHistoryDialog
2
+ <AccessCardDetailsDialog
3
3
  v-model="historyDialog"
4
4
  :card="selectedCardInUnit"
5
+ :site-id="siteId"
5
6
  />
6
7
 
7
8
  <v-dialog :model-value="modelValue" width="450" persistent>
@@ -221,7 +222,7 @@
221
222
  </v-list-item>
222
223
  <v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
223
224
  <v-list-item-title class="text-subtitle-2 cursor-pointer">
224
- Card History
225
+ Card Details
225
226
  </v-list-item-title>
226
227
  </v-list-item>
227
228
  <v-list-item
@@ -277,6 +278,10 @@ const props = defineProps({
277
278
  type: Boolean,
278
279
  default: false,
279
280
  },
281
+ siteId: {
282
+ type: String,
283
+ default: "",
284
+ },
280
285
  });
281
286
 
282
287
  const emit = defineEmits<{
@@ -112,6 +112,7 @@
112
112
  :can-delete-access-card="canDeleteAccessCard"
113
113
  :is-selected-card-assigned-physical="isSelectedCardAssignedPhysical"
114
114
  :is-selected-card-physical="isSelectedCardPhysical"
115
+ :site-id="siteId"
115
116
  @replace="openReplaceDialog()"
116
117
  @delete="openDeleteDialog()"
117
118
  />
@@ -354,8 +355,11 @@ async function handleDeleteCard(remarks: string) {
354
355
  selectedCardId.value = null;
355
356
  confirmDialog.value = false;
356
357
  previewDialog.value = false;
358
+ showMessage("Access card deleted successfully!", "success");
357
359
  } catch (error: any) {
358
- deleteError.value = error?.response?._data?.message || "Failed to delete card";
360
+ const msg = error?.response?._data?.message || "Failed to delete card";
361
+ deleteError.value = msg;
362
+ showMessage(msg, "error");
359
363
  } finally {
360
364
  deleteLoading.value = false;
361
365
  }
@@ -13,6 +13,7 @@
13
13
  style="max-width: 95px"
14
14
  :rules="[...props.rules]"
15
15
  :readonly="props.readOnly"
16
+ :disabled="props.disabled"
16
17
  @update:model-value="handleUpdateCountry"
17
18
  >
18
19
  <template v-slot:item="{ props: itemProps, item }">
@@ -40,6 +41,7 @@
40
41
  :prefix="phonePrefix || ''"
41
42
  persistent-placeholder
42
43
  :density="density"
44
+ :disabled="props.disabled"
43
45
  :placeholder="placeholder || currentMask"
44
46
  />
45
47
  </v-col>
@@ -64,6 +66,7 @@ const props = defineProps({
64
66
  hideDetails: { type: Boolean, default: false },
65
67
  loading: { type: Boolean, default: false },
66
68
  readOnly: { type: Boolean, default: false },
69
+ disabled: { type: Boolean, default: false },
67
70
  })
68
71
 
69
72
  const emit = defineEmits(['update:modelValue'])
@@ -5,7 +5,7 @@
5
5
  </span>
6
6
 
7
7
  <v-chip
8
- v-if="extraCount > 0"
8
+ v-if="extraCount > 0 && !showAll"
9
9
  density="comfortable"
10
10
  size="small"
11
11
  >
@@ -23,12 +23,20 @@ const props = defineProps({
23
23
  defaultValue: {
24
24
  type: String,
25
25
  default: ""
26
+ },
27
+ showAll: {
28
+ type: Boolean,
29
+ default: false
26
30
  }
27
31
  })
28
32
 
29
33
  const formatted = computed(() => {
30
34
  if (!props.plateNumbers?.length) return props.defaultValue || ""
31
35
 
36
+ if (props.showAll) {
37
+ return props.plateNumbers.map((v: any) => v?.plateNumber || "").join(", ")
38
+ }
39
+
32
40
  const firstTwo = props.plateNumbers
33
41
  .slice(0, 2)
34
42
  .map((v: any) => v?.plateNumber || "")
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
- <!-- Top Actions -->
4
3
  <v-col cols="12" class="mb-2" v-if="canCreate || $slots.actions">
5
4
  <v-row no-gutters>
6
5
  <slot name="actions">
@@ -18,7 +17,6 @@
18
17
  </v-row>
19
18
  </v-col>
20
19
 
21
- <!-- List Card -->
22
20
  <v-col cols="12">
23
21
  <v-card
24
22
  width="100%"
@@ -27,7 +25,6 @@
27
25
  rounded="lg"
28
26
  :loading="loading"
29
27
  >
30
- <!-- Toolbar -->
31
28
  <v-toolbar
32
29
  density="compact"
33
30
  color="grey-lighten-4"
@@ -40,7 +37,10 @@
40
37
  <slot name="prepend-additional" />
41
38
  </template>
42
39
 
43
- <v-toolbar-title v-if="title" class="text-subtitle-1 font-weight-medium">
40
+ <v-toolbar-title
41
+ v-if="title"
42
+ class="text-subtitle-1 font-weight-medium"
43
+ >
44
44
  {{ title }}
45
45
  </v-toolbar-title>
46
46
 
@@ -64,126 +64,224 @@
64
64
 
65
65
  <v-divider />
66
66
 
67
- <!-- List Items -->
68
- <v-list
69
- :style="`max-height: calc(100vh - (${offset}px)); overflow-y: auto;`"
67
+ <v-sheet
68
+ :max-height="`calc(100vh - (${offset}px))`"
69
+ class="overflow-y-auto"
70
70
  >
71
- <v-list-item
71
+ <v-row
72
72
  v-if="groupedItems.length === 0"
73
- class="py-6 text-center text-medium-emphasis"
73
+ no-gutters
74
+ justify="center"
75
+ class="py-10"
74
76
  >
75
- {{ noDataText }}
76
- </v-list-item>
77
+ <v-col
78
+ cols="auto"
79
+ class="text-center text-medium-emphasis text-body-2"
80
+ >
81
+ {{ noDataText }}
82
+ </v-col>
83
+ </v-row>
77
84
 
78
85
  <template
79
86
  v-for="(group, groupIndex) in groupedItems"
80
87
  :key="`group-${groupIndex}`"
81
88
  >
82
- <div
83
- v-if="group.set !== undefined"
84
- class="d-flex align-center justify-space-between px-4 py-1 text-caption text-medium-emphasis"
85
- style="min-height: 32px"
86
- >
87
- <span>Set {{ group.set }}</span>
88
- <div class="d-flex align-center ga-2">
89
- <span v-if="group.completedByName">
90
- Completed by: {{ group.completedByName }}
91
- </span>
92
- <v-btn
93
- v-if="group.attachments && group.attachments.length > 0"
94
- size="x-small"
95
- variant="tonal"
96
- color="primary"
97
- class="text-none"
98
- prepend-icon="mdi-paperclip"
99
- @click.stop="
100
- openAttachmentDialog(group.set, group.attachments)
101
- "
102
- >
103
- View Attachment ({{ group.attachments.length }})
104
- </v-btn>
105
- </div>
106
- </div>
107
-
108
- <v-list-item
89
+ <v-sheet color="grey-lighten-4" border="b t">
90
+ <v-row no-gutters align="center" class="px-4 py-2">
91
+ <v-col cols="auto" class="d-flex align-center ga-2">
92
+ <span
93
+ class="text-caption font-weight-bold text-medium-emphasis text-uppercase"
94
+ >
95
+ Set {{ group.set }}
96
+ </span>
97
+ <v-chip
98
+ v-if="group.completedByName && isGroupComplete(group)"
99
+ size="x-small"
100
+ color="success"
101
+ variant="tonal"
102
+ prepend-icon="mdi-check-circle-outline"
103
+ class="text-none"
104
+ >
105
+ Completed · {{ group.completedByName }}
106
+ </v-chip>
107
+ <v-chip
108
+ v-else-if="
109
+ group.completedByName && isGroupInProgress(group)
110
+ "
111
+ size="x-small"
112
+ color="warning"
113
+ variant="tonal"
114
+ prepend-icon="mdi-progress-clock"
115
+ class="text-none"
116
+ >
117
+ Ongoing · {{ group.completedByName }}
118
+ </v-chip>
119
+ </v-col>
120
+ <v-spacer />
121
+ <v-col cols="auto">
122
+ <v-btn
123
+ v-if="group.attachments && group.attachments.length > 0"
124
+ size="x-small"
125
+ variant="tonal"
126
+ color="primary"
127
+ class="text-none"
128
+ prepend-icon="mdi-paperclip"
129
+ @click.stop="
130
+ openAttachmentDialog(group.set, group.attachments)
131
+ "
132
+ >
133
+ {{ group.attachments.length }} attachment{{
134
+ group.attachments.length > 1 ? "s" : ""
135
+ }}
136
+ </v-btn>
137
+ </v-col>
138
+ </v-row>
139
+ </v-sheet>
140
+
141
+ <v-sheet
109
142
  v-for="item in group.items"
110
143
  :key="item[itemValue]"
111
- class="py-3"
112
- :class="
113
- isItemSelected(item, group.set)
114
- ? ['bg-grey-lighten-4', 'rounded']
115
- : []
144
+ :color="
145
+ isItemSelected(item, group.set) ? 'grey-lighten-4' : 'white'
116
146
  "
147
+ border="b"
117
148
  >
118
- <slot name="list-item" :item="item">
119
- <v-list-item-title v-if="headers[0]">
120
- {{ getItemValue(item, headers[0].value) }}
121
- </v-list-item-title>
122
-
123
- <v-list-item-subtitle
124
- v-if="headers[1]"
125
- class="mb-1 text-high-emphasis opacity-100"
126
- >
127
- {{ getItemValue(item, headers[1].value) || "N/A" }}
128
- </v-list-item-subtitle>
129
-
130
- <v-list-item-subtitle
131
- v-if="headers[2]"
132
- class="text-high-emphasis"
133
- >
134
- {{ getItemValue(item, headers[2].value) || "N/A" }}
135
- </v-list-item-subtitle>
136
- </slot>
137
-
138
- <template v-slot:append>
139
- <slot
140
- name="list-item-append"
141
- :item="item"
142
- :isSelected="isItemSelected(item, group.set)"
143
- >
144
- <v-list-item-action class="d-flex flex-row ga-2">
145
- <template v-if="canManageScheduleTasks">
146
- <v-btn
147
- icon="mdi-close"
148
- size="small"
149
- :variant="
150
- activeActions[getKey(item, group.set)] === 'reject'
151
- ? 'flat'
152
- : 'text'
153
- "
154
- color="error"
155
- @click.stop="
156
- handleActionClick(item, group.set, 'reject')
157
- "
158
- />
159
-
160
- <v-btn
161
- icon="mdi-check"
162
- size="small"
163
- :variant="
164
- activeActions[getKey(item, group.set)] === 'approve'
165
- ? 'flat'
166
- : 'text'
167
- "
168
- color="success"
169
- @click.stop="
170
- handleActionClick(item, group.set, 'approve')
149
+ <v-row no-gutters align="center" class="px-4 py-2">
150
+ <v-col cols="auto" class="mr-3">
151
+ <v-icon
152
+ size="20"
153
+ :color="
154
+ activeActions[getKey(item, group.set)] === 'approve'
155
+ ? 'success'
156
+ : activeActions[getKey(item, group.set)] === 'reject'
157
+ ? 'error'
158
+ : 'grey-lighten-2'
159
+ "
160
+ >
161
+ {{
162
+ activeActions[getKey(item, group.set)] === "approve"
163
+ ? "mdi-check-circle"
164
+ : activeActions[getKey(item, group.set)] === "reject"
165
+ ? "mdi-close-circle"
166
+ : "mdi-circle-outline"
167
+ }}
168
+ </v-icon>
169
+ </v-col>
170
+ <v-col>
171
+ <slot name="list-item" :item="item">
172
+ <v-row no-gutters align-center>
173
+ <v-col cols="12">
174
+ <span
175
+ class="text-body-2 font-weight-medium"
176
+ :class="
177
+ activeActions[getKey(item, group.set)] === 'approve'
178
+ ? 'text-decoration-line-through text-medium-emphasis'
179
+ : ''
180
+ "
181
+ >
182
+ {{ getItemValue(item, headers[0].value) }}
183
+ </span>
184
+ </v-col>
185
+ <v-col
186
+ v-if="
187
+ item.timestamp ||
188
+ (headers[1] &&
189
+ getItemValue(item, headers[1].value)) ||
190
+ (headers[2] && getItemValue(item, headers[2].value))
171
191
  "
172
- />
173
- </template>
174
- </v-list-item-action>
175
- </slot>
176
- </template>
177
- </v-list-item>
192
+ cols="12"
193
+ >
194
+ <v-row
195
+ no-gutters
196
+ align="center"
197
+ class="ga-3 flex-wrap mt-1"
198
+ >
199
+ <v-col
200
+ v-if="item.timestamp"
201
+ cols="auto"
202
+ class="d-flex align-center ga-1 text-caption text-medium-emphasis pa-0"
203
+ >
204
+ <v-icon size="11">mdi-clock-outline</v-icon>
205
+ {{ formatTimestamp(item.timestamp) }}
206
+ </v-col>
207
+ <v-col
208
+ v-if="
209
+ headers[1] && getItemValue(item, headers[1].value)
210
+ "
211
+ cols="auto"
212
+ class="text-caption text-medium-emphasis pa-0"
213
+ >
214
+ {{ getItemValue(item, headers[1].value) }}
215
+ </v-col>
216
+ <v-col
217
+ v-if="
218
+ headers[2] && getItemValue(item, headers[2].value)
219
+ "
220
+ cols="auto"
221
+ class="text-caption text-medium-emphasis pa-0"
222
+ >
223
+ {{ getItemValue(item, headers[2].value) }}
224
+ </v-col>
225
+ </v-row>
226
+ </v-col>
227
+ </v-row>
228
+ </slot>
229
+ </v-col>
230
+
231
+ <v-col cols="auto">
232
+ <slot
233
+ name="list-item-append"
234
+ :item="item"
235
+ :isSelected="isItemSelected(item, group.set)"
236
+ >
237
+ <v-row
238
+ v-if="canManageScheduleTasks"
239
+ no-gutters
240
+ align="center"
241
+ >
242
+ <v-col cols="auto">
243
+ <v-btn
244
+ icon="mdi-close"
245
+ size="small"
246
+ :variant="
247
+ activeActions[getKey(item, group.set)] === 'reject'
248
+ ? 'flat'
249
+ : 'text'
250
+ "
251
+ color="error"
252
+ @click.stop="
253
+ handleActionClick(item, group.set, 'reject')
254
+ "
255
+ />
256
+ </v-col>
257
+ <v-col cols="auto">
258
+ <v-btn
259
+ icon="mdi-check"
260
+ size="small"
261
+ :variant="
262
+ activeActions[getKey(item, group.set)] === 'approve'
263
+ ? 'flat'
264
+ : 'text'
265
+ "
266
+ color="success"
267
+ @click.stop="
268
+ handleActionClick(item, group.set, 'approve')
269
+ "
270
+ />
271
+ </v-col>
272
+ </v-row>
273
+ </slot>
274
+ </v-col>
275
+ </v-row>
276
+ </v-sheet>
178
277
  </template>
179
- </v-list>
278
+ </v-sheet>
180
279
 
181
280
  <slot name="footer" />
182
281
  </v-card>
183
282
  </v-col>
184
283
  </v-row>
185
284
 
186
- <!-- Attachment Preview Dialog -->
187
285
  <v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
188
286
  <v-card>
189
287
  <v-card-title class="d-flex align-center pa-4">
@@ -248,7 +346,6 @@
248
346
  </v-card>
249
347
  </v-dialog>
250
348
 
251
- <!-- Full Image Lightbox -->
252
349
  <v-dialog v-model="showLightbox" max-width="900">
253
350
  <v-card>
254
351
  <v-card-actions class="pa-2 justify-end">
@@ -431,6 +528,34 @@ const allItemsApproved = computed(() => {
431
528
  );
432
529
  });
433
530
 
531
+ function formatTimestamp(ts: string): string {
532
+ if (!ts) return "";
533
+ const date = new Date(ts);
534
+ return date.toLocaleString("en-SG", {
535
+ day: "2-digit",
536
+ month: "short",
537
+ year: "numeric",
538
+ hour: "2-digit",
539
+ minute: "2-digit",
540
+ hour12: true,
541
+ });
542
+ }
543
+
544
+ function isGroupComplete(group: { items: any[] }): boolean {
545
+ return (
546
+ group.items.length > 0 &&
547
+ group.items.every((item: any) => item.approve === true)
548
+ );
549
+ }
550
+
551
+ function isGroupInProgress(group: { items: any[] }): boolean {
552
+ return (
553
+ group.items.some(
554
+ (item: any) => item.approve === true || item.reject === true
555
+ ) && !isGroupComplete(group)
556
+ );
557
+ }
558
+
434
559
  function isSetFullyApproved(setNumber: number): boolean {
435
560
  const group = groupedItems.value.find((g) => g.set === setNumber);
436
561
  if (!group) return false;
@@ -442,7 +567,7 @@ function isSetFullyApproved(setNumber: number): boolean {
442
567
  }
443
568
 
444
569
  function getNewApprovedItemsForSet(
445
- setNumber: number,
570
+ setNumber: number
446
571
  ): Array<{ key: string; item: any; action: "approve" }> {
447
572
  const group = groupedItems.value.find((g) => g.set === setNumber);
448
573
  if (!group) return [];
@@ -472,14 +597,14 @@ watch(
472
597
  internalPage.value = val;
473
598
 
474
599
  itemOrderMap.clear();
475
- },
600
+ }
476
601
  );
477
602
 
478
603
  watch(
479
604
  () => props.selected,
480
605
  (val) => {
481
606
  selected.value = val;
482
- },
607
+ }
483
608
  );
484
609
 
485
610
  watch(selected, (val) => {
@@ -492,7 +617,7 @@ watch(
492
617
  if (!items || !Array.isArray(items)) return;
493
618
 
494
619
  Object.keys(persistedActions).forEach(
495
- (key) => delete persistedActions[key],
620
+ (key) => delete persistedActions[key]
496
621
  );
497
622
 
498
623
  items.forEach((group: any) => {
@@ -511,7 +636,7 @@ watch(
511
636
  });
512
637
  });
513
638
  },
514
- { immediate: true },
639
+ { immediate: true }
515
640
  );
516
641
 
517
642
  function getKey(item: any, set?: number): string {
@@ -530,7 +655,7 @@ function isItemSelected(item: any, set?: number): boolean {
530
655
 
531
656
  if (typeof selected.value[0] === "object" && "unit" in selected.value[0]) {
532
657
  return selected.value.some(
533
- (s: any) => s.unit === item[props.itemValue] && s.set === set,
658
+ (s: any) => s.unit === item[props.itemValue] && s.set === set
534
659
  );
535
660
  }
536
661
 
@@ -540,7 +665,7 @@ function isItemSelected(item: any, set?: number): boolean {
540
665
  function handleActionClick(
541
666
  item: any,
542
667
  set: number | undefined,
543
- action: "approve" | "reject",
668
+ action: "approve" | "reject"
544
669
  ): void {
545
670
  const key = getKey(item, set);
546
671
 
@@ -612,6 +737,6 @@ watch(
612
737
  () => {
613
738
  completedSets.value.clear();
614
739
  },
615
- { deep: true },
740
+ { deep: true }
616
741
  );
617
742
  </script>
@@ -32,47 +32,53 @@
32
32
  </v-combobox>
33
33
  </v-col>
34
34
 
35
+ <v-col v-if="shouldShowField('nric')" cols="12">
36
+ <InputLabel class="text-capitalize" title="NRIC" required />
37
+ <InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]" />
38
+ </v-col>
39
+
35
40
  <v-col v-if="shouldShowField('name')" cols="12">
36
41
  <v-row>
37
42
  <v-col cols="12">
38
43
  <InputLabel class="text-capitalize" title="Full Name" required />
39
- <v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" />
44
+ <v-text-field v-model.trim="vehicle.name" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
40
45
  </v-col>
41
46
  </v-row>
42
47
  </v-col>
43
48
 
44
- <v-col v-if="shouldShowField('nric')" cols="12">
45
- <InputLabel class="text-capitalize" title="NRIC" required />
46
- <InputNRICNumber v-model="vehicle.nric" density="comfortable" :rules="[requiredRule]" />
47
- </v-col>
49
+
48
50
 
49
51
  <v-col v-if="shouldShowField('phone')" cols="12">
50
52
  <InputLabel class="text-capitalize" title="Phone Number" required />
51
- <InputPhoneNumberV2 v-model="vehicle.phoneNumber" density="comfortable" :rules="[requiredRule]" />
53
+ <InputPhoneNumberV2 v-model="vehicle.phoneNumber" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
52
54
  </v-col>
53
55
 
54
56
  <v-col v-if="shouldShowField('block')" cols="12">
55
57
  <InputLabel class="text-capitalize" title="Block" required />
56
58
  <v-select v-model="vehicle.block" :items="blocksArray" item-value="value" item-title="title"
57
- @update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" />
59
+ @update:model-value="handleChangeBlock" density="comfortable" :rules="[requiredRule]" :disabled="disablePrefilledInputs" />
58
60
  </v-col>
59
61
 
60
62
  <v-col v-if="shouldShowField('level')" cols="12">
61
63
  <InputLabel class="text-capitalize" title="Level" required />
62
- <v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block"
64
+ <v-select v-model="vehicle.level" :items="levelsArray" density="comfortable" :disabled="!vehicle.block || disablePrefilledInputs"
63
65
  @update:model-value="handleChangeLevel" :rules="[requiredRule]" />
64
66
  </v-col>
65
67
 
66
68
  <v-col v-if="shouldShowField('unit')" cols="12">
67
69
  <InputLabel class="text-capitalize" title="Unit" required />
68
- <v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level"
70
+ <v-select v-model="vehicle.unit" :items="unitsArray" density="comfortable" :disabled="!vehicle.level || disablePrefilledInputs"
69
71
  :rules="[requiredRule]" />
70
72
  </v-col>
71
73
 
72
74
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
73
- <InputLabel class="text-capitalize" title="Vehicle Number" required />
75
+ <InputLabel class="text-capitalize" title="Vehicle Numbers" required />
74
76
  <!-- <v-text-field v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" /> -->
75
- <InputVehicleNumber v-model="vehicle.plateNumber" density="comfortable" :rules="[requiredRule]" />
77
+ <template v-for="plate in vehicle.plates" :key="plate.plateNumber">
78
+ <v-text-field v-model="plate.plateNumber" density="comfortable" :rules="[requiredRule]" class="mb-2" read-only />
79
+ </template>
80
+
81
+ <InputVehicleNumber v-model="newPlateNumber" density="comfortable" :rules="[requiredRule]" />
76
82
  </v-col>
77
83
 
78
84
  <v-col v-if="shouldShowField('remarks')" cols="12">
@@ -149,6 +155,44 @@
149
155
  </v-col>
150
156
  </v-row>
151
157
  </v-toolbar>
158
+
159
+ <v-dialog v-model="showMatchingPeopleDialog" max-width="700">
160
+ <v-card>
161
+ <v-toolbar>
162
+ <v-toolbar-title>
163
+ Existing Records Found
164
+ </v-toolbar-title>
165
+ </v-toolbar>
166
+
167
+ <v-card-text>
168
+
169
+ <v-list lines="three">
170
+ <v-list-item v-for="v in matchingPeople" :key="v._id" class="cursor-pointer">
171
+ <v-list-item-title>
172
+ {{ v.name }}
173
+ </v-list-item-title>
174
+
175
+ <v-list-item-subtitle>
176
+ Block {{ v.block }} - {{ v.level }} - {{ v.unitName }}
177
+ </v-list-item-subtitle>
178
+
179
+ <div class="mt-1">
180
+ <v-chip v-for="p in v.plates" :key="p?.plateNumber" size="small" class="mr-1">
181
+ {{ p?.plateNumber }}
182
+ </v-chip>
183
+ </div>
184
+
185
+ <template #append>
186
+ <v-btn variant="flat" color="primary" @click="selectNRICRecord(v)">Select</v-btn>
187
+ </template>
188
+
189
+ </v-list-item>
190
+ </v-list>
191
+
192
+ </v-card-text>
193
+ </v-card>
194
+ </v-dialog>
195
+
152
196
  </v-card>
153
197
  </template>
154
198
 
@@ -182,12 +226,13 @@ const prop = defineProps({
182
226
  const { requiredRule, formatDateISO8601, debounce } = useUtils();
183
227
  const { addVehicle, getCustomSeasonPassTypes, updateVehicle, getVehicleByNRIC } = useVehicle();
184
228
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
229
+ const { findPersonByNRICMultipleResult } = usePeople();
185
230
 
186
231
  const emit = defineEmits(['back', 'select', 'done', 'error', 'close', 'close:all']);
187
232
 
188
233
 
189
234
  const vehicle = reactive<Partial<TVehicle>>({
190
- plateNumber: '',
235
+ plates: [],
191
236
  type: prop.type,
192
237
  category: "resident",
193
238
  name: '',
@@ -205,15 +250,16 @@ const vehicle = reactive<Partial<TVehicle>>({
205
250
  _id: '',
206
251
  });
207
252
 
208
-
253
+ const newPlateNumber = ref('');
254
+ const disablePrefilledInputs = ref(true);
209
255
 
210
256
  const blocksArray = ref<TDefaultOptionObj[]>([]);
211
257
  const levelsArray = ref<TDefaultOptionObj[]>([]);
212
258
  const unitsArray = ref<TDefaultOptionObj[]>([]);
213
259
  const seasonPassTypeArray = ref<{ title: string, value: string }[]>([]);
214
260
 
215
- const matchingVehicles = ref<TVehicle[]>([]);
216
- const showVehicleMatchDialog = ref(false);
261
+ const matchingPeople = ref<Partial<TPeople>[]>([]);
262
+ const showMatchingPeopleDialog = ref(false);
217
263
  const checkingNRIC = ref(false);
218
264
 
219
265
  const defaultSeasonPassTypeArray = computed(() => {
@@ -541,23 +587,42 @@ watch([() => vehicle.end, () => vehicle.start], () => {
541
587
  });
542
588
 
543
589
 
590
+ function selectNRICRecord(record: TPeople) {
591
+
592
+ vehicle.name = record.name;
593
+ vehicle.phoneNumber = record.contact;
594
+ vehicle.block = Number(record.block);
595
+ vehicle.level = record.level;
596
+ vehicle.unit = record.unit;
597
+
598
+ vehicle.plates = record.plates || [];
599
+
600
+ disablePrefilledInputs.value = true;
601
+ showMatchingPeopleDialog.value = false;
602
+
603
+ refreshLevelsData();
604
+ refreshUnitsData();
605
+ }
606
+
607
+
544
608
  async function checkNRIC() {
545
609
  if (!vehicle.nric || vehicle.nric.length < 5) return;
546
610
 
547
611
  checkingNRIC.value = true;
612
+
548
613
  try {
549
- const res = await getVehicleByNRIC(vehicle.nric, prop.site);
550
- if (res && res.vehicles && res.vehicles.length > 0) {
551
- matchingVehicles.value = res.vehicles;
552
- showVehicleMatchDialog.value = true;
614
+ const res = await findPersonByNRICMultipleResult(vehicle.nric, prop.site) as { items: TPeople[] } | null;
615
+
616
+ if (res?.items && res.items.length > 0) {
617
+ matchingPeople.value = res.items || []
618
+ showMatchingPeopleDialog.value = true;
553
619
  } else {
554
- showVehicleMatchDialog.value = false;
555
- matchingVehicles.value = [];
620
+ matchingPeople.value = [];
621
+ showMatchingPeopleDialog.value = false;
556
622
  }
623
+
557
624
  } catch (error) {
558
- console.error('Error checking NRIC:', error);
559
- showVehicleMatchDialog.value = false;
560
- matchingVehicles.value = [];
625
+ console.error("NRIC search failed:", error);
561
626
  } finally {
562
627
  checkingNRIC.value = false;
563
628
  }
@@ -568,26 +633,24 @@ const debounceedCheckNRIC = debounce(checkNRIC, 500);
568
633
  watch(
569
634
  () => vehicle.nric,
570
635
  async (newNRIC) => {
571
- if (!newNRIC || newNRIC.length < 5) return;
636
+ resetVehicleDetails();
637
+ if (!newNRIC || newNRIC.length < 3) return;
572
638
 
573
639
  debounceedCheckNRIC();
574
640
  }
575
641
  );
576
642
 
577
- watch(
578
- () => vehicle.plateNumber,
579
- (newData) => {
580
-
581
- const plateNumberExistsInNRICMatches = matchingVehicles.value.some(v => v.plateNumber === newData);
582
- if (plateNumberExistsInNRICMatches) {
583
- errorMessage.value = 'The vehicle number you entered is already associated with the NRIC provided. Please double check.';
584
- showVehicleMatchDialog.value = true;
585
- } else {
586
- errorMessage.value = '';
587
- showVehicleMatchDialog.value = false;
588
- }
643
+
644
+ const resetVehicleDetails = () => {
645
+ vehicle.name = '';
646
+ vehicle.phoneNumber = '';
647
+ vehicle.block = '';
648
+ vehicle.level = '';
649
+ vehicle.unit = '';
650
+ vehicle.plates = [];
651
+ disablePrefilledInputs.value = false;
589
652
  }
590
- );
653
+
591
654
 
592
655
 
593
656
 
@@ -9,13 +9,13 @@
9
9
  <v-row no-gutters class="px-5 py-1 d-flex align-center justify-end ga-3">
10
10
  <v-text-field v-model="searchInput" density="compact" placeholder="Search" clearable max-width="300"
11
11
  append-inner-icon="mdi-magnify" hide-details />
12
- <v-select v-model="vehicleType" density="compact" item-title="label" item-value="value"
12
+ <v-select v-model="vehicleTypeFilter" density="compact" item-title="label" item-value="value"
13
13
  placeholder="Filter by Type" clearable max-width="200" hide-details :items="typeOptions" />
14
14
  </v-row>
15
15
  </template>
16
16
 
17
17
  <template #item.block="{ value }">
18
- {{ value ? `blk ${value}` : "" }}
18
+ {{ value ? `Blk ${value}` : "" }}
19
19
  </template>
20
20
  <template #item.status="{ value }">
21
21
  <v-chip :color="formatVehicleStatus(value).color" size="x-small" dark>
@@ -25,7 +25,7 @@
25
25
 
26
26
  </template>
27
27
 
28
- <template #item.plateNumbers="{ value, item }">
28
+ <template #item.plates="{ value, item }">
29
29
  <PlateNumberDisplay :plate-numbers="value" :default-value="item.plateNumber" />
30
30
  </template>
31
31
  </TableMain>
@@ -55,9 +55,9 @@
55
55
  <v-row no-gutters class="ga-1 mb-5">
56
56
 
57
57
  <template v-for="(label, key) in formattedFields" :key="key">
58
- <v-col v-if="key === 'plateNumbers'" class="d-flex ga-2">
58
+ <v-col v-if="key === 'plates'" class="d-flex ga-2">
59
59
  <span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong></span>
60
- <PlateNumberDisplay :plate-numbers="selectedVehicleObject[key]" :default-value="selectedVehicleObject.plateNumber" />
60
+ <PlateNumberDisplay :plate-numbers="selectedVehicleObject[key]" show-all :default-value="selectedVehicleObject.plateNumber" />
61
61
  </v-col>
62
62
 
63
63
  <v-col v-else-if="selectedVehicleObject[key]" cols="12">
@@ -99,10 +99,11 @@ const props = defineProps({
99
99
  const headers = [
100
100
  { title: "Name", value: "name" },
101
101
  // { title: "Building", value: "buildingName" },
102
- { title: "Vehicle Numbers", value: "plateNumbers" },
102
+ { title: "Vehicle Numbers", value: "plates" },
103
103
  { title: "NRIC", value: "nric" },
104
104
  { title: "Block", value: "block" },
105
105
  { title: "Floor", value: "level" },
106
+ { title: "Unit", value: "unit" },
106
107
  { title: "Category", value: "category" },
107
108
  { title: "Type", value: "type" },
108
109
  { title: "Status", value: "status" },
@@ -123,6 +124,7 @@ const searchInput = ref("")
123
124
  const loading = ref(false);
124
125
  const selectedVehicleId = ref<string | null>(null)
125
126
  const vehicleType = ref<TVehicleType | null>(null);
127
+ const vehicleTypeFilter = ref<TVehicleType | null>(null);
126
128
 
127
129
  const message = ref("");
128
130
  const messageColor = ref("");
@@ -151,7 +153,7 @@ const typeOptions = [
151
153
 
152
154
  const formattedFields: Partial<Record<keyof TVehicle, string>> = {
153
155
  name: "Name",
154
- plateNumbers: "Vehicle Numbers",
156
+ plates: "Vehicle Numbers",
155
157
  phoneNumber: "Phone Number",
156
158
  nric: "NRIC",
157
159
  block: "Block",
@@ -201,7 +203,7 @@ const { data: getVehiclesReq, refresh: getVehiclesRefresh, pending: getVehiclesP
201
203
  getVehicles({
202
204
  page: page.value,
203
205
  search: searchInput.value,
204
- type: vehicleType.value ?? "",
206
+ type: vehicleTypeFilter.value ?? "",
205
207
  }),
206
208
  {
207
209
  watch: [page],
@@ -289,7 +291,7 @@ async function submitDelete() {
289
291
  }
290
292
 
291
293
  const debouncedSearch = debounce(getVehiclesRefresh, 500);
292
- watch([searchInput, vehicleType], () => {
294
+ watch([searchInput, vehicleTypeFilter], () => {
293
295
  page.value = 1;
294
296
  debouncedSearch()
295
297
  })
@@ -108,7 +108,7 @@
108
108
  </template>
109
109
  </TableMain>
110
110
 
111
- <v-dialog v-model="dialog.showSelection" width="450" persistent>
111
+ <v-dialog v-model="dialog.showSelection" width="450" persistent>
112
112
  <VisitorFormSelection @cancel="dialog.showSelection = false" @select="handleSelectVisitorType" />
113
113
  </v-dialog>
114
114
 
@@ -437,7 +437,8 @@ async function handleFileAdded(file: File) {
437
437
  const res = await addFile(file);
438
438
  const uploadedId = res?.id;
439
439
  if (uploadedId) {
440
- const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
440
+ // const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
441
+ const url = `${uploadedId}`;
441
442
  _workOrder.value.attachments = _workOrder.value.attachments ?? [];
442
443
  _workOrder.value.attachments.push(url);
443
444
  }
@@ -162,6 +162,16 @@ export default function useAccessManagement() {
162
162
  );
163
163
  }
164
164
 
165
+ function getCardDetails(params: { siteId: string; cardId: string }) {
166
+ return useNuxtApp().$api<{ message: string; data: Record<string, any> }>(
167
+ `/api/access-management/card-details`,
168
+ {
169
+ method: "GET",
170
+ query: { siteId: params.siteId, cardId: params.cardId },
171
+ }
172
+ );
173
+ }
174
+
165
175
  return {
166
176
  getDoorAccessLevels,
167
177
  getLiftAccessLevels,
@@ -175,5 +185,6 @@ export default function useAccessManagement() {
175
185
  getAvailableAccessCards,
176
186
  deleteCard,
177
187
  getCardHistory,
188
+ getCardDetails,
178
189
  };
179
190
  }
@@ -75,9 +75,9 @@ export default function useFeedback() {
75
75
  }
76
76
 
77
77
  function deleteFeedback(id: string) {
78
- return useNuxtApp().$api<Record<string, any>>(`/api/feedbacks/deleted/feedback`, {
78
+ return useNuxtApp().$api<Record<string, any>>(`/api/feedbacks/deleted/feedback/${id}`, {
79
79
  method: "PUT",
80
- query: { id },
80
+ // query: { id },
81
81
  });
82
82
  }
83
83
 
@@ -25,6 +25,15 @@ export default function () {
25
25
  });
26
26
  }
27
27
 
28
+ async function findPersonByNRICMultipleResult(
29
+ nric: string, site: string
30
+ ){
31
+ return await $fetch<Record<any, any>>(`/api/people/all-nric`, {
32
+ method: "GET",
33
+ query: { nric, site }
34
+ });
35
+ }
36
+
28
37
  async function findPersonByContact(
29
38
  contact: string
30
39
  ): Promise<null | Partial<TPeople>> {
@@ -95,6 +104,7 @@ export default function () {
95
104
  updateById,
96
105
  deleteById,
97
106
  findPersonByNRIC,
107
+ findPersonByNRICMultipleResult,
98
108
  findPersonByContact,
99
109
  getPeopleByUnit,
100
110
  searchCompanyList,
@@ -72,9 +72,9 @@ export default function useWorkOrder() {
72
72
  }
73
73
 
74
74
  function deleteWorkOrder(id: string) {
75
- return useNuxtApp().$api<Record<string, any>>(`/api/work-orders/deleted/work-order`, {
75
+ return useNuxtApp().$api<Record<string, any>>(`/api/work-orders/deleted/work-order/${id}`, {
76
76
  method: "PUT",
77
- query: { id },
77
+ // query: { id },
78
78
  });
79
79
  }
80
80
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.10.5",
5
+ "version": "1.10.6",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
package/types/people.d.ts CHANGED
@@ -16,8 +16,11 @@ declare type TPeople = {
16
16
  org?: string;
17
17
  site?: string,
18
18
  type?: TPeopleType;
19
+ plates?: TPlateNumber[]
19
20
  };
20
21
 
22
+ declare type TPlateNumber = { plateNumber: string, recNo: string }
23
+
21
24
 
22
25
  declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type">
23
26
 
@@ -1,6 +1,6 @@
1
1
  declare type TVehicle = {
2
2
  plateNumber: string;
3
- plateNumbers?: string[]; // For display purposes, the API will return an array of plate numbers if there are multiple associated with the same vehicle record
3
+ plates?: TPlateNumber[]; // For display purposes, the API will return an array of plate numbers if there are multiple associated with the same vehicle record
4
4
  type: TVehicleType;
5
5
  category: "resident" | "visitor";
6
6
  direction: "entry" | "exit" | "both" | "none";
@@ -1,133 +0,0 @@
1
- <template>
2
- <v-dialog :model-value="modelValue" width="480" persistent>
3
- <v-card width="100%">
4
- <v-toolbar density="compact" color="black">
5
- <v-toolbar-title class="text-subtitle-1 font-weight-medium">
6
- Card History — {{ card?.cardNo ?? "N/A" }}
7
- </v-toolbar-title>
8
- <v-btn icon @click="emit('update:modelValue', false)">
9
- <v-icon>mdi-close</v-icon>
10
- </v-btn>
11
- </v-toolbar>
12
-
13
- <v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
14
- <div v-if="pending" class="d-flex justify-center align-center py-8">
15
- <v-progress-circular indeterminate color="black" />
16
- </div>
17
-
18
- <div v-else-if="!history.length" class="text-center text-grey py-8">
19
- No history available.
20
- </div>
21
-
22
- <v-timeline v-else side="end" density="compact" truncate-line="both">
23
- <v-timeline-item
24
- v-for="(item, index) in history"
25
- :key="index"
26
- :dot-color="actionColor(item.action)"
27
- size="small"
28
- >
29
- <template #icon>
30
- <v-icon size="14" color="white">{{ actionIcon(item.action) }}</v-icon>
31
- </template>
32
-
33
- <div class="mb-4">
34
- <div class="d-flex align-center ga-2 mb-1">
35
- <v-chip
36
- :color="actionColor(item.action)"
37
- variant="flat"
38
- size="x-small"
39
- class="text-capitalize font-weight-medium"
40
- >
41
- {{ actionLabel(item.action) }}
42
- </v-chip>
43
- </div>
44
- <div class="text-body-2">{{ item.performedBy?.name ?? "N/A" }}</div>
45
- <div class="text-caption text-grey">{{ formatDate(item.date) }}</div>
46
- </div>
47
- </v-timeline-item>
48
- </v-timeline>
49
- </v-card-text>
50
- </v-card>
51
- </v-dialog>
52
- </template>
53
-
54
- <script setup lang="ts">
55
- const props = defineProps({
56
- modelValue: {
57
- type: Boolean,
58
- default: false,
59
- },
60
- card: {
61
- type: Object as PropType<Record<string, any> | null>,
62
- default: null,
63
- },
64
- });
65
-
66
- const emit = defineEmits<{
67
- "update:modelValue": [value: boolean];
68
- }>();
69
-
70
- const { getCardHistory } = useAccessManagement();
71
-
72
- const history = ref<Record<string, any>[]>([]);
73
- const pending = ref(false);
74
-
75
- watch(
76
- () => props.modelValue,
77
- async (val) => {
78
- if (val && props.card?._id) {
79
- pending.value = true;
80
- try {
81
- const data = await getCardHistory(props.card._id);
82
- history.value = Array.isArray(data) ? data : [];
83
- } finally {
84
- pending.value = false;
85
- }
86
- } else {
87
- history.value = [];
88
- }
89
- }
90
- );
91
-
92
- function actionLabel(action: string) {
93
- const map: Record<string, string> = {
94
- available: "Created",
95
- assign: "Assigned",
96
- replace: "Replaced",
97
- deleted: "Deleted",
98
- };
99
- return map[action] ?? action;
100
- }
101
-
102
- function actionColor(action: string) {
103
- const map: Record<string, string> = {
104
- available: "success",
105
- assign: "primary",
106
- replace: "orange",
107
- deleted: "error",
108
- };
109
- return map[action] ?? "grey";
110
- }
111
-
112
- function actionIcon(action: string) {
113
- const map: Record<string, string> = {
114
- available: "mdi-plus",
115
- assign: "mdi-account-check",
116
- replace: "mdi-swap-horizontal",
117
- deleted: "mdi-delete",
118
- };
119
- return map[action] ?? "mdi-circle-small";
120
- }
121
-
122
- function formatDate(date: string) {
123
- if (!date) return "N/A";
124
- return new Intl.DateTimeFormat("en-GB", {
125
- day: "2-digit",
126
- month: "short",
127
- year: "numeric",
128
- hour: "2-digit",
129
- minute: "2-digit",
130
- hour12: true,
131
- }).format(new Date(date));
132
- }
133
- </script>