@7365admin1/layer-common 1.10.4 → 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.
@@ -57,10 +57,9 @@
57
57
  }}
58
58
  </template>
59
59
  <template #item.closeIn="{ item }">
60
- <!-- <v-chip class="text-capitalize">{{ value || "No Status" }}</v-chip> -->
61
- <v-chip class="text-capitalize" variant="flat" color="primary">{{
62
- remainingTime[item._id] || "No Status"
63
- }}</v-chip>
60
+ <v-chip class="text-capitalize" variant="flat" color="primary" pill>
61
+ {{ remainingTime[item._id] || "No Status" }}
62
+ </v-chip>
64
63
  </template>
65
64
  <template #item.status="{ value }">
66
65
  <v-chip
@@ -129,7 +128,8 @@ const headers = [
129
128
  { title: "Completion Date", value: "completionDate" },
130
129
  { title: "", value: "download" },
131
130
  ];
132
- const remainingTime = ref({} as any);
131
+ const remainingTime = ref({} as Record<string, string>);
132
+ const remainingSeconds = ref({} as Record<string, number>);
133
133
  const downloadingId = ref<string | null>(null);
134
134
 
135
135
  const { getCleaningSchedules, downloadChecklistPdf } = useCleaningSchedules();
@@ -148,11 +148,22 @@ const getStatusColor = (status: unknown): string => {
148
148
  case "completed":
149
149
  case "accepted":
150
150
  return "success";
151
+ case "closed":
152
+ case "rejected":
153
+ return "error";
151
154
  default:
152
155
  return "secondary";
153
156
  }
154
157
  };
155
158
 
159
+ const getCloseInColor = (id: string): string => {
160
+ const secs = remainingSeconds.value[id];
161
+ if (secs === undefined) return "primary";
162
+ if (secs <= 0) return "error";
163
+ if (secs < 3 * 3600) return "warning";
164
+ return "primary";
165
+ };
166
+
156
167
  const {
157
168
  canDownloadSchedule,
158
169
  canViewSchedules,
@@ -205,11 +216,11 @@ const formatTime = (seconds: number) => {
205
216
  };
206
217
 
207
218
  const updateRemainingTime = () => {
208
- console.log(items.value);
209
219
  items.value.forEach((item) => {
210
220
  const itemId = item._id as string;
211
221
  const _time = calculateRemainingTime(item.date as string);
212
- remainingTime.value[itemId] = _time < 0 ? "00h 00m" : formatTime(_time);
222
+ remainingSeconds.value[itemId] = _time;
223
+ remainingTime.value[itemId] = _time <= 0 ? "00h 00m" : formatTime(_time);
213
224
  });
214
225
  };
215
226
 
@@ -76,6 +76,26 @@
76
76
  style="max-height: calc(100vh - (200px))"
77
77
  class="border mt-5"
78
78
  >
79
+ <template #header.nric>
80
+ <div class="d-flex align-center">
81
+ NRIC
82
+ <v-icon
83
+ class="cursor-pointer ml-1"
84
+ size="18"
85
+ color="blue"
86
+ @click="isShowNRIC = !isShowNRIC"
87
+ >
88
+ {{ isShowNRIC ? "mdi-eye-off-outline" : "mdi-eye-outline" }}
89
+ </v-icon>
90
+ </div>
91
+ </template>
92
+ <template #item.nric="{ value }">
93
+ <tr>
94
+ {{
95
+ maskedNric(value)
96
+ }}
97
+ </tr>
98
+ </template>
79
99
  </v-data-table>
80
100
  </v-col>
81
101
 
@@ -142,6 +162,7 @@ const injuredTableHeader = [
142
162
  value: "contact",
143
163
  },
144
164
  ];
165
+
145
166
  const damagePropertyTableHeader = [
146
167
  {
147
168
  title: "Description",
@@ -164,4 +185,12 @@ const damagePropertyTableHeader = [
164
185
  value: "action",
165
186
  },
166
187
  ];
188
+
189
+ const isShowNRIC = ref(false);
190
+
191
+ const maskedNric = (nric: string) => {
192
+ if (!nric) return "";
193
+ if (isShowNRIC.value) return nric;
194
+ return nric[0] + "*".repeat(nric.length - 5) + nric.slice(-4);
195
+ };
167
196
  </script>
@@ -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'])
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="d-flex align-center ga-2 flex-wrap">
3
+ <span v-if="formatted">
4
+ {{ formatted }}
5
+ </span>
6
+
7
+ <v-chip
8
+ v-if="extraCount > 0 && !showAll"
9
+ density="comfortable"
10
+ size="small"
11
+ >
12
+ + {{ extraCount }}
13
+ </v-chip>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ const props = defineProps({
19
+ plateNumbers: {
20
+ type: Array as PropType<string[]>,
21
+ default: () => []
22
+ },
23
+ defaultValue: {
24
+ type: String,
25
+ default: ""
26
+ },
27
+ showAll: {
28
+ type: Boolean,
29
+ default: false
30
+ }
31
+ })
32
+
33
+ const formatted = computed(() => {
34
+ if (!props.plateNumbers?.length) return props.defaultValue || ""
35
+
36
+ if (props.showAll) {
37
+ return props.plateNumbers.map((v: any) => v?.plateNumber || "").join(", ")
38
+ }
39
+
40
+ const firstTwo = props.plateNumbers
41
+ .slice(0, 2)
42
+ .map((v: any) => v?.plateNumber || "")
43
+
44
+ return firstTwo.join(", ")
45
+ })
46
+
47
+ const extraCount = computed(() => {
48
+ if (!props.plateNumbers) return 0
49
+ return props.plateNumbers.length > 2
50
+ ? props.plateNumbers.length - 2
51
+ : 0
52
+ })
53
+ </script>
@@ -243,6 +243,9 @@ const getStatusColor = (status: unknown): string => {
243
243
  case "completed":
244
244
  case "accepted":
245
245
  return "success";
246
+ case "closed":
247
+ case "rejected":
248
+ return "error";
246
249
  default:
247
250
  return "secondary";
248
251
  }
@@ -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>