@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 1.10.5
10
+
11
+ ### Patch Changes
12
+
13
+ - eaec446: Update changes for March 6, 2024
14
+
3
15
  ## 1.10.4
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <v-dialog :model-value="modelValue" width="450" persistent>
3
+ <v-card width="100%">
4
+ <v-toolbar density="compact" class="pl-4">
5
+ <span class="font-weight-medium text-h5">Delete Card</span>
6
+ </v-toolbar>
7
+
8
+ <v-card-text>
9
+ <p class="text-subtitle-2 text-center mb-4">
10
+ Are you sure you want to delete this card? This action cannot be
11
+ undone.
12
+ </p>
13
+
14
+ <v-form v-model="validForm" :disabled="loading">
15
+ <InputLabel class="text-capitalize font-weight-bold" title="Remarks" required />
16
+ <v-textarea
17
+ v-model="remarks"
18
+ placeholder="Enter remarks..."
19
+ persistent-placeholder
20
+ rows="3"
21
+ auto-grow
22
+ hide-details="auto"
23
+ :rules="[requiredRule]"
24
+ />
25
+ </v-form>
26
+
27
+ <v-row v-if="error" no-gutters justify="center" class="mt-3">
28
+ <span class="text-caption text-error text-center">{{ error }}</span>
29
+ </v-row>
30
+ </v-card-text>
31
+
32
+ <v-toolbar density="compact">
33
+ <v-row no-gutters>
34
+ <v-col cols="6">
35
+ <v-btn
36
+ tile
37
+ block
38
+ size="48"
39
+ variant="text"
40
+ class="text-none"
41
+ :disabled="loading"
42
+ @click="handleClose"
43
+ >
44
+ Close
45
+ </v-btn>
46
+ </v-col>
47
+ <v-col cols="6">
48
+ <v-btn
49
+ tile
50
+ block
51
+ size="48"
52
+ color="black"
53
+ variant="flat"
54
+ class="text-none"
55
+ :loading="loading"
56
+ :disabled="!validForm || loading"
57
+ @click="handleConfirm"
58
+ >
59
+ Delete Card
60
+ </v-btn>
61
+ </v-col>
62
+ </v-row>
63
+ </v-toolbar>
64
+ </v-card>
65
+ </v-dialog>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ const props = defineProps({
70
+ modelValue: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ loading: {
75
+ type: Boolean,
76
+ default: false,
77
+ },
78
+ error: {
79
+ type: String,
80
+ default: "",
81
+ },
82
+ });
83
+
84
+ const emit = defineEmits<{
85
+ "update:modelValue": [value: boolean];
86
+ confirm: [remarks: string];
87
+ }>();
88
+
89
+ const { requiredRule } = useUtils();
90
+
91
+ const validForm = ref(false);
92
+ const remarks = ref("");
93
+
94
+ watch(
95
+ () => props.modelValue,
96
+ (val) => {
97
+ if (val) remarks.value = "";
98
+ }
99
+ );
100
+
101
+ function handleClose() {
102
+ emit("update:modelValue", false);
103
+ }
104
+
105
+ function handleConfirm() {
106
+ if (!validForm.value) return;
107
+ emit("confirm", remarks.value.trim());
108
+ }
109
+ </script>
@@ -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,4 +1,10 @@
1
1
  <template>
2
+ <AccessCardDetailsDialog
3
+ v-model="historyDialog"
4
+ :card="selectedCardInUnit"
5
+ :site-id="siteId"
6
+ />
7
+
2
8
  <v-dialog :model-value="modelValue" width="450" persistent>
3
9
  <v-card width="100%">
4
10
  <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
@@ -214,9 +220,9 @@
214
220
  Replace Card
215
221
  </v-list-item-title>
216
222
  </v-list-item>
217
- <v-list-item :disabled="selectedCardInUnit === null">
223
+ <v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
218
224
  <v-list-item-title class="text-subtitle-2 cursor-pointer">
219
- Card History
225
+ Card Details
220
226
  </v-list-item-title>
221
227
  </v-list-item>
222
228
  <v-list-item
@@ -268,6 +274,14 @@ const props = defineProps({
268
274
  type: Boolean,
269
275
  default: false,
270
276
  },
277
+ isSelectedCardPhysical: {
278
+ type: Boolean,
279
+ default: false,
280
+ },
281
+ siteId: {
282
+ type: String,
283
+ default: "",
284
+ },
271
285
  });
272
286
 
273
287
  const emit = defineEmits<{
@@ -277,6 +291,8 @@ const emit = defineEmits<{
277
291
  delete: [];
278
292
  }>();
279
293
 
294
+ const historyDialog = ref(false);
295
+
280
296
  const isSelectedCardDeletable = computed(() => {
281
297
  if (!props.selectedCardInUnit?._id) return false;
282
298
  const id = props.selectedCardInUnit._id;
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12" class="mb-2">
4
+ <v-row no-gutters align="center" justify="space-between">
5
+ <v-row no-gutters class="ga-2">
6
+ <v-btn
7
+ class="text-none"
8
+ rounded="pill"
9
+ variant="tonal"
10
+ size="large"
11
+ :disabled="!items.length"
12
+ >
13
+ Print All
14
+ </v-btn>
15
+ <v-btn
16
+ class="text-none"
17
+ rounded="pill"
18
+ variant="tonal"
19
+ size="large"
20
+ :disabled="!selected.length"
21
+ >
22
+ Generate QR Code
23
+ </v-btn>
24
+ <v-btn
25
+ class="text-none"
26
+ rounded="pill"
27
+ variant="tonal"
28
+ size="large"
29
+ :disabled="!selected.length"
30
+ >
31
+ Print QR Code
32
+ </v-btn>
33
+ </v-row>
34
+
35
+ <v-row no-gutters class="ga-2" justify="end" style="max-width: 420px">
36
+ <v-select
37
+ v-model="typeFilter"
38
+ :items="typeOptions"
39
+ placeholder="Type"
40
+ variant="outlined"
41
+ density="comfortable"
42
+ hide-details
43
+ clearable
44
+ style="max-width: 160px"
45
+ />
46
+ <v-text-field
47
+ v-model="searchText"
48
+ placeholder="Search Card..."
49
+ variant="outlined"
50
+ density="comfortable"
51
+ clearable
52
+ hide-details
53
+ style="max-width: 240px"
54
+ />
55
+ </v-row>
56
+ </v-row>
57
+ </v-col>
58
+
59
+ <v-col cols="12">
60
+ <v-card
61
+ width="100%"
62
+ variant="outlined"
63
+ border="thin"
64
+ rounded="lg"
65
+ :loading="loading"
66
+ >
67
+ <v-toolbar density="compact" color="grey-lighten-4">
68
+ <template #prepend>
69
+ <v-btn fab icon density="comfortable" @click="fetchItems">
70
+ <v-icon>mdi-refresh</v-icon>
71
+ </v-btn>
72
+ </template>
73
+
74
+ <template #append>
75
+ <v-row no-gutters justify="end" align="center">
76
+ <span class="mr-2 text-caption text-fontgray">
77
+ {{ pageRange }}
78
+ </span>
79
+ <local-pagination
80
+ v-model="page"
81
+ :length="pages"
82
+ @update:value="fetchItems"
83
+ />
84
+ </v-row>
85
+ </template>
86
+ </v-toolbar>
87
+
88
+ <v-data-table
89
+ v-model="selected"
90
+ :headers="tableHeaders"
91
+ :items="items"
92
+ item-value="_id"
93
+ items-per-page="10"
94
+ fixed-header
95
+ hide-default-footer
96
+ show-select
97
+ style="max-height: calc(100vh - 200px)"
98
+ >
99
+ <template #item.card="{ item }">
100
+ <v-row no-gutters align="center" class="ga-2">
101
+ <v-icon size="18" color="grey-darken-1">mdi-card-account-details</v-icon>
102
+ <span>{{ item.hid ?? "N/A" }}</span>
103
+ </v-row>
104
+ </template>
105
+
106
+ <template #item.qrCode="{ item }">
107
+ <v-icon
108
+ :color="item.qrCode ? 'success' : 'grey-lighten-1'"
109
+ size="22"
110
+ >
111
+ {{ item.qrCode ? "mdi-check-circle" : "mdi-circle-outline" }}
112
+ </v-icon>
113
+ </template>
114
+ </v-data-table>
115
+ </v-card>
116
+ </v-col>
117
+
118
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
119
+ </v-row>
120
+ </template>
121
+
122
+ <script setup lang="ts">
123
+ definePageMeta({
124
+ middleware: ["01-auth", "02-org"],
125
+ memberOnly: true,
126
+ });
127
+
128
+ const tableHeaders = [
129
+ { title: "Card", key: "card", sortable: false },
130
+ { title: "Location", key: "location", sortable: false },
131
+ { title: "Card No.", key: "cardNo", sortable: false },
132
+ { title: "HID", key: "hid", sortable: false },
133
+ { title: "QR Code", key: "qrCode", sortable: false },
134
+ ];
135
+
136
+ const typeOptions = ["Physical", "Non-Physical"];
137
+
138
+ const route = useRoute();
139
+ const siteId = computed(() => route.params.site as string);
140
+ const orgId = computed(() => route.params.org as string);
141
+
142
+ const page = ref(1);
143
+ const pages = ref(0);
144
+ const pageRange = ref("-- - -- of --");
145
+
146
+ const message = ref("");
147
+ const messageSnackbar = ref(false);
148
+ const messageColor = ref("");
149
+
150
+ const items = ref<Record<string, any>[]>([]);
151
+ const selected = ref<string[]>([]);
152
+ const searchText = ref("");
153
+ const typeFilter = ref<string | null>(null);
154
+
155
+ const {
156
+ data: qrTaggingReq,
157
+ refresh: fetchItems,
158
+ status: fetchStatus,
159
+ } = useLazyAsyncData(
160
+ "get-qr-tagging",
161
+ () => {
162
+ // TODO: wire up QR tagging API
163
+ return Promise.resolve({ data: { items: [], pages: 0, pageRange: "0 - 0 of 0" } });
164
+ },
165
+ { watch: [page, searchText, typeFilter] }
166
+ );
167
+
168
+ const loading = computed(() => fetchStatus.value === "pending");
169
+
170
+ watchEffect(() => {
171
+ if (qrTaggingReq.value?.data) {
172
+ items.value = qrTaggingReq.value.data.items;
173
+ pages.value = qrTaggingReq.value.data.pages;
174
+ pageRange.value = qrTaggingReq.value.data.pageRange;
175
+ }
176
+ });
177
+
178
+ function showMessage(msg: string, color: string) {
179
+ message.value = msg;
180
+ messageColor.value = color;
181
+ messageSnackbar.value = true;
182
+ }
183
+ </script>
@@ -111,6 +111,8 @@
111
111
  :can-replace-access-card="canReplaceAccessCard"
112
112
  :can-delete-access-card="canDeleteAccessCard"
113
113
  :is-selected-card-assigned-physical="isSelectedCardAssignedPhysical"
114
+ :is-selected-card-physical="isSelectedCardPhysical"
115
+ :site-id="siteId"
114
116
  @replace="openReplaceDialog()"
115
117
  @delete="openDeleteDialog()"
116
118
  />
@@ -129,61 +131,12 @@
129
131
  </v-dialog>
130
132
 
131
133
  <!-- Delete Dialog -->
132
- <v-dialog
134
+ <AccessCardDeleteDialog
133
135
  v-model="confirmDialog"
134
136
  :loading="deleteLoading"
135
- width="450"
136
- persistent
137
- >
138
- <v-card width="100%">
139
- <v-toolbar density="compact" class="pl-4">
140
- <span class="font-weight-medium text-h5">Delete Card</span>
141
- </v-toolbar>
142
- <v-card-text>
143
- <p class="text-subtitle-2 text-center">
144
- Are you sure you want to delete this card? This action cannot be
145
- undone.
146
- </p>
147
-
148
- <v-row v-if="message" no-gutters justify="center" class="mt-4">
149
- <span class="text-caption text-error text-center">
150
- {{ message }}
151
- </span>
152
- </v-row></v-card-text
153
- >
154
- <v-toolbar density="compact">
155
- <v-row no-gutters>
156
- <v-col cols="6">
157
- <v-btn
158
- tile
159
- block
160
- size="48"
161
- variant="text"
162
- class="text-none"
163
- @click="confirmDialog = false"
164
- :disabled="deleteLoading"
165
- >
166
- Close
167
- </v-btn>
168
- </v-col>
169
- <v-col cols="6">
170
- <v-btn
171
- tile
172
- block
173
- size="48"
174
- color="black"
175
- variant="flat"
176
- class="text-none"
177
- @click="handleDeleteCard"
178
- :loading="deleteLoading"
179
- >
180
- Delete Card
181
- </v-btn>
182
- </v-col>
183
- </v-row></v-toolbar
184
- >
185
- </v-card>
186
- </v-dialog>
137
+ :error="deleteError"
138
+ @confirm="handleDeleteCard"
139
+ />
187
140
 
188
141
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
189
142
  </v-row>
@@ -276,6 +229,7 @@ const createDialog = ref(false);
276
229
  const assignDialog = ref(false);
277
230
  const previewDialog = ref(false);
278
231
  const deleteLoading = ref(false);
232
+ const deleteError = ref("");
279
233
  const confirmDialog = ref(false);
280
234
  const searchText = ref("");
281
235
  const replaceDialog = ref(false);
@@ -300,6 +254,14 @@ const isSelectedCardAssignedPhysical = computed(() =>
300
254
  ) ?? false
301
255
  );
302
256
 
257
+ const isSelectedCardPhysical = computed(() =>
258
+ ["available", "assigned", "replaced", "deleted"].some((state) =>
259
+ selectedCard.value?.[state]?.physical?.some(
260
+ (c: any) => c._id === selectedCardInUnit.value?._id
261
+ )
262
+ )
263
+ );
264
+
303
265
  const { getUserTypeAccessCards, deleteCard: _deleteCard } = useAccessManagement();
304
266
 
305
267
  const statsRef = ref<{ refresh: () => void } | null>(null);
@@ -369,7 +331,7 @@ function tableRowClickHandler(_: any, data: any) {
369
331
 
370
332
  function openDeleteDialog() {
371
333
  confirmDialog.value = true;
372
- message.value = "";
334
+ deleteError.value = "";
373
335
  }
374
336
 
375
337
  function openReplaceDialog() {
@@ -384,17 +346,20 @@ function successReplace() {
384
346
  showMessage("Access card replaced successfully!", "success");
385
347
  }
386
348
 
387
- async function handleDeleteCard() {
349
+ async function handleDeleteCard(remarks: string) {
388
350
  deleteLoading.value = true;
389
351
  try {
390
- await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "" });
352
+ await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "", remarks });
391
353
  await getCards();
392
354
  statsRef.value?.refresh();
393
355
  selectedCardId.value = null;
394
356
  confirmDialog.value = false;
395
357
  previewDialog.value = false;
358
+ showMessage("Access card deleted successfully!", "success");
396
359
  } catch (error: any) {
397
- message.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");
398
363
  } finally {
399
364
  deleteLoading.value = false;
400
365
  }
@@ -83,6 +83,11 @@ const props = defineProps({
83
83
  }, canDeleteBulletinBoard: {
84
84
  type: Boolean,
85
85
  default: true
86
+ },
87
+ recipients: {
88
+ type: String,
89
+ default: "",
90
+ required: false
86
91
  }
87
92
  })
88
93
 
@@ -158,7 +163,7 @@ const {
158
163
  pending: getAnnouncementPending,
159
164
  } = await useLazyAsyncData(
160
165
  `get-all-announcements-${page.value}`,
161
- () => getAll({ page: page.value, site: siteId, status: status.value }),
166
+ () => getAll({ page: page.value, site: siteId, status: status.value, recipients: props.recipients }),
162
167
  {
163
168
  watch: [page, () => route.query],
164
169
  }