@7365admin1/layer-common 1.10.4 → 1.10.5

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,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.5
4
+
5
+ ### Patch Changes
6
+
7
+ - eaec446: Update changes for March 6, 2024
8
+
3
9
  ## 1.10.4
4
10
 
5
11
  ### 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,133 @@
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 History — {{ 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="!history.length" class="text-center text-grey py-8">
19
+ No history available.
20
+ </div>
21
+
22
+ <v-timeline v-else side="end" density="compact" truncate-line="both">
23
+ <v-timeline-item
24
+ v-for="(item, index) in history"
25
+ :key="index"
26
+ :dot-color="actionColor(item.action)"
27
+ size="small"
28
+ >
29
+ <template #icon>
30
+ <v-icon size="14" color="white">{{ actionIcon(item.action) }}</v-icon>
31
+ </template>
32
+
33
+ <div class="mb-4">
34
+ <div class="d-flex align-center ga-2 mb-1">
35
+ <v-chip
36
+ :color="actionColor(item.action)"
37
+ variant="flat"
38
+ size="x-small"
39
+ class="text-capitalize font-weight-medium"
40
+ >
41
+ {{ actionLabel(item.action) }}
42
+ </v-chip>
43
+ </div>
44
+ <div class="text-body-2">{{ item.performedBy?.name ?? "N/A" }}</div>
45
+ <div class="text-caption text-grey">{{ formatDate(item.date) }}</div>
46
+ </div>
47
+ </v-timeline-item>
48
+ </v-timeline>
49
+ </v-card-text>
50
+ </v-card>
51
+ </v-dialog>
52
+ </template>
53
+
54
+ <script setup lang="ts">
55
+ const props = defineProps({
56
+ modelValue: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
60
+ card: {
61
+ type: Object as PropType<Record<string, any> | null>,
62
+ default: null,
63
+ },
64
+ });
65
+
66
+ const emit = defineEmits<{
67
+ "update:modelValue": [value: boolean];
68
+ }>();
69
+
70
+ const { getCardHistory } = useAccessManagement();
71
+
72
+ const history = ref<Record<string, any>[]>([]);
73
+ const pending = ref(false);
74
+
75
+ watch(
76
+ () => props.modelValue,
77
+ async (val) => {
78
+ if (val && props.card?._id) {
79
+ pending.value = true;
80
+ try {
81
+ const data = await getCardHistory(props.card._id);
82
+ history.value = Array.isArray(data) ? data : [];
83
+ } finally {
84
+ pending.value = false;
85
+ }
86
+ } else {
87
+ history.value = [];
88
+ }
89
+ }
90
+ );
91
+
92
+ function actionLabel(action: string) {
93
+ const map: Record<string, string> = {
94
+ available: "Created",
95
+ assign: "Assigned",
96
+ replace: "Replaced",
97
+ deleted: "Deleted",
98
+ };
99
+ return map[action] ?? action;
100
+ }
101
+
102
+ function actionColor(action: string) {
103
+ const map: Record<string, string> = {
104
+ available: "success",
105
+ assign: "primary",
106
+ replace: "orange",
107
+ deleted: "error",
108
+ };
109
+ return map[action] ?? "grey";
110
+ }
111
+
112
+ function actionIcon(action: string) {
113
+ const map: Record<string, string> = {
114
+ available: "mdi-plus",
115
+ assign: "mdi-account-check",
116
+ replace: "mdi-swap-horizontal",
117
+ deleted: "mdi-delete",
118
+ };
119
+ return map[action] ?? "mdi-circle-small";
120
+ }
121
+
122
+ function formatDate(date: string) {
123
+ if (!date) return "N/A";
124
+ return new Intl.DateTimeFormat("en-GB", {
125
+ day: "2-digit",
126
+ month: "short",
127
+ year: "numeric",
128
+ hour: "2-digit",
129
+ minute: "2-digit",
130
+ hour12: true,
131
+ }).format(new Date(date));
132
+ }
133
+ </script>
@@ -1,4 +1,9 @@
1
1
  <template>
2
+ <AccessCardHistoryDialog
3
+ v-model="historyDialog"
4
+ :card="selectedCardInUnit"
5
+ />
6
+
2
7
  <v-dialog :model-value="modelValue" width="450" persistent>
3
8
  <v-card width="100%">
4
9
  <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
@@ -214,7 +219,7 @@
214
219
  Replace Card
215
220
  </v-list-item-title>
216
221
  </v-list-item>
217
- <v-list-item :disabled="selectedCardInUnit === null">
222
+ <v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
218
223
  <v-list-item-title class="text-subtitle-2 cursor-pointer">
219
224
  Card History
220
225
  </v-list-item-title>
@@ -268,6 +273,10 @@ const props = defineProps({
268
273
  type: Boolean,
269
274
  default: false,
270
275
  },
276
+ isSelectedCardPhysical: {
277
+ type: Boolean,
278
+ default: false,
279
+ },
271
280
  });
272
281
 
273
282
  const emit = defineEmits<{
@@ -277,6 +286,8 @@ const emit = defineEmits<{
277
286
  delete: [];
278
287
  }>();
279
288
 
289
+ const historyDialog = ref(false);
290
+
280
291
  const isSelectedCardDeletable = computed(() => {
281
292
  if (!props.selectedCardInUnit?._id) return false;
282
293
  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,7 @@
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"
114
115
  @replace="openReplaceDialog()"
115
116
  @delete="openDeleteDialog()"
116
117
  />
@@ -129,61 +130,12 @@
129
130
  </v-dialog>
130
131
 
131
132
  <!-- Delete Dialog -->
132
- <v-dialog
133
+ <AccessCardDeleteDialog
133
134
  v-model="confirmDialog"
134
135
  :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>
136
+ :error="deleteError"
137
+ @confirm="handleDeleteCard"
138
+ />
187
139
 
188
140
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
189
141
  </v-row>
@@ -276,6 +228,7 @@ const createDialog = ref(false);
276
228
  const assignDialog = ref(false);
277
229
  const previewDialog = ref(false);
278
230
  const deleteLoading = ref(false);
231
+ const deleteError = ref("");
279
232
  const confirmDialog = ref(false);
280
233
  const searchText = ref("");
281
234
  const replaceDialog = ref(false);
@@ -300,6 +253,14 @@ const isSelectedCardAssignedPhysical = computed(() =>
300
253
  ) ?? false
301
254
  );
302
255
 
256
+ const isSelectedCardPhysical = computed(() =>
257
+ ["available", "assigned", "replaced", "deleted"].some((state) =>
258
+ selectedCard.value?.[state]?.physical?.some(
259
+ (c: any) => c._id === selectedCardInUnit.value?._id
260
+ )
261
+ )
262
+ );
263
+
303
264
  const { getUserTypeAccessCards, deleteCard: _deleteCard } = useAccessManagement();
304
265
 
305
266
  const statsRef = ref<{ refresh: () => void } | null>(null);
@@ -369,7 +330,7 @@ function tableRowClickHandler(_: any, data: any) {
369
330
 
370
331
  function openDeleteDialog() {
371
332
  confirmDialog.value = true;
372
- message.value = "";
333
+ deleteError.value = "";
373
334
  }
374
335
 
375
336
  function openReplaceDialog() {
@@ -384,17 +345,17 @@ function successReplace() {
384
345
  showMessage("Access card replaced successfully!", "success");
385
346
  }
386
347
 
387
- async function handleDeleteCard() {
348
+ async function handleDeleteCard(remarks: string) {
388
349
  deleteLoading.value = true;
389
350
  try {
390
- await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "" });
351
+ await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "", remarks });
391
352
  await getCards();
392
353
  statsRef.value?.refresh();
393
354
  selectedCardId.value = null;
394
355
  confirmDialog.value = false;
395
356
  previewDialog.value = false;
396
357
  } catch (error: any) {
397
- message.value = error?.response?._data?.message || "Failed to delete card";
358
+ deleteError.value = error?.response?._data?.message || "Failed to delete card";
398
359
  } finally {
399
360
  deleteLoading.value = false;
400
361
  }
@@ -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
  }
@@ -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