@7365admin1/layer-common 1.11.2 → 1.11.4

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.11.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 8dba5eb: Update redirect org
8
+
9
+ ## 1.11.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 20a463d: Update Layer-common package
14
+
3
15
  ## 1.11.2
4
16
 
5
17
  ### Patch Changes
@@ -146,7 +146,7 @@
146
146
  type="number"
147
147
  :min="1"
148
148
  hide-spin-buttons
149
- @input="enforceMinQuantity"
149
+ @blur="enforceMinQuantity"
150
150
  />
151
151
  </v-col>
152
152
  <v-col cols="12" class="px-1 pb-5" v-if="!isNonPhysicalCard">
@@ -224,7 +224,7 @@
224
224
  density="compact"
225
225
  :items="accessGroupItems"
226
226
  item-title="name"
227
- item-value="no"
227
+ item-value="name"
228
228
  hide-details
229
229
  :rules="[requiredRule]"
230
230
  multiple
@@ -358,11 +358,13 @@ import useAreas from "../composables/useAreas";
358
358
  import useUnits from "../composables/useUnits";
359
359
  import { useAreaPermission } from "../composables/useAreaPermission";
360
360
  import useSiteSettings from "../composables/useSiteSettings";
361
+ import useUtils from "../composables/useUtils";
361
362
 
362
363
  const props = defineProps({
363
364
  orgId: { type: String, default: "" },
364
365
  site: { type: String, default: "" },
365
366
  type: { type: String, default: "" },
367
+ serviceType: { type: String, default: "" },
366
368
  });
367
369
 
368
370
  const isCleanerArea = computed(() => {
@@ -458,6 +460,7 @@ const {
458
460
  site: props.site,
459
461
  search: searchInput.value,
460
462
  type: areaTypeFilter.value === "all" ? undefined : areaTypeFilter.value,
463
+ serviceType: props.serviceType,
461
464
  }),
462
465
  {
463
466
  watch: [page, () => props.site, () => props.type, areaTypeFilter],
@@ -646,6 +649,7 @@ async function _createArea() {
646
649
  set: itemSet.value,
647
650
  type: (itemType.value || "common").toLowerCase(),
648
651
  units: units.value,
652
+ serviceType: props.serviceType,
649
653
  };
650
654
 
651
655
  let response;
@@ -91,7 +91,7 @@ const props = defineProps({
91
91
  default: false,
92
92
  },
93
93
  cardPeriods: {
94
- type: Object as () => TPeriodState,
94
+ type: Object as () => Record<string, TDashboardValues>,
95
95
  required: true,
96
96
  },
97
97
  currentSiteName: {
@@ -134,7 +134,7 @@ const countCardList = computed(() => {
134
134
  : [];
135
135
 
136
136
  return sourceCards.map((card: any, idx: number) => {
137
- const periodKey = card.periodKey as keyof TPeriodState;
137
+ const periodKey = card.periodKey as keyof Record<string, TDashboardValues>;
138
138
  const apiPeriod = props.cardPeriods[periodKey] || "today";
139
139
  const displayPeriod = datePeriod[apiPeriod] || "Today";
140
140
 
@@ -10,15 +10,15 @@
10
10
  v-model:page="page"
11
11
  :pages="pages"
12
12
  :pageRange="pageRange"
13
- :no-data-text="`No supplies found for ${siteName}.`"
14
- @refresh="getSuppliesRefresh"
15
- :canCreate="canAddSupply"
16
- createLabel="Add Supply"
13
+ :no-data-text="`No equipment found for ${siteName}.`"
14
+ @refresh="getEquipmentsRefresh"
15
+ :canCreate="canAddEquipment"
16
+ createLabel="Add Equipment"
17
17
  @row-click="handleRowClick"
18
18
  >
19
19
  <template #actions>
20
20
  <v-row no-gutters align="center" class="w-100">
21
- <v-col cols="auto" v-if="canAddSupply">
21
+ <v-col cols="auto" v-if="canAddEquipment">
22
22
  <v-btn
23
23
  class="text-none"
24
24
  rounded="pill"
@@ -26,7 +26,7 @@
26
26
  size="large"
27
27
  @click="onCreateUnit()"
28
28
  >
29
- Add Supply
29
+ Add Equipment
30
30
  </v-btn>
31
31
  </v-col>
32
32
 
@@ -57,28 +57,27 @@
57
57
  </v-col>
58
58
  </v-row>
59
59
 
60
- <AddSupplyForm
60
+ <AddEqupmentForm
61
61
  v-model="dialogShowForm"
62
- v-model:name="supplyName"
63
- v-model:qty="supplyQuantity"
64
- v-model:unitOfMeasurement="supplyUnit"
62
+ v-model:name="equipmentName"
63
+ v-model:unitOfMeasurement="equipmentUnit"
65
64
  :mode="dialogMode"
66
- :supplyData="editingSupply"
65
+ :equipmentData="editingEquipment"
67
66
  :site="props.site"
68
- @saved="_createSupply"
67
+ @saved="_createEquipment"
69
68
  />
70
69
 
71
70
  <v-dialog v-model="dialogShowMoreActions" width="400" persistent>
72
71
  <HygieneUpdateMoreAction
73
- :title="selectedSupply?.name || 'Unit Actions'"
74
- :canUpdate="canUpdateSupply"
75
- :canDelete="canDeleteSupply"
72
+ :title="selectedEquipment?.name || 'Unit Actions'"
73
+ :canUpdate="canUpdateEquipment"
74
+ :canDelete="canDeleteEquipment"
76
75
  :showAddStock="canAddStock"
77
76
  :showViewStock="canViewStock"
78
77
  :showRequestItem="canRequestItem"
79
78
  :manageButtonLabel="''"
80
- :editButtonLabel="'Edit Supply'"
81
- :deleteButtonLabel="'Delete Supply'"
79
+ :editButtonLabel="'Edit Equipment'"
80
+ :deleteButtonLabel="'Delete Equipment'"
82
81
  :addStockButtonLabel="'Add Stock'"
83
82
  :viewButtonLabel="'View Stock'"
84
83
  :requestItemButtonLabel="'Checkout Item'"
@@ -91,13 +90,13 @@
91
90
  >
92
91
  <template #content>
93
92
  <v-row no-gutters>
94
- <v-col v-if="selectedSupply" cols="12" class="mb-2">
93
+ <v-col v-if="selectedEquipment" cols="12" class="mb-2">
95
94
  <v-row no-gutters class="mb-2">
96
95
  <v-col cols="5" class="text-subtitle-2 font-weight-bold">
97
96
  Name:
98
97
  </v-col>
99
98
  <v-col cols="7" class="text-subtitle-2">
100
- {{ selectedSupply.name || "N/A" }}
99
+ {{ selectedEquipment.name || "N/A" }}
101
100
  </v-col>
102
101
  </v-row>
103
102
 
@@ -106,7 +105,7 @@
106
105
  Quantity:
107
106
  </v-col>
108
107
  <v-col cols="7" class="text-subtitle-2">
109
- {{ selectedSupply.qty || "N/A" }}
108
+ {{ selectedEquipment.qty || "N/A" }}
110
109
  </v-col>
111
110
  </v-row>
112
111
 
@@ -115,7 +114,7 @@
115
114
  Unit of Measurement:
116
115
  </v-col>
117
116
  <v-col cols="7" class="text-subtitle-2">
118
- {{ selectedSupply.unitOfMeasurement || "N/A" }}
117
+ {{ selectedEquipment.unitOfMeasurement || "N/A" }}
119
118
  </v-col>
120
119
  </v-row>
121
120
 
@@ -128,18 +127,18 @@
128
127
  </HygieneUpdateMoreAction>
129
128
  </v-dialog>
130
129
 
131
- <v-dialog v-model="dialogDeleteSupply" max-width="450">
130
+ <v-dialog v-model="dialogDeleteEquipment" max-width="450">
132
131
  <v-card>
133
132
  <v-toolbar>
134
133
  <v-row no-gutters class="fill-height px-6" align="center">
135
134
  <span class="font-weight-bold text-h6 text-capitalize">
136
- Delete {{ selectedSupply?.name || "Unit" }}
135
+ Delete {{ selectedEquipment?.name || "Unit" }}
137
136
  </span>
138
137
  </v-row>
139
138
  </v-toolbar>
140
139
  <v-card-text>
141
140
  <span class="text-subtitle-2"
142
- >Are you sure you want to delete this unitOfMeasurement?</span
141
+ >Are you sure you want to delete this equipment?</span
143
142
  >
144
143
  </v-card-text>
145
144
 
@@ -151,7 +150,7 @@
151
150
  variant="text"
152
151
  class="text-none"
153
152
  size="large"
154
- @click="dialogDeleteSupply = false"
153
+ @click="dialogDeleteEquipment = false"
155
154
  height="48"
156
155
  >
157
156
  Cancel
@@ -166,7 +165,7 @@
166
165
  class="text-none font-weight-bold"
167
166
  height="48"
168
167
  :loading="submitting"
169
- @click="_deleteSupply"
168
+ @click="_deleteEquipment"
170
169
  >
171
170
  Delete
172
171
  </v-btn>
@@ -245,11 +244,11 @@
245
244
 
246
245
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
247
246
  </template>
248
-
249
247
  <script setup lang="ts">
250
- import useSupply from "../composables/useSupply";
248
+ import useEquipment from "../composables/useEquipment";
251
249
  import useStock from "../composables/useStock";
252
- import { useSupplyPermission } from "../composables/useSupplyPermission";
250
+ import { useEquipmentPermission } from "../composables/useEquipmentPermission";
251
+ import useUtils from "../composables/useUtils";
253
252
 
254
253
  const props = defineProps({
255
254
  orgId: { type: String, default: "" },
@@ -257,14 +256,14 @@ const props = defineProps({
257
256
  });
258
257
 
259
258
  const {
260
- canViewSupplies,
261
- canAddSupply,
262
- canUpdateSupply,
263
- canDeleteSupply,
259
+ canViewEquipments,
260
+ canAddEquipment,
261
+ canUpdateEquipment,
262
+ canDeleteEquipment,
264
263
  canAddStock,
265
264
  canViewStock,
266
265
  canRequestItem,
267
- } = useSupplyPermission();
266
+ } = useEquipmentPermission();
268
267
 
269
268
  const items = ref<Array<Record<string, any>>>([]);
270
269
  const siteName = ref<string>("");
@@ -276,8 +275,13 @@ const headers = [
276
275
  { title: "Status", value: "status" },
277
276
  ];
278
277
 
279
- const { getSupplies, getSupplyById, createSupply, updateSupply, deleteSupply } =
280
- useSupply();
278
+ const {
279
+ getEquipments,
280
+ getEquipmentById,
281
+ createEquipment,
282
+ updateEquipment,
283
+ deleteEquipment,
284
+ } = useEquipment();
281
285
  const { debounce, requiredRule } = useUtils();
282
286
 
283
287
  const searchInput = ref("");
@@ -288,13 +292,13 @@ const pages = ref(0);
288
292
  const pageRange = ref("-- - -- of --");
289
293
 
290
294
  const {
291
- data: getSuppliesReq,
292
- refresh: getSuppliesRefresh,
295
+ data: getEquipmentsReq,
296
+ refresh: getEquipmentsRefresh,
293
297
  pending: loading,
294
- } = await useLazyAsyncData(
295
- "get-all-supplies",
298
+ } = await useLazyAsyncData<any>(
299
+ "get-all-equipments",
296
300
  () =>
297
- getSupplies({
301
+ getEquipments({
298
302
  page: page.value,
299
303
  search: searchInput.value,
300
304
  site: props.site,
@@ -305,14 +309,14 @@ const {
305
309
  );
306
310
 
307
311
  watchEffect(() => {
308
- if (getSuppliesReq.value) {
309
- items.value = getSuppliesReq.value.items;
310
- pages.value = getSuppliesReq.value.pages;
311
- pageRange.value = getSuppliesReq.value.pageRange;
312
+ if (getEquipmentsReq.value) {
313
+ items.value = getEquipmentsReq.value.items;
314
+ pages.value = getEquipmentsReq.value.pages;
315
+ pageRange.value = getEquipmentsReq.value.pageRange;
312
316
  }
313
317
  });
314
318
 
315
- const debounceSearch = debounce(getSuppliesRefresh, 500);
319
+ const debounceSearch = debounce(getEquipmentsRefresh, 500);
316
320
  watch(
317
321
  [searchInput, endDate, startDate],
318
322
  ([]) => {
@@ -321,23 +325,21 @@ watch(
321
325
  { immediate: false, deep: true }
322
326
  );
323
327
 
324
- const selectedSupply = ref<Record<string, any>>({});
328
+ const selectedEquipment = ref<Record<string, any>>({});
325
329
  const message = ref("");
326
330
  const messageSnackbar = ref(false);
327
331
  const messageColor = ref("");
328
- const fileInput = ref<HTMLInputElement | null>(null);
329
332
 
330
333
  const dialogShowMoreActions = ref(false);
331
334
  const dialogShowForm = ref(false);
332
- const dialogDeleteSupply = ref(false);
335
+ const dialogDeleteEquipment = ref(false);
333
336
  const dialogStockAction = ref(false);
334
337
  const stockActionMode = ref<"add" | "request">("add");
335
338
  const dialogMode = ref<"add" | "edit">("add");
336
339
 
337
- const editingSupply = ref<Record<string, any>>({});
338
- const supplyName = ref("");
339
- const supplyQuantity = ref(0);
340
- const supplyUnit = ref("");
340
+ const editingEquipment = ref<Record<string, any>>({});
341
+ const equipmentName = ref("");
342
+ const equipmentUnit = ref("");
341
343
 
342
344
  const stockActionFormRef = ref<any>(null);
343
345
  const stockActionValid = ref(false);
@@ -350,49 +352,39 @@ function showMessage(msg: string, color: string = "error") {
350
352
 
351
353
  function onCreateUnit() {
352
354
  dialogMode.value = "add";
353
- editingSupply.value = {};
354
- supplyName.value = "";
355
- supplyQuantity.value = 0;
356
- supplyUnit.value = "";
355
+ editingEquipment.value = {};
356
+ equipmentName.value = "";
357
+ equipmentUnit.value = "";
357
358
  dialogShowForm.value = true;
358
359
  }
359
360
 
360
- function onSaveSupply(data: {
361
- name: string;
362
- qty: number;
363
- unitOfMeasurement: string;
364
- }) {
365
- showMessage("Supply saved successfully", "success");
366
- getSuppliesRefresh();
367
- }
368
-
369
- async function _createSupply() {
361
+ async function _createEquipment() {
370
362
  submitting.value = true;
371
363
  try {
372
- const payload: TSupplyCreate = {
373
- name: supplyName.value,
374
- unitOfMeasurement: supplyUnit.value,
364
+ const payload: TEquipmentCreate = {
365
+ name: equipmentName.value,
366
+ unitOfMeasurement: equipmentUnit.value,
375
367
  };
376
368
 
377
369
  let response;
378
370
 
379
- if (dialogMode.value === "edit" && editingSupply.value) {
371
+ if (dialogMode.value === "edit" && editingEquipment.value) {
380
372
  const id =
381
- (editingSupply.value as any)._id || (editingSupply.value as any).id;
382
- if (!id) throw new Error("Invalid supply id for update");
383
- response = await updateSupply(id, payload);
373
+ (editingEquipment.value as any)._id ||
374
+ (editingEquipment.value as any).id;
375
+ if (!id) throw new Error("Invalid equipment id for update");
376
+ response = await updateEquipment(id, payload);
384
377
  } else {
385
- response = await createSupply(payload, props.site);
378
+ response = await createEquipment(payload, props.site);
386
379
  }
387
380
 
388
381
  showMessage(response?.message, "success");
389
382
  dialogShowForm.value = false;
390
- editingSupply.value = {};
391
- supplyName.value = "";
392
- supplyQuantity.value = 0;
393
- supplyUnit.value = "";
383
+ editingEquipment.value = {};
384
+ equipmentName.value = "";
385
+ equipmentUnit.value = "";
394
386
 
395
- await getSuppliesRefresh();
387
+ await getEquipmentsRefresh();
396
388
  } catch (error: any) {
397
389
  console.error(error);
398
390
  showMessage(error?.data?.message, "error");
@@ -401,60 +393,59 @@ async function _createSupply() {
401
393
  }
402
394
  }
403
395
 
404
- const selectedSupplyId = ref("");
396
+ const selectedEquipmentId = ref("");
405
397
 
406
398
  const {
407
- data: getSupplyByIdReq,
408
- refresh: getSupplyByIdRefresh,
409
- status: getSupplyByIdStatus,
410
- } = await useLazyAsyncData(
411
- "get-supply-by-id",
412
- () => getSupplyById(selectedSupplyId.value),
399
+ data: getEquipmentByIdReq,
400
+ refresh: getEquipmentByIdRefresh,
401
+ status: getEquipmentByIdStatus,
402
+ } = await useLazyAsyncData<any>(
403
+ "get-equipment-by-id",
404
+ () => getEquipmentById(selectedEquipmentId.value),
413
405
  {
414
- watch: [selectedSupplyId],
406
+ watch: [selectedEquipmentId],
415
407
  immediate: false,
416
408
  }
417
409
  );
418
410
 
419
411
  watchEffect(() => {
420
- if (getSupplyByIdReq.value && getSupplyByIdStatus.value === "success") {
421
- selectedSupply.value = getSupplyByIdReq.value;
412
+ if (getEquipmentByIdReq.value && getEquipmentByIdStatus.value === "success") {
413
+ selectedEquipment.value = getEquipmentByIdReq.value;
422
414
  }
423
415
  });
424
416
 
425
417
  async function handleRowClick(data: any) {
426
- selectedSupply.value = data?.item;
418
+ selectedEquipment.value = data?.item;
427
419
  message.value = "";
428
420
 
429
- selectedSupplyId.value = selectedSupply.value._id;
430
- await getSupplyByIdRefresh();
421
+ selectedEquipmentId.value = selectedEquipment.value._id;
422
+ await getEquipmentByIdRefresh();
431
423
 
432
424
  dialogShowMoreActions.value = true;
433
425
  }
434
426
 
435
427
  const onEditFromMoreAction = async () => {
436
428
  dialogShowMoreActions.value = false;
437
- if (!selectedSupply.value) return;
429
+ if (!selectedEquipment.value) return;
438
430
 
439
431
  submitting.value = true;
440
- selectedSupplyId.value = selectedSupply.value._id;
441
- await getSupplyByIdRefresh();
432
+ selectedEquipmentId.value = selectedEquipment.value._id;
433
+ await getEquipmentByIdRefresh();
442
434
  submitting.value = false;
443
435
 
444
- const response = getSupplyByIdReq.value;
436
+ const response = getEquipmentByIdReq.value;
445
437
  if (!response) return;
446
438
 
447
439
  dialogMode.value = "edit";
448
- editingSupply.value = response;
449
- supplyName.value = response.name || "";
450
- supplyQuantity.value = response.qty || 0;
451
- supplyUnit.value = response.unitOfMeasurement || "";
440
+ editingEquipment.value = response;
441
+ equipmentName.value = response.name || "";
442
+ equipmentUnit.value = response.unitOfMeasurement || "";
452
443
  dialogShowForm.value = true;
453
444
  };
454
445
 
455
446
  const onDeleteFromMoreAction = () => {
456
447
  dialogShowMoreActions.value = false;
457
- dialogDeleteSupply.value = true;
448
+ dialogDeleteEquipment.value = true;
458
449
  };
459
450
 
460
451
  const onAddStockFromMoreAction = () => {
@@ -469,8 +460,8 @@ const onViewStockFromMoreAction = () => {
469
460
  dialogShowMoreActions.value = false;
470
461
  const org = props.orgId;
471
462
  const site = props.site;
472
- const id = selectedSupply.value._id;
473
- useRouter().push(`/${org}/${site}/supply-management/stock/${id}`);
463
+ const id = selectedEquipment.value._id;
464
+ useRouter().push(`/${org}/${site}/equipment-management/stock/${id}`);
474
465
  };
475
466
 
476
467
  const onRequestItemFromMoreAction = () => {
@@ -481,20 +472,21 @@ const onRequestItemFromMoreAction = () => {
481
472
  dialogStockAction.value = true;
482
473
  };
483
474
 
484
- async function _deleteSupply() {
485
- if (!selectedSupply.value) return;
475
+ async function _deleteEquipment() {
476
+ if (!selectedEquipment.value) return;
486
477
 
487
478
  try {
488
479
  submitting.value = true;
489
480
  const id =
490
- (selectedSupply.value as any)._id || (selectedSupply.value as any).id;
491
- if (!id) throw new Error("Invalid supply id");
481
+ (selectedEquipment.value as any)._id ||
482
+ (selectedEquipment.value as any).id;
483
+ if (!id) throw new Error("Invalid equipment id");
492
484
 
493
- const response = await deleteSupply(id);
494
- dialogDeleteSupply.value = false;
485
+ const response = await deleteEquipment(id);
486
+ dialogDeleteEquipment.value = false;
495
487
  showMessage(response?.message, "success");
496
488
 
497
- await getSuppliesRefresh();
489
+ await getEquipmentsRefresh();
498
490
  } catch (error: any) {
499
491
  showMessage(error?.data?.message, "error");
500
492
  } finally {
@@ -510,7 +502,7 @@ const stockActionRemarks = ref("");
510
502
  async function _addStock() {
511
503
  submitting.value = true;
512
504
  try {
513
- const id = selectedSupply.value._id || selectedSupply.value.id;
505
+ const id = selectedEquipment.value._id || selectedEquipment.value.id;
514
506
 
515
507
  const payload: TStockCreate = {
516
508
  qty: stockActionQuantity.value,
@@ -523,7 +515,7 @@ async function _addStock() {
523
515
  stockActionQuantity.value = 0;
524
516
  stockActionRemarks.value = "";
525
517
 
526
- await getSuppliesRefresh();
518
+ await getEquipmentsRefresh();
527
519
  } catch (error: any) {
528
520
  console.error(error);
529
521
  showMessage(error?.data?.message, "error");
@@ -535,7 +527,7 @@ async function _addStock() {
535
527
  async function _requestItem() {
536
528
  submitting.value = true;
537
529
  try {
538
- const id = selectedSupply.value._id || selectedSupply.value.id;
530
+ const id = selectedEquipment.value._id || selectedEquipment.value.id;
539
531
 
540
532
  const payload: TStockCreate = {
541
533
  qty: stockActionQuantity.value,
@@ -546,7 +538,7 @@ async function _requestItem() {
546
538
  dialogStockAction.value = false;
547
539
  stockActionQuantity.value = 0;
548
540
 
549
- await getSuppliesRefresh();
541
+ await getEquipmentsRefresh();
550
542
  } catch (error: any) {
551
543
  console.error(error);
552
544
  showMessage(error?.data?.message, "error");
@@ -117,20 +117,45 @@
117
117
  </v-col>
118
118
  </v-row>
119
119
  </template>
120
+ <template #group-header-chips> </template>
120
121
  <template #group-header-append="{ group }">
121
- <v-btn
122
- v-if="group.attachments && group.attachments.length > 0"
123
- size="x-small"
124
- variant="tonal"
125
- color="primary"
126
- class="text-none"
127
- prepend-icon="mdi-paperclip"
128
- @click.stop="openAttachmentDialog(group.set, group.attachments)"
129
- >
130
- {{ group.attachments.length }} attachment{{
131
- group.attachments.length > 1 ? "s" : ""
132
- }}
133
- </v-btn>
122
+ <v-row no-gutters align="center" class="ga-2">
123
+ <v-col v-if="isGroupAnyApproved(group)" cols="auto">
124
+ <v-chip
125
+ size="x-small"
126
+ :color="isGroupComplete(group) ? 'success' : 'warning'"
127
+ variant="tonal"
128
+ :prepend-icon="
129
+ isGroupComplete(group)
130
+ ? 'mdi-check-circle-outline'
131
+ : 'mdi-progress-clock'
132
+ "
133
+ class="text-none"
134
+ >
135
+ {{ isGroupComplete(group) ? "Completed" : "Ongoing" }}
136
+ <template v-if="getGroupCompletedByName(group)">
137
+ · {{ getGroupCompletedByName(group) }}
138
+ </template>
139
+ </v-chip>
140
+ </v-col>
141
+ <v-col
142
+ v-if="group.attachments && group.attachments.length > 0"
143
+ cols="auto"
144
+ >
145
+ <v-btn
146
+ size="x-small"
147
+ variant="tonal"
148
+ color="primary"
149
+ class="text-none"
150
+ prepend-icon="mdi-paperclip"
151
+ @click.stop="openAttachmentDialog(group.set, group.attachments)"
152
+ >
153
+ {{ group.attachments.length }} attachment{{
154
+ group.attachments.length > 1 ? "s" : ""
155
+ }}
156
+ </v-btn>
157
+ </v-col>
158
+ </v-row>
134
159
  </template>
135
160
  </TableHygiene>
136
161
  </v-col>
@@ -328,6 +353,36 @@ function getKey(item: any, set?: number): string {
328
353
  return `${item.unit}_${set ?? ""}`;
329
354
  }
330
355
 
356
+ function isGroupComplete(group: { set: number; items: any[] }): boolean {
357
+ return (
358
+ group.items.length > 0 &&
359
+ group.items.every(
360
+ (item: any) =>
361
+ item.approve === true ||
362
+ activeActions[getKey(item, group.set)] === "approve"
363
+ )
364
+ );
365
+ }
366
+
367
+ function isGroupAnyApproved(group: { set: number; items: any[] }): boolean {
368
+ return group.items.some(
369
+ (item: any) =>
370
+ item.approve === true ||
371
+ activeActions[getKey(item, group.set)] === "approve"
372
+ );
373
+ }
374
+
375
+ function getGroupCompletedByName(group: {
376
+ completedByName: string | null;
377
+ items: any[];
378
+ }): string | null {
379
+ return (
380
+ group.completedByName ??
381
+ group.items.find((item: any) => item.completedByName)?.completedByName ??
382
+ null
383
+ );
384
+ }
385
+
331
386
  function isSetFullyApproved(setNumber: number): boolean {
332
387
  const group = items.value.find((g: any) => g.set === setNumber);
333
388
  if (!group) return false;