@7365admin1/layer-common 1.10.5 → 1.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardDetailsDialog.vue +144 -0
  3. package/components/AccessCardPreviewDialog.vue +7 -2
  4. package/components/AccessCardQrTagging.vue +314 -34
  5. package/components/AccessCardQrTaggingPrintQr.vue +75 -0
  6. package/components/AccessManagement.vue +5 -1
  7. package/components/AreaChecklistHistoryLogs.vue +9 -0
  8. package/components/BuildingForm.vue +36 -5
  9. package/components/BuildingManagement/buildings.vue +18 -9
  10. package/components/BuildingManagement/units.vue +12 -114
  11. package/components/BuildingUnitFormAdd.vue +39 -30
  12. package/components/BuildingUnitFormEdit.vue +265 -116
  13. package/components/CleaningScheduleMain.vue +60 -13
  14. package/components/Dialog/DeleteConfirmation.vue +2 -2
  15. package/components/Dialog/UpdateMoreAction.vue +2 -2
  16. package/components/EntryPassInformation.vue +215 -0
  17. package/components/Input/InputPhoneNumberV2.vue +11 -0
  18. package/components/ManageChecklistMain.vue +29 -3
  19. package/components/PlateNumberDisplay.vue +9 -1
  20. package/components/ScheduleAreaMain.vue +56 -0
  21. package/components/TableHygiene.vue +265 -113
  22. package/components/UnitPersonCard.vue +63 -0
  23. package/components/VehicleAddSelection.vue +2 -2
  24. package/components/VehicleForm.vue +169 -47
  25. package/components/VehicleManagement.vue +169 -43
  26. package/components/VisitorForm.vue +19 -0
  27. package/components/VisitorManagement.vue +1 -1
  28. package/components/WorkOrder/Main.vue +2 -1
  29. package/composables/useAccessManagement.ts +63 -0
  30. package/composables/useFeedback.ts +2 -2
  31. package/composables/usePeople.ts +14 -3
  32. package/composables/useVehicle.ts +15 -1
  33. package/composables/useWorkOrder.ts +2 -2
  34. package/package.json +3 -2
  35. package/types/cleaner-schedule.d.ts +1 -0
  36. package/types/html2pdf.d.ts +19 -0
  37. package/types/people.d.ts +4 -0
  38. package/types/site.d.ts +8 -0
  39. package/types/vehicle.d.ts +3 -4
  40. package/.playground/app.vue +0 -41
  41. package/.playground/eslint.config.mjs +0 -6
  42. package/.playground/nuxt.config.ts +0 -22
  43. package/.playground/pages/feedback.vue +0 -30
  44. package/components/AccessCardHistoryDialog.vue +0 -133
@@ -1,64 +1,194 @@
1
1
  <template>
2
- <v-card width="100%">
2
+ <v-card width="100%" :loading="getUnitPeoplePending">
3
3
  <v-toolbar>
4
4
  <v-row no-gutters class="fill-height px-6" align="center">
5
- <span class="font-weight-bold text-h5"> Edit Unit </span>
5
+ <span class="font-weight-bold text-h5"> Unit Info ( {{ buildingUnit.name }} ) </span>
6
6
  </v-row>
7
7
  </v-toolbar>
8
- <v-card-text style="max-height: 100vh; overflow-y: auto">
9
- <v-form v-model="validForm" :disabled="disable">
10
- <v-row no-gutters>
11
- <v-col cols="12">
12
- <v-row>
13
- <v-col cols="12" class="mt-2">
14
- <v-row no-gutters>
15
- <InputLabel class="text-capitalize font-weight-bold" title="Building" />
16
- <v-col cols="12">
17
- {{ buildingUnit.buildingName }}
18
- </v-col>
19
- </v-row>
20
- </v-col>
21
- </v-row>
22
- </v-col>
23
-
24
- <v-col cols="12">
25
- <v-row>
26
- <v-col cols="12" class="mt-2">
27
- <v-row no-gutters>
28
- <InputLabel class="text-capitalize font-weight-bold" title="Block" />
29
- <v-col cols="12">
30
- {{ buildingUnit.block }}
31
- </v-col>
32
- </v-row>
33
- </v-col>
34
- </v-row>
35
- </v-col>
36
-
37
- <v-col cols="12">
38
- <v-row>
39
- <v-col cols="12" md="6" class="mt-2">
40
- <v-row no-gutters>
41
- <InputLabel class="text-capitalize font-weight-bold" title="Unit Name" />
42
- <v-col cols="12">
43
- <v-text-field v-model="buildingUnit.name" density="comfortable"
44
- :rules="[requiredRule]"></v-text-field>
45
- </v-col>
46
- </v-row>
47
- </v-col>
48
-
49
- <v-col cols="12" md="6" class="mt-2">
50
- <v-row no-gutters>
51
- <InputLabel class="text-capitalize font-weight-bold" title="Owner" />
52
- <v-col cols="12">
53
- <v-autocomplete v-model="buildingUnit.ownerName" v-model:search="ownerNameInput"
54
- :items="peopleItems" item-title="name" return-object density="comfortable">
55
- </v-autocomplete>
56
- </v-col>
57
- </v-row>
8
+ <v-toolbar density="compact">
9
+ <v-tabs v-model="tab" class="mt-0" active-class="bg-success">
10
+ <v-tab value="general">General</v-tab>
11
+ <v-tab value="residents">Residents/Tenants</v-tab>
12
+ <v-tab value="vehicles">Vehicles</v-tab>
13
+ <v-tab value="others">Others</v-tab>
14
+ </v-tabs>
15
+ </v-toolbar>
16
+ <v-card-text style="max-height: 100vh; min-height: 60svh; overflow-y: auto">
17
+ <template v-if="tab === 'general'">
18
+ <v-form v-model="validForm" :disabled="disable">
19
+ <v-row>
20
+ <v-col cols="12" md="6" lg="4" class="mt-2">
21
+ <v-row no-gutters>
22
+ <InputLabel class="text-capitalize font-weight-bold" title="Unit Name" />
23
+ <v-col cols="12">
24
+ <v-text-field v-model="buildingUnit.name" density="comfortable"
25
+ :rules="[requiredRule]"></v-text-field>
26
+ </v-col>
27
+ </v-row>
28
+ </v-col>
29
+
30
+ <v-col cols="12" md="6" lg="4" class="mt-2">
31
+ <v-row no-gutters>
32
+ <InputLabel class="text-capitalize font-weight-bold" title="Level" required />
33
+ <v-col cols="12">
34
+ <v-select v-model="buildingUnit.level" :items="buildingLevels" density="comfortable"
35
+ :rules="[requiredRule]"></v-select>
36
+ </v-col>
37
+ </v-row>
38
+ </v-col>
39
+
40
+ <v-col cols="12" md="6" lg="4">
41
+ <v-row>
42
+ <v-col cols="12" class="mt-2">
43
+ <v-row no-gutters>
44
+ <InputLabel class="text-capitalize font-weight-bold" title="Block Name" />
45
+ <v-col cols="12">
46
+ <v-text-field :model-value="`Block ${buildingUnit.block} (${buildingUnit.buildingName})`"
47
+ density="comfortable" readonly class="no-pointer"></v-text-field>
48
+ </v-col>
49
+ </v-row>
50
+ </v-col>
51
+ </v-row>
52
+ </v-col>
53
+ </v-row>
54
+
55
+ <v-divider class="mb-10" />
56
+ <v-row>
57
+ <v-col cols="12" md="6" class="mt-2">
58
+ <v-row no-gutters>
59
+ <InputLabel class="text-capitalize font-weight-bold" title="Billing Address Line 1" />
60
+ <v-col cols="12">
61
+ <v-text-field :model-value="siteData?.address?.line1" density="compact" readonly
62
+ class="no-pointer"></v-text-field>
63
+ </v-col>
64
+ </v-row>
65
+ </v-col>
66
+ <v-col cols="12" md="6" class="mt-2">
67
+ <v-row no-gutters>
68
+ <InputLabel class="text-capitalize font-weight-bold" title="Billing Address Line 2" />
69
+ <v-col cols="12">
70
+ <v-text-field :model-value="siteData?.address?.line2" density="compact" readonly
71
+ class="no-pointer"></v-text-field>
72
+ </v-col>
73
+ </v-row>
74
+ </v-col>
75
+ <v-col cols="12" md="4" lg="4" class="mt-2">
76
+ <v-row no-gutters>
77
+ <InputLabel class="text-capitalize font-weight-bold" title="City" />
78
+ <v-col cols="12">
79
+ <v-text-field :model-value="siteData?.address?.city" density="compact" readonly
80
+ class="no-pointer"></v-text-field>
81
+ </v-col>
82
+ </v-row>
83
+ </v-col>
84
+ <v-col cols="12" md="4" lg="4" class="mt-2">
85
+ <v-row no-gutters>
86
+ <InputLabel class="text-capitalize font-weight-bold" title="State" />
87
+ <v-col cols="12">
88
+ <v-text-field :model-value="siteData?.address?.state" density="compact" readonly
89
+ class="no-pointer"></v-text-field>
90
+ </v-col>
91
+ </v-row>
92
+ </v-col>
93
+ <v-col cols="12" md="4" lg="4" class="mt-2">
94
+ <v-row no-gutters>
95
+ <InputLabel class="text-capitalize font-weight-bold" title="Country" />
96
+ <v-col cols="12">
97
+ <v-text-field :model-value="siteData?.address?.country" density="compact" readonly
98
+ class="no-pointer"></v-text-field>
99
+ </v-col>
100
+ </v-row>
101
+ </v-col>
102
+ <v-col cols="12" md="4" lg="4" class="mt-2">
103
+ <v-row no-gutters>
104
+ <InputLabel class="text-capitalize font-weight-bold" title="Postal Code" />
105
+ <v-col cols="12">
106
+ <v-text-field :model-value="siteData?.address?.postalCode" density="compact" readonly
107
+ class="no-pointer"></v-text-field>
108
+ </v-col>
109
+ </v-row>
110
+ </v-col>
111
+ </v-row>
112
+ </v-form>
113
+ </template>
114
+
115
+ <template v-else-if="tab === 'residents'">
116
+ <v-card width="100%">
117
+ <v-expansion-panels v-model="panels" variant="accordion" >
118
+
119
+ <!-- Residents -->
120
+ <v-expansion-panel value="residents">
121
+ <v-expansion-panel-title class="bg-primary">
122
+ Residents ({{ unitResidentsArray.length }})
123
+ </v-expansion-panel-title>
124
+
125
+ <v-expansion-panel-text>
126
+ <template v-if="unitResidentsArray.length" v-for="resident, residentIndex in unitResidentsArray" :key="residentIndex">
127
+ <UnitPersonCard
128
+ :person="resident" :readOnly="true" />
129
+ <v-divider v-if="(residentIndex + 1) !== unitResidentsArray.length" thickness="2" color="primary" class="my-4" />
130
+ </template>
131
+ <p v-else class="font-weight-semibold text-caption ml-2 mt-5">
132
+ **No residents to display**
133
+ </p>
134
+ </v-expansion-panel-text>
135
+ </v-expansion-panel>
136
+
137
+ <!-- Tenants -->
138
+ <v-expansion-panel value="tenants">
139
+ <v-expansion-panel-title class="bg-primary">
140
+ Tenants ({{ unitTenantsArray.length }})
141
+ </v-expansion-panel-title>
142
+
143
+ <v-expansion-panel-text>
144
+ <template v-if="unitTenantsArray.length" v-for="(tenant, tenantIndex) in unitTenantsArray" :key="tenant._id || tenantIndex">
145
+ <UnitPersonCard :person="tenant" :readOnly="true" />
146
+ <v-divider v-if="(tenantIndex + 1) !== unitTenantsArray.length" thickness="2" color="primary" class="my-4" />
147
+ </template>
148
+ <p v-else class=" font-weight-semibold text-caption ml-2 mt-5">
149
+ **No tenants to display**
150
+ </p>
151
+ </v-expansion-panel-text>
152
+ </v-expansion-panel>
153
+
154
+ <!-- Guests -->
155
+ <v-expansion-panel value="guests">
156
+ <v-expansion-panel-title class="bg-primary">
157
+ Guests ({{ unitGuestsArray.length }})
158
+ </v-expansion-panel-title>
159
+
160
+ <v-expansion-panel-text>
161
+ <template v-if="unitGuestsArray.length" v-for="(guest, guestIndex) in unitGuestsArray" :key="guest._id || guestIndex">
162
+ <UnitPersonCard :person="guest" :readOnly="true" />
163
+ <v-divider v-if="(guestIndex + 1) !== unitGuestsArray.length" thickness="2" color="primary" class="my-4" />
164
+ </template>
165
+ <p v-else class=" font-weight-semibold text-caption ml-2 mt-5">
166
+ **No guests to display**
167
+ </p>
168
+ </v-expansion-panel-text>
169
+ </v-expansion-panel>
170
+
171
+ </v-expansion-panels>
172
+ </v-card>
173
+ </template>
174
+ <template v-else-if="tab === 'vehicles'">
175
+ <p class="font-weight-semibold text-caption ml-2 mt-5">
176
+ **No vehicles to display**
177
+ </p>
178
+ </template>
179
+
180
+ <template v-else-if="tab === 'others'">
181
+ <v-row>
182
+ <!-- <v-col cols="12" md="6" class="mt-2">
183
+ <v-row no-gutters>
184
+ <InputLabel class="text-capitalize font-weight-bold" title="Owner" />
185
+ <v-col cols="12">
186
+ <v-autocomplete v-model="buildingUnit.ownerName" v-model:search="ownerNameInput" :items="peopleItems"
187
+ item-title="name" return-object density="comfortable">
188
+ </v-autocomplete>
58
189
  </v-col>
59
190
  </v-row>
60
- </v-col>
61
-
191
+ </v-col> -->
62
192
 
63
193
  <v-col cols="12">
64
194
  <v-row>
@@ -83,7 +213,6 @@
83
213
  </v-row>
84
214
  </v-col>
85
215
 
86
-
87
216
  <v-col cols="12">
88
217
  <v-row>
89
218
  <v-col cols="12" md="6" class="mt-2">
@@ -106,65 +235,52 @@
106
235
 
107
236
  </v-row>
108
237
  </v-col>
109
-
110
-
111
- <v-col cols="12" md="6" class="mt-2">
112
- <v-row no-gutters>
113
- <InputLabel class="text-capitalize font-weight-bold" title="Category" required />
114
- <v-col cols="12">
115
- <v-autocomplete v-model="buildingUnit.category" :items="unitCategories" density="comfortable"
116
- :rules="[requiredRule]"></v-autocomplete>
117
- </v-col>
118
- </v-row>
119
- </v-col>
120
-
121
- <v-col cols="12">
122
- <v-row>
123
- <v-col cols="6" md="6" class="mt-2">
124
- <v-row no-gutters>
125
- <InputLabel class="text-capitalize font-weight-bold" title="Level" required />
126
- <v-col cols="12">
127
- <v-select v-model="buildingUnit.level" :items="buildingLevels" density="comfortable"
128
- :rules="[requiredRule]"></v-select>
129
- </v-col>
130
- </v-row>
131
- </v-col>
132
- </v-row>
133
- </v-col>
134
-
135
- <v-col cols="12" class="mt-5">
136
- <InputLabel class="text-capitalize" title="Upload Files (Lease of Contract, Cert. of Occupancy)" />
137
- <InputFileV2 v-model="buildingUnit.buildingUnitFiles" :multiple="false" :max-length="10"
138
- title="Upload PDF Files" accept="application/pdf" />
139
- </v-col>
140
-
141
- <v-col cols="12" class="my-2">
142
- <v-row no-gutters>
143
- <v-col cols="12" class="text-center">
144
- <span class="text-none text-subtitle-2 font-weight-medium text-error">
145
- {{ message }}
146
- </span>
147
- </v-col>
148
- </v-row>
149
- </v-col>
150
238
  </v-row>
151
- </v-form>
239
+ </template>
152
240
  </v-card-text>
153
241
 
154
- <v-toolbar density="compact">
242
+ <v-col cols="12" class="my-2">
155
243
  <v-row no-gutters>
156
- <v-col cols="6">
157
- <v-btn tile block variant="text" class="text-none" size="48" @click="cancel">
158
- Cancel
159
- </v-btn>
244
+ <v-col cols="12" class="text-center">
245
+ <span class="text-none text-subtitle-2 font-weight-medium text-error">
246
+ {{ message }}
247
+ </span>
160
248
  </v-col>
249
+ </v-row>
250
+ </v-col>
161
251
 
162
- <v-col cols="6">
163
- <v-btn tile block variant="flat" color="black" class="text-none" size="48" :disabled="!validForm || disable"
164
- @click="submit" :loading="disable">
165
- Submit
252
+ <v-toolbar class="pa-0" density="compact">
253
+ <v-row no-gutters>
254
+ <v-col :cols="canUpdateUnit || canDeleteUnit ? 6 : 12" class="pa-0">
255
+ <v-btn block variant="text" class="text-none" size="large" @click="emit('cancel')" height="48">
256
+ Close
166
257
  </v-btn>
167
258
  </v-col>
259
+
260
+ <v-col v-if="canUpdateUnit || canDeleteUnit" cols="6" class="pa-0">
261
+ <v-menu>
262
+ <template #activator="{ props }">
263
+ <v-btn block variant="flat" color="black" class="text-none" height="48" v-bind="props" tile
264
+ :disabled="!canUpdateUnit && !canDeleteUnit">
265
+ More actions
266
+ </v-btn>
267
+ </template>
268
+
269
+ <v-list class="pa-0">
270
+ <v-list-item v-if="canUpdateUnit" @click="submit">
271
+ <v-list-item-title class="text-subtitle-2">
272
+ Save
273
+ </v-list-item-title>
274
+ </v-list-item>
275
+
276
+ <v-list-item v-if="canDeleteUnit" @click="emit('delete-unit')" class="text-red">
277
+ <v-list-item-title class="text-subtitle-2">
278
+ Delete Unit
279
+ </v-list-item-title>
280
+ </v-list-item>
281
+ </v-list>
282
+ </v-menu>
283
+ </v-col>
168
284
  </v-row>
169
285
  </v-toolbar>
170
286
  </v-card>
@@ -186,7 +302,7 @@ const prop = defineProps({
186
302
  ownerName: "",
187
303
  building: "",
188
304
  buildingName: "",
189
- category: "",
305
+ // category: "",
190
306
  block: null,
191
307
  level: 0,
192
308
  status: "active",
@@ -197,6 +313,14 @@ const prop = defineProps({
197
313
  leaseEnd: "",
198
314
  }),
199
315
  },
316
+ canUpdateUnit: {
317
+ type: Boolean,
318
+ default: true,
319
+ },
320
+ canDeleteUnit: {
321
+ type: Boolean,
322
+ default: true,
323
+ }
200
324
  });
201
325
 
202
326
  const buildingUnit = ref({
@@ -208,7 +332,7 @@ const buildingUnit = ref({
208
332
  building: "",
209
333
  buildingName: "",
210
334
  level: 0,
211
- category: "",
335
+ // category: "",
212
336
  block: null,
213
337
  status: "active",
214
338
  buildingUnitFiles: [],
@@ -218,20 +342,27 @@ const buildingUnit = ref({
218
342
  leaseEnd: "",
219
343
  });
220
344
 
345
+
346
+ const siteData = ref<TSite | null>(null);
347
+
221
348
  buildingUnit.value = JSON.parse(JSON.stringify(prop.roomFacility));
222
349
 
223
- const emit = defineEmits(["cancel", "success", "success:create-more"]);
350
+ const emit = defineEmits(["cancel", "success", "success:create-more", "delete-unit"]);
351
+
224
352
 
225
353
  const ownerNameInput = ref("");
354
+ const panels = ref<string[]>(['residents']);
355
+
356
+ const tab = ref('general')
226
357
 
227
358
  const validForm = ref(false);
228
359
 
229
360
  const { getAll } = useBuilding();
230
- const { getAll: getAllPeople } = usePeople();
361
+ const { getPeopleByUnit } = usePeople();
231
362
  const { debounce } = useUtils();
232
363
 
233
364
  const buildings = ref<Record<string, any>[]>([]);
234
- const peopleItems = ref<Array<Record<string, any>>>([]);
365
+ const peopleItems = ref<TPeople[]>([]);
235
366
 
236
367
  const { data: getBuildingReq } = useLazyAsyncData(
237
368
  "get-all-buildings",
@@ -248,15 +379,27 @@ watchEffect(() => {
248
379
  }
249
380
  });
250
381
 
251
- const { data: getUnitPeople, refresh: getUnitPeopleRefresh } = useLazyAsyncData(
382
+ const { data: getUnitPeople, refresh: getUnitPeopleRefresh, pending: getUnitPeoplePending } = useLazyAsyncData(
252
383
  "get-unit-people",
253
- async () => getAllPeople({ limit: 10, site: prop.site, type: "tenant", search: ownerNameInput.value })
384
+ async () => await getPeopleByUnit(buildingUnit.value._id, { limit: 10, site: prop.site })
254
385
  );
255
386
 
256
- watch(getUnitPeople, (newData: any) => {
387
+ watch(getUnitPeople, (newData: TPeople[]) => {
257
388
  peopleItems.value = newData ?? [];
258
389
  });
259
390
 
391
+ const unitResidentsArray = computed(() => {
392
+ return peopleItems.value.filter((person) => person?.type === 'resident');
393
+ });
394
+
395
+ const unitTenantsArray = computed(() => {
396
+ return peopleItems.value.filter((person) => person?.type === 'tenant');
397
+ });
398
+
399
+ const unitGuestsArray = computed(() => {
400
+ return peopleItems.value.filter((person) => person?.type === 'guest');
401
+ });
402
+
260
403
  buildingUnit.value.site = prop.site;
261
404
 
262
405
  const selectedBuilding = computed(() => {
@@ -285,7 +428,7 @@ function arraysAreEqual(a: any[], b: any[]): boolean {
285
428
  const hasChanges = computed(() => {
286
429
  return (
287
430
  prop.roomFacility.name !== buildingUnit.value.name ||
288
- prop.roomFacility.category !== buildingUnit.value.category ||
431
+ // prop.roomFacility.category !== buildingUnit.value.category ||
289
432
  prop.roomFacility.level !== buildingUnit.value.level ||
290
433
  !arraysAreEqual(
291
434
  prop.roomFacility.buildingUnitFiles,
@@ -306,7 +449,7 @@ async function submit() {
306
449
  await updateById(buildingUnit.value._id ?? "", {
307
450
  name: buildingUnit.value.name,
308
451
  level: buildingUnit.value.level,
309
- category: buildingUnit.value.category,
452
+ // category: buildingUnit.value.category,
310
453
  buildingUnitFiles: buildingUnit.value.buildingUnitFiles || [],
311
454
  companyName: buildingUnit.value.companyName,
312
455
  companyRegistrationNumber: buildingUnit.value.companyRegistrationNumber || "",
@@ -338,3 +481,9 @@ watch(ownerNameInput, (newVal) => {
338
481
  debounceSearchOwner();
339
482
  }, { immediate: false });
340
483
  </script>
484
+
485
+ <style lang="scss" scoped>
486
+ .no-pointer {
487
+ pointer-events: none;
488
+ }
489
+ </style>
@@ -57,8 +57,13 @@
57
57
  }}
58
58
  </template>
59
59
  <template #item.closeIn="{ item }">
60
- <v-chip class="text-capitalize" variant="flat" color="primary" pill>
61
- {{ remainingTime[item._id] || "No Status" }}
60
+ <v-chip
61
+ class="text-capitalize"
62
+ variant="flat"
63
+ :color="getCloseInColor(item._id)"
64
+ pill
65
+ >
66
+ {{ remainingTime[item._id] || "00h 00m" }}
62
67
  </v-chip>
63
68
  </template>
64
69
  <template #item.status="{ value }">
@@ -99,6 +104,7 @@
99
104
  <script lang="ts" setup>
100
105
  import { useCleaningSchedulePermission } from "../composables/useCleaningSchedulePermission";
101
106
  import useCleaningSchedules from "../composables/useCleaningSchedules";
107
+ import useUtils from "../composables/useUtils";
102
108
 
103
109
  const props = defineProps({
104
110
  orgId: { type: String, required: true },
@@ -191,18 +197,13 @@ const {
191
197
  );
192
198
 
193
199
  function calculateRemainingTime(
194
- item: Date | string | number | null | undefined
200
+ closeIn: Date | string | number | null | undefined
195
201
  ) {
196
- if (!item) return -1;
197
- const _date = new Date(item);
202
+ if (!closeIn) return -1;
203
+ const _date = new Date(closeIn);
198
204
  if (isNaN(_date.getTime())) return -1;
199
- const creationTime = _date.getTime();
200
- const currentTime = Date.now();
201
- const differenceInMillis = currentTime - creationTime;
202
- const differenceInSeconds = Math.floor(differenceInMillis / 1000);
203
- const desiredDurationInSeconds = 24 * 60 * 60;
204
- const remainingTimeInSeconds = desiredDurationInSeconds - differenceInSeconds;
205
- return remainingTimeInSeconds;
205
+ const remainingMs = _date.getTime() - Date.now();
206
+ return Math.floor(remainingMs / 1000);
206
207
  }
207
208
 
208
209
  const formatTime = (seconds: number) => {
@@ -218,7 +219,7 @@ const formatTime = (seconds: number) => {
218
219
  const updateRemainingTime = () => {
219
220
  items.value.forEach((item) => {
220
221
  const itemId = item._id as string;
221
- const _time = calculateRemainingTime(item.date as string);
222
+ const _time = calculateRemainingTime(item.closeIn as string);
222
223
  remainingSeconds.value[itemId] = _time;
223
224
  remainingTime.value[itemId] = _time <= 0 ? "00h 00m" : formatTime(_time);
224
225
  });
@@ -234,6 +235,41 @@ watchEffect(() => {
234
235
  updateRemainingTime();
235
236
  });
236
237
 
238
+ let _countdownInterval: ReturnType<typeof setInterval> | null = null;
239
+
240
+ onMounted(() => {
241
+ _countdownInterval = setInterval(updateRemainingTime, 60_000);
242
+
243
+ try {
244
+ const { $socket } = useNuxtApp() as any;
245
+ if ($socket) {
246
+ $socket.emit("join:cleaning-schedule", props.site);
247
+
248
+ $socket.on("cleaning-schedule:updated", (payload: { siteId: string }) => {
249
+ if (payload?.siteId !== props.site) return;
250
+ getCleanerChecklistRefresh();
251
+ });
252
+
253
+ $socket.on("cleaning-schedule:expired", () => {
254
+ getCleanerChecklistRefresh();
255
+ });
256
+ }
257
+ } catch (_) {}
258
+ });
259
+
260
+ onUnmounted(() => {
261
+ if (_countdownInterval) clearInterval(_countdownInterval);
262
+
263
+ try {
264
+ const { $socket } = useNuxtApp() as any;
265
+ if ($socket) {
266
+ $socket.emit("leave:cleaning-schedule", props.site);
267
+ $socket.off("cleaning-schedule:updated");
268
+ $socket.off("cleaning-schedule:expired");
269
+ }
270
+ } catch (_) {}
271
+ });
272
+
237
273
  async function downloadItem(item: any) {
238
274
  const id = item?._id;
239
275
  if (!id) return;
@@ -248,6 +284,14 @@ async function downloadItem(item: any) {
248
284
  }
249
285
  }
250
286
 
287
+ const selectedScheduleStatus = useState<string>(
288
+ "selectedScheduleStatus",
289
+ () => ""
290
+ );
291
+
292
+ const isItemClosed = (item: any): boolean =>
293
+ String(item?.status ?? "").toLowerCase() === "closed";
294
+
251
295
  function onRowClick(data: any) {
252
296
  const item = data?.item ?? data;
253
297
  const id = item?._id || item?.id || item?.areaId;
@@ -260,6 +304,9 @@ function onRowClick(data: any) {
260
304
  return;
261
305
  }
262
306
 
307
+ // Store status so nested pages know whether the schedule is closed
308
+ selectedScheduleStatus.value = String(item?.status ?? "");
309
+
263
310
  if (props.type === "toilet") {
264
311
  const path = `/${props.orgId}/${props.site}/toilet-checklist/${id}`;
265
312
  navigateTo(path);
@@ -3,9 +3,9 @@
3
3
  <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
4
4
  <span> {{ promptTitle }}</span>
5
5
 
6
- <span v-if="message" class="text-error mt-2">
6
+ <div v-if="message" class="text-error mt-2">
7
7
  {{ message }} Do you want to delete anyway?
8
- </span>
8
+ </div>
9
9
  </v-card-text>
10
10
 
11
11
  <v-toolbar class="pa-0" density="compact">
@@ -19,7 +19,7 @@
19
19
 
20
20
  <v-toolbar class="pa-0" density="compact">
21
21
  <v-row no-gutters>
22
- <v-col cols="6" class="pa-0">
22
+ <v-col :cols="(canUpdate || canDelete) ? 6 : 12" class="pa-0">
23
23
  <v-btn
24
24
  block
25
25
  variant="text"
@@ -32,7 +32,7 @@
32
32
  </v-btn>
33
33
  </v-col>
34
34
 
35
- <v-col cols="6" class="pa-0" >
35
+ <v-col v-if="canUpdate || canDelete" cols="6" class="pa-0" >
36
36
  <v-menu contained>
37
37
  <template #activator="{ props }">
38
38
  <v-btn