@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
@@ -0,0 +1,215 @@
1
+ <template>
2
+ <div>
3
+ <v-skeleton-loader v-if="loading" type="list-item-two-line" width="100%" />
4
+ <template v-else>
5
+ <v-divider class="mb-4" />
6
+ <InputLabel title="Pass Type (Optional)" />
7
+ <v-row no-gutters class="ga-3 mt-1">
8
+ <v-col>
9
+ <v-card
10
+ :variant="passType === 'QR' ? 'tonal' : 'outlined'"
11
+ :color="passType === 'QR' ? 'primary' : undefined"
12
+ class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
13
+ rounded="lg"
14
+ @click="passType = passType === 'QR' ? null : 'QR'"
15
+ >
16
+ <v-icon v-if="passType === 'QR'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
17
+ <v-icon size="32">mdi-qrcode</v-icon>
18
+ <span class="text-subtitle-2 font-weight-bold">QR Pass</span>
19
+ </v-card>
20
+ </v-col>
21
+ <v-col v-if="nfcEnabled">
22
+ <v-card
23
+ :variant="passType === 'NFC' ? 'tonal' : 'outlined'"
24
+ :color="passType === 'NFC' ? 'primary' : undefined"
25
+ class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
26
+ rounded="lg"
27
+ @click="passType = passType === 'NFC' ? null : 'NFC'"
28
+ >
29
+ <v-icon v-if="passType === 'NFC'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
30
+ <v-icon size="32">mdi-nfc</v-icon>
31
+ <span class="text-subtitle-2 font-weight-bold">NFC</span>
32
+ </v-card>
33
+ </v-col>
34
+ </v-row>
35
+
36
+ <!-- QR Pass section -->
37
+ <template v-if="passType === 'QR'">
38
+ <v-alert
39
+ v-if="!hasPrinter"
40
+ type="warning"
41
+ variant="tonal"
42
+ density="compact"
43
+ class="mt-3"
44
+ icon="mdi-printer-alert"
45
+ >
46
+ No printer is configured in the settings. QR Pass may not print.
47
+ </v-alert>
48
+ <template v-else>
49
+ <InputLabel title="Quantity" class="mt-3" />
50
+ <v-text-field
51
+ v-model.number="quantity"
52
+ type="number"
53
+ density="comfortable"
54
+ :min="1"
55
+ hide-details
56
+ />
57
+ </template>
58
+ </template>
59
+
60
+ <!-- NFC Pass section -->
61
+ <template v-if="passType === 'NFC'">
62
+ <div class="mt-3">
63
+ <InputLabel title="Select Cards" />
64
+ <v-select
65
+ v-model="selectedCards"
66
+ :items="cards"
67
+ :loading="cardsLoading"
68
+ item-title="name"
69
+ item-value="_id"
70
+ density="comfortable"
71
+ multiple
72
+ chips
73
+ closable-chips
74
+ hide-details
75
+ placeholder="Select cards..."
76
+ >
77
+ <template #item="{ props: itemProps, item }">
78
+ <v-list-item v-bind="itemProps">
79
+ <template #subtitle>
80
+ <span class="text-caption text-grey">{{ item.raw.cardNumber }}</span>
81
+ </template>
82
+ </v-list-item>
83
+ </template>
84
+ </v-select>
85
+ </div>
86
+
87
+ <div class="mt-3">
88
+ <InputLabel title="Quantity" />
89
+ <v-text-field
90
+ :model-value="selectedCards.length"
91
+ type="number"
92
+ density="comfortable"
93
+ disabled
94
+ hide-details
95
+ />
96
+ </div>
97
+
98
+ <v-row no-gutters class="ga-3 mt-3">
99
+ <v-col>
100
+ <v-btn
101
+ variant="outlined"
102
+ color="primary"
103
+ block
104
+ prepend-icon="mdi-barcode-scan"
105
+ @click="emit('scan:barcode')"
106
+ >
107
+ Scan Via BarCode
108
+ </v-btn>
109
+ </v-col>
110
+ <v-col>
111
+ <v-btn
112
+ variant="outlined"
113
+ color="primary"
114
+ block
115
+ prepend-icon="mdi-camera"
116
+ @click="emit('scan:camera')"
117
+ >
118
+ Scan with Camera
119
+ </v-btn>
120
+ </v-col>
121
+ </v-row>
122
+ </template>
123
+ </template>
124
+ <v-divider class="mt-6" />
125
+ </div>
126
+ </template>
127
+
128
+ <script setup lang="ts">
129
+ const props = defineProps({
130
+ settings: {
131
+ type: Object as PropType<Record<string, any> | null>,
132
+ default: null,
133
+ },
134
+ loading: {
135
+ type: Boolean,
136
+ default: false,
137
+ },
138
+ modelValue: {
139
+ type: String as PropType<string | null>,
140
+ default: null,
141
+ },
142
+ quantity: {
143
+ type: Number as PropType<number | null>,
144
+ default: null,
145
+ },
146
+ cards: {
147
+ type: Array as PropType<string[]>,
148
+ default: () => [],
149
+ },
150
+ });
151
+
152
+ const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "scan:barcode", "scan:camera"]);
153
+
154
+ const { getAll: getAllCards } = useCard();
155
+
156
+ const nfcEnabled = computed(
157
+ () => props.settings?.data?.settings?.nfcPass ?? false
158
+ );
159
+
160
+ const printer = computed(
161
+ () => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null }
162
+ );
163
+
164
+ const hasPrinter = computed(
165
+ () => !!printer.value.vendorId && !!printer.value.productId
166
+ );
167
+
168
+ const passType = computed({
169
+ get: () => props.modelValue,
170
+ set: (val) => emit("update:modelValue", val),
171
+ });
172
+
173
+ const quantity = computed({
174
+ get: () => props.quantity,
175
+ set: (val) => emit("update:quantity", val),
176
+ });
177
+
178
+ const selectedCards = computed({
179
+ get: () => props.cards,
180
+ set: (val) => {
181
+ emit("update:cards", val);
182
+ emit("update:quantity", val.length || null);
183
+ },
184
+ });
185
+
186
+ const cardItems = ref<TCard[]>([]);
187
+ const cardsLoading = ref(false);
188
+
189
+ const cards = computed(() => cardItems.value);
190
+
191
+ async function fetchCards() {
192
+ cardsLoading.value = true;
193
+ try {
194
+ const res = await getAllCards({ assignFilter: "available", limit: 100 });
195
+ cardItems.value = res?.data ?? [];
196
+ } finally {
197
+ cardsLoading.value = false;
198
+ }
199
+ }
200
+
201
+ watch(
202
+ () => passType.value,
203
+ (val) => {
204
+ if (val === "NFC") fetchCards();
205
+ }
206
+ );
207
+ </script>
208
+
209
+ <style scoped>
210
+ .selected-check {
211
+ position: absolute;
212
+ top: 6px;
213
+ right: 6px;
214
+ }
215
+ </style>
@@ -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'])
@@ -138,6 +141,14 @@ watch(input, (newInput) => {
138
141
  else phone.value = prefix + newInput
139
142
  })
140
143
 
144
+ watch(
145
+ () => props.modelValue,
146
+ (val) => {
147
+ phone.value = val || ''
148
+ },
149
+ { immediate: true }
150
+ )
151
+
141
152
 
142
153
  watch(phone, (newVal) => {
143
154
  const prefix = phonePrefix.value
@@ -8,6 +8,13 @@
8
8
  Back
9
9
  </v-btn>
10
10
  </v-col>
11
+ <v-spacer />
12
+ <v-col v-if="isScheduleClosed" cols="auto" class="d-flex align-center">
13
+ <v-chip color="error" variant="tonal" class="text-capitalize">
14
+ <v-icon start>mdi-lock</v-icon>
15
+ Closed – View Only
16
+ </v-chip>
17
+ </v-col>
11
18
  </v-row>
12
19
  <TableHygiene
13
20
  ref="tableHygieneRef"
@@ -22,8 +29,8 @@
22
29
  :loading="loading"
23
30
  :no-data-text="`No checklist found`"
24
31
  :show-header="true"
25
- :can-manage-schedule-tasks="canManageScheduleTasks"
26
- :can-add-remarks="canAddRemarks"
32
+ :can-manage-schedule-tasks="!isScheduleClosed && canManageScheduleTasks"
33
+ :can-add-remarks="!isScheduleClosed && canAddRemarks"
27
34
  @refresh="getUnitCleanerChecklistRefresh"
28
35
  @update:selected="selectedItems = $event"
29
36
  @action-click="handleActionClick"
@@ -113,6 +120,9 @@ const { back } = useUtils();
113
120
  const { canAddRemarks, canManageScheduleTasks } =
114
121
  useCleaningSchedulePermission();
115
122
 
123
+ const selectedScheduleStatus = useState<string>('selectedScheduleStatus', () => '')
124
+ const isScheduleClosed = computed(() => selectedScheduleStatus.value.toLowerCase() === 'closed')
125
+
116
126
  const page = ref<number>(1);
117
127
  const pages = ref<number>(0);
118
128
  const pageRange = ref<string>("-- - -- of --");
@@ -241,6 +251,11 @@ function openCompletionDialog({
241
251
  approvedItems: Array<{ key: string; item: any; action: "approve" }>;
242
252
  lastApprovedKey: string | null;
243
253
  }): void {
254
+ if (isScheduleClosed.value) {
255
+ showMessage("This schedule is closed. No actions can be performed.", "error");
256
+ return;
257
+ }
258
+
244
259
  // If user does not need to add remarks, perform approvals immediately
245
260
  if (!canAddRemarks.value) {
246
261
  // call update directly without opening modal
@@ -283,6 +298,11 @@ async function handleActionClick(data: {
283
298
  item: TFlattenedUnitItem;
284
299
  action: "approve" | "reject";
285
300
  }): Promise<void> {
301
+ if (isScheduleClosed.value) {
302
+ showMessage("This schedule is closed. No actions can be performed.", "error");
303
+ return;
304
+ }
305
+
286
306
  const { item, action } = data;
287
307
 
288
308
  if (!item?.unit || item?.set === undefined) {
@@ -322,9 +342,15 @@ async function handleActionClick(data: {
322
342
  await getUnitCleanerChecklistRefresh();
323
343
  } catch (error: any) {
324
344
  console.error("Error updating unit checklist:", error);
325
- showMessage(error?.message || "Failed to update checklist", "error");
345
+ showMessage(
346
+ error?.data?.message || error?.message || "Failed to update checklist",
347
+ "error"
348
+ );
326
349
  } finally {
327
350
  submitting.value = false;
351
+ if (action === "approve") {
352
+ tableHygieneRef.value?.stopLoadingAction(`${item.unit}_${item.set}`);
353
+ }
328
354
  }
329
355
  }
330
356
 
@@ -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 || "")
@@ -68,6 +68,15 @@
68
68
  <v-spacer />
69
69
 
70
70
  <v-col cols="auto">
71
+ <v-chip
72
+ v-if="isScheduleClosed"
73
+ color="error"
74
+ variant="tonal"
75
+ class="text-capitalize mr-2"
76
+ >
77
+ <v-icon start>mdi-lock</v-icon>
78
+ Closed – View Only
79
+ </v-chip>
71
80
  <v-btn
72
81
  v-if="canViewHistory"
73
82
  class="text-none mr-2"
@@ -86,6 +95,7 @@
86
95
  aria-label="Generate"
87
96
  @click="_generateChecklist"
88
97
  :loading="submitting"
98
+ :disabled="isScheduleClosed"
89
99
  >
90
100
  Generate List
91
101
  </v-btn>
@@ -103,6 +113,15 @@
103
113
  {{ item.acceptedByName || item.acceptedBy || "Accepted" }}
104
114
  </span>
105
115
  </div>
116
+ <div v-else-if="isScheduleClosed">
117
+ <v-chip
118
+ size="small"
119
+ color="error"
120
+ variant="tonal"
121
+ class="text-capitalize"
122
+ >Closed</v-chip
123
+ >
124
+ </div>
106
125
  <div v-else>
107
126
  <v-btn
108
127
  v-if="canManageScheduleTasks"
@@ -176,6 +195,14 @@ const { formatDate, back } = useUtils();
176
195
  const { canGenerateChecklist, canViewHistory, canManageScheduleTasks } =
177
196
  useCleaningSchedulePermission();
178
197
 
198
+ const selectedScheduleStatus = useState<string>(
199
+ "selectedScheduleStatus",
200
+ () => ""
201
+ );
202
+ const isScheduleClosed = computed(
203
+ () => selectedScheduleStatus.value.toLowerCase() === "closed"
204
+ );
205
+
179
206
  const items = ref<Array<Record<string, any>>>([]);
180
207
 
181
208
  const statusFilter = ref<TScheduleAreaStatus>("All");
@@ -313,4 +340,33 @@ async function _generateChecklist() {
313
340
  submitting.value = false;
314
341
  }
315
342
  }
343
+
344
+ onMounted(() => {
345
+ try {
346
+ const { $socket } = useNuxtApp() as any;
347
+ if ($socket) {
348
+ $socket.emit("join:cleaning-area", props.scheduleAreaId);
349
+
350
+ $socket.on(
351
+ "area-checklist:generated",
352
+ (payload: { scheduleId: string; siteId: string }) => {
353
+ if (payload?.scheduleId !== props.scheduleAreaId) return;
354
+ if (typeof getScheduleAreasRefresh === "function") {
355
+ getScheduleAreasRefresh();
356
+ }
357
+ }
358
+ );
359
+ }
360
+ } catch (_) {}
361
+ });
362
+
363
+ onUnmounted(() => {
364
+ try {
365
+ const { $socket } = useNuxtApp() as any;
366
+ if ($socket) {
367
+ $socket.emit("leave:cleaning-area", props.scheduleAreaId);
368
+ $socket.off("area-checklist:generated");
369
+ }
370
+ } catch (_) {}
371
+ });
316
372
  </script>