@7365admin1/layer-common 1.10.1 → 1.10.3

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.
@@ -0,0 +1,322 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <TableMain :headers="headers" :items="paginatePlaceholderItem" v-model:search="searchInput"
4
+ :loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
5
+ @refresh="getAnnouncementsRefresh" show-header @update:page="handleUpdatePage" @row-click="handleRowClick"
6
+ @create="handleCreateEvent" :can-create="canCreateBulletinBoard" create-label="Add Announcement">
7
+ <template #extension>
8
+ <v-row no-gutters class="w-100 d-flex flex-column">
9
+ <v-tabs v-model="status" color="primary" :height="40" @update:model-value="toRoute" class="w-100">
10
+ <v-tab v-for="tab in tabOptions" :value="tab.status" :key="tab.status" class="text-capitalize">
11
+ {{ tab.name }}
12
+ </v-tab>
13
+ </v-tabs>
14
+ </v-row>
15
+ </template>
16
+
17
+ <template v-slot:item.createdAt="{ item }">
18
+ <span class="d-flex align-center ga-2">
19
+ <v-icon icon="mdi-calendar-start" color="green" size="20" />
20
+ <span class="text-capitalize">{{ toLocalDate(item.createdAt) || "-" }}</span>
21
+ </span>
22
+
23
+ </template>
24
+
25
+ <template v-slot:item.noExpiration="{ item }">
26
+ <BulletinExpirationChip :value="item?.noExpiration" />
27
+ </template>
28
+
29
+ <template v-slot:item.duration="{ item }">
30
+ <span class="d-flex align-center ga-2">
31
+ <v-icon icon="mdi-calendar-start" color="green" size="20" />
32
+ <span class="text-capitalize">{{ toLocalDate(item.startDate) || "-" }}</span>
33
+ </span>
34
+ <span class="d-flex align-center ga-2">
35
+ <v-icon icon="mdi-calendar-end" color="red" size="20" />
36
+ <span class="text-capitalize">{{ toLocalDate(item.endDate) || "_" }}</span>
37
+ </span>
38
+ </template>
39
+ </TableMain>
40
+
41
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" style="z-index: 3000;" />
42
+
43
+ <v-dialog v-model="dialog.showForm" width="600" persistent>
44
+ <BulletinBoardForm :mode="mode" :site-id="siteId" :active-id="selectedAnnouncementId"
45
+ @close="dialog.showForm = false" @done="handleDone" />
46
+ </v-dialog>
47
+
48
+ <v-dialog v-model="dialog.moreActions" v-if="activeAnnouncementObj && canViewBulletinBoardDetails" width="600"
49
+ persistent>
50
+ <BulletinBoardView :active-announcement="activeAnnouncementObj" :can-delete="canDeleteBulletinBoard"
51
+ :can-update="canUpdateBulletinBoard" @edit="handleEditAnnouncement" @close="dialog.moreActions = false"
52
+ @delete="dialog.deletePrompt = true" />
53
+ </v-dialog>
54
+
55
+ <v-dialog v-model="dialog.deletePrompt" width="450" persistent>
56
+ <CardDeleteConfirmation prompt-title="Are you sure want to delete this announcement?"
57
+ :loading="loading.deletingAnnouncement" @close="dialog.deletePrompt = false"
58
+ @delete="handleProceedDeleteAnnouncement" />
59
+ </v-dialog>
60
+ </v-row>
61
+ </template>
62
+ <script setup lang="ts">
63
+ definePageMeta({
64
+ memberOnly: true,
65
+ })
66
+
67
+ const props = defineProps({
68
+ siteId: {
69
+ type: String,
70
+ required: true
71
+ },
72
+ canCreateBulletinBoard: {
73
+ type: Boolean,
74
+ default: true
75
+ },
76
+ canUpdateBulletinBoard: {
77
+ type: Boolean,
78
+ default: true
79
+ },
80
+ canViewBulletinBoardDetails: {
81
+ type: Boolean,
82
+ default: true
83
+ }, canDeleteBulletinBoard: {
84
+ type: Boolean,
85
+ default: true
86
+ }
87
+ })
88
+
89
+
90
+ const { authenticate } = useLocalAuth();
91
+ authenticate();
92
+
93
+ const route = useRoute();
94
+ const orgId = route.params.org as string;
95
+ const siteId = props.siteId;
96
+ const routeName = (useRoute().name as string) ?? "";
97
+ const { getAll, deleteBulletinById } = useBulletin()
98
+ const { debounce } = useUtils()
99
+
100
+
101
+
102
+ const headers = [
103
+ { title: "Title", value: "title" },
104
+ { title: "Date Created", value: "createdAt", align: "start" },
105
+ { title: "Start Date/End Date", value: "duration", align: "start" },
106
+ { title: "No Expiration", value: "noExpiration", align: "start" },
107
+ { title: "", value: "actions" },
108
+ ];
109
+ const items = ref<TAnnouncement[]>([]);
110
+ const page = ref(1);
111
+ const pages = ref(0);
112
+ const pageRange = ref("-- - -- of --");
113
+ const selectedAnnouncementId = ref<string>("")
114
+ const mode = ref<'add' | 'edit'>('add')
115
+ const status = ref<TAnnouncementStatus>()
116
+ const searchInput = ref('')
117
+
118
+ const dialog = reactive({
119
+ showForm: false,
120
+ moreActions: false,
121
+ viewDetails: false,
122
+ deletePrompt: false,
123
+ })
124
+
125
+ const loading = reactive({
126
+ deletingAnnouncement: false
127
+ })
128
+
129
+ const messageSnackbar = ref(false)
130
+ const message = ref('')
131
+ const messageColor = ref<'success' | 'error' | 'info'>()
132
+
133
+ const activeAnnouncementObj = computed(() => {
134
+ return items.value.find(x => x._id === selectedAnnouncementId.value)
135
+ })
136
+
137
+ const paginatePlaceholderItem = computed(() => {
138
+ const pageSize = 10
139
+ const total = items.value.length
140
+ const currentPage = page.value
141
+
142
+ const start = (currentPage - 1) * pageSize
143
+ const end = start + pageSize
144
+
145
+ const from = total === 0 ? 0 : start + 1
146
+ const to = Math.min(end, total)
147
+ pageRange.value = `${from}-${to} of ${total}`
148
+
149
+ pages.value = Math.ceil(total / pageSize)
150
+
151
+ return items.value.slice(start, end)
152
+ })
153
+
154
+
155
+ const {
156
+ data: getAnnouncementReq,
157
+ refresh: getAnnouncementsRefresh,
158
+ pending: getAnnouncementPending,
159
+ } = await useLazyAsyncData(
160
+ `get-all-announcements-${page.value}`,
161
+ () => getAll({ page: page.value, site: siteId, status: status.value }),
162
+ {
163
+ watch: [page, () => route.query],
164
+ }
165
+ );
166
+
167
+ watch(getAnnouncementReq, (newVal) => {
168
+ items.value = newVal?.items || [];
169
+ pages.value = newVal?.pages || 0;
170
+ pageRange.value = newVal?.pageRange || "-- - -- of --";
171
+ });
172
+
173
+
174
+ function eventStatusFormat(status: any) {
175
+ switch (status) {
176
+ case 'In Progress':
177
+ return {
178
+ color: 'orange',
179
+ text: 'In Progress'
180
+ };
181
+ case 'Active':
182
+ return {
183
+ color: 'green',
184
+ text: status
185
+ }
186
+ case 'Scheduled':
187
+ return {
188
+ color: 'blue',
189
+ text: status
190
+ }
191
+ default:
192
+ return {
193
+ color: 'grey',
194
+ text: status
195
+ }
196
+ }
197
+ }
198
+
199
+ const tabOptions = [
200
+ { name: "Active", status: "active" },
201
+ { name: "Expired", status: "expired" },
202
+ ];
203
+
204
+ function toRoute(status: any) {
205
+ const obj = tabOptions.find((x) => x.status === status);
206
+ if (!obj) return;
207
+ page.value = 1
208
+ navigateTo({
209
+ name: routeName,
210
+ params: {
211
+ org: orgId,
212
+ },
213
+ query: {
214
+ status: obj.status,
215
+ },
216
+ });
217
+ }
218
+
219
+ function toLocalDate(utcString: string) {
220
+ if (!utcString) return ""
221
+ return new Date(utcString).toLocaleString("en-US", {
222
+ year: "numeric",
223
+ month: "2-digit",
224
+ day: "2-digit",
225
+ })
226
+ }
227
+
228
+ function handleRowClick(data: any) {
229
+ selectedAnnouncementId.value = data?.item?._id;
230
+ dialog.moreActions = true;
231
+ }
232
+
233
+ function handleUpdatePage(newPageNum: number) {
234
+ page.value = newPageNum;
235
+ }
236
+
237
+ function handleClose() {
238
+ dialog.showForm = false;
239
+
240
+ }
241
+
242
+ function handleFormDone() {
243
+ getAnnouncementsRefresh();
244
+ dialog.showForm = false;
245
+ }
246
+
247
+ function handleFormCreateMore() {
248
+ getAnnouncementsRefresh();
249
+ }
250
+
251
+ function handleCreateEvent() {
252
+ selectedAnnouncementId.value = ""
253
+ mode.value = 'add'
254
+ dialog.showForm = true;
255
+ }
256
+
257
+ function handleDeleteAnnouncement() {
258
+ dialog.moreActions = false;
259
+ dialog.showForm = false;
260
+ dialog.deletePrompt = true;
261
+ }
262
+
263
+ function handleEditAnnouncement() {
264
+ dialog.moreActions = false
265
+ mode.value = "edit"
266
+ dialog.showForm = true;
267
+ }
268
+
269
+ function showMessage(text: string, type: "error" | 'success' | 'info') {
270
+ messageSnackbar.value = true;
271
+ message.value = text;
272
+ messageColor.value = type
273
+ }
274
+
275
+
276
+
277
+
278
+
279
+ async function handleProceedDeleteAnnouncement() {
280
+ try {
281
+ loading.deletingAnnouncement = true;
282
+ const bulletinId = selectedAnnouncementId.value;
283
+ const res = await deleteBulletinById(bulletinId as string);
284
+ if (res) {
285
+ showMessage("Announcement successfully deleted!", "info");
286
+ await getAnnouncementsRefresh();
287
+ dialog.deletePrompt = false;
288
+ }
289
+ } catch (error: any) {
290
+ const errorMessage = error?.response?._data?.message;
291
+ console.log("[ERROR]", error);
292
+ showMessage(
293
+ errorMessage || "Something went wrong. Please try again later.",
294
+ "error"
295
+ );
296
+ } finally {
297
+ loading.deletingAnnouncement = false;
298
+ }
299
+ }
300
+
301
+
302
+
303
+
304
+ async function handleDone() {
305
+ await getAnnouncementsRefresh()
306
+ dialog.showForm = false
307
+ }
308
+
309
+
310
+ const debounceSearch = debounce(getAnnouncementsRefresh, 500)
311
+
312
+ watch([searchInput], ([search]) => {
313
+ debounceSearch()
314
+ })
315
+
316
+
317
+ onMounted(() => {
318
+ const statusQuery = (useRoute()?.query?.status)
319
+ status.value = (statusQuery === "active" || statusQuery === "expired") ? statusQuery : "active"
320
+ })
321
+
322
+ </script>
@@ -0,0 +1,195 @@
1
+ <template>
2
+ <v-card width="100%" :loading="processing">
3
+ <v-toolbar>
4
+ <v-row no-gutters class="fill-height px-6" align="center">
5
+ <span class="font-weight-bold text-subtitle-1 text-capitalize">
6
+ Preview Announcement
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0 my-3">
12
+ <v-row no-gutters>
13
+ <template v-for="(label, key) in formattedFields" :key="key">
14
+ <v-col cols="12" class="mb-5">
15
+ <v-row no-gutters class="">
16
+ <v-col class="font-weight-bold">{{ label }}:</v-col>
17
+
18
+ <v-col cols="9" class="px-10">
19
+ <span v-if="key === 'content'" v-html="getFieldValue(key)" />
20
+ <span v-else-if="key === 'noExpiration'">
21
+ <BulletinExpirationChip :value="getFieldValue(key) as boolean" />
22
+ </span>
23
+ <span v-else>
24
+ {{ getFieldValue(key) }}
25
+ </span>
26
+ </v-col>
27
+
28
+
29
+ </v-row>
30
+ </v-col>
31
+ </template>
32
+
33
+ <v-col cols="12" v-if="prop.activeAnnouncement?.file?.length > 0" class="mb-5">
34
+ <v-row no-gutters class="font-weight-bold text-subtitle-1">Attachments:</v-row>
35
+ <v-row no-gutters class="">
36
+ <template v-if="videosArray.length > 0">
37
+ <v-col cols="12">Videos ({{ videosArray.length }})</v-col>
38
+ <AttachmentList :list="videosArray" show-checkbox read-only />
39
+ </template>
40
+
41
+ <template v-if="photosArray.length > 0">
42
+ <v-col cols="12" class="mt-1">Photos ({{ photosArray.length }})</v-col>
43
+ <AttachmentList :list="photosArray" show-checkbox read-only />
44
+ </template>
45
+
46
+ <template v-if="filesArray.length > 0">
47
+ <v-col cols="12" class="mt-1">Files ({{ filesArray.length }})</v-col>
48
+ <AttachmentList :list="filesArray" show-checkbox read-only />
49
+ </template>
50
+ </v-row>
51
+ </v-col>
52
+ </v-row>
53
+ </v-card-text>
54
+
55
+ <v-toolbar class="pa-0" density="compact">
56
+ <v-row no-gutters>
57
+ <v-col cols="6" class="pa-0">
58
+ <v-btn block variant="text" height="48" class="text-none" @click="emit('close')">
59
+ Close
60
+ </v-btn>
61
+ </v-col>
62
+
63
+ <v-col cols="6" class="pa-0">
64
+ <v-menu contained>
65
+ <template #activator="{ props }">
66
+ <v-btn block variant="flat" color="black" height="48" class="text-none" tile v-bind="props">
67
+ More actions
68
+ </v-btn>
69
+ </template>
70
+
71
+ <v-list class="pa-0">
72
+ <v-list-item v-if="canUpdate" @click="handleEdit">
73
+ <v-list-item-title class="text-subtitle-2">
74
+ Edit Announcement
75
+ </v-list-item-title>
76
+ </v-list-item>
77
+
78
+ <v-list-item v-if="canDelete" class="text-red" @click="handleDelete">
79
+ <v-list-item-title class="text-subtitle-2">
80
+ Delete Announcement
81
+ </v-list-item-title>
82
+ </v-list-item>
83
+ </v-list>
84
+ </v-menu>
85
+ </v-col>
86
+ </v-row>
87
+ </v-toolbar>
88
+ </v-card>
89
+ </template>
90
+
91
+
92
+ <script setup lang="ts">
93
+
94
+ const prop = defineProps({
95
+ activeAnnouncement: {
96
+ type: Object as PropType<TAnnouncement>,
97
+ required: true
98
+ },
99
+ canUpdate: Boolean,
100
+ canDelete: Boolean,
101
+ })
102
+
103
+ const processing = ref(false)
104
+ const { recipientList } = useBulletin()
105
+ const { getFileUrl, urlToFile } = useFile()
106
+
107
+ const emit = defineEmits(['close', 'edit', 'delete'])
108
+
109
+ /* Field labels */
110
+ const formattedFields: Partial<Record<keyof TAnnouncement, string>> = {
111
+ title: "Title",
112
+ createdAt: "Date Created",
113
+ recipients: "Recipients",
114
+ content: "Content",
115
+ startDate: "Start Date",
116
+ endDate: "End Date",
117
+ noExpiration: "No Expiration",
118
+ }
119
+
120
+ const formattedRecipients = computed(() => {
121
+ const arr = recipientList.filter(x =>
122
+ prop.activeAnnouncement?.recipients?.includes(x.value)
123
+ )
124
+ return arr.map(item => item.title).join(", ")
125
+ })
126
+
127
+ function getFieldValue(key: keyof TAnnouncement) {
128
+ const value = prop.activeAnnouncement?.[key]
129
+
130
+ if (value === null || value === "" || value === undefined) return ""
131
+
132
+ switch (key) {
133
+ case "recipients":
134
+ return formattedRecipients.value
135
+ case "createdAt":
136
+ case "startDate":
137
+ case "endDate":
138
+ return toLocalDate(value as string)
139
+ default:
140
+ return value
141
+ }
142
+ }
143
+
144
+ const rawFileArray = ref<{ file: File, _id: string, preview: boolean }[]>([])
145
+
146
+ const photosArray = getByType('image/')
147
+ const videosArray = getByType('video/')
148
+ const filesArray = getByType('other')
149
+
150
+ function getByType(prefix: 'image/' | 'video/' | 'other') {
151
+ return computed(() => {
152
+ return rawFileArray.value.filter(x => {
153
+ const type = x?.file?.type
154
+ if (typeof type !== 'string') return false
155
+
156
+ if (prefix === 'other') {
157
+ return !type.startsWith('image/') && !type.startsWith('video/')
158
+ }
159
+ return type.startsWith(prefix)
160
+ })
161
+ })
162
+ }
163
+
164
+ function toLocalDate(utcString: string) {
165
+ if (typeof utcString !== 'string') return;
166
+ return new Date(utcString).toLocaleString("en-US", {
167
+ year: "numeric",
168
+ month: "2-digit",
169
+ day: "2-digit",
170
+ })
171
+ }
172
+
173
+ function handleEdit() {
174
+ emit("edit")
175
+ }
176
+
177
+ function handleDelete() {
178
+ emit("delete")
179
+ }
180
+
181
+ watchEffect(async () => {
182
+ const data = prop.activeAnnouncement
183
+ if (!data) return
184
+
185
+ rawFileArray.value = await Promise.all(
186
+ data.file.map(async (item) => {
187
+ const fileUrl = getFileUrl(item._id)
188
+ const file = await urlToFile(fileUrl, item.name)
189
+ return { file, _id: item._id, preview: item.preview ?? false }
190
+ })
191
+ )
192
+ })
193
+
194
+
195
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <v-chip
3
+ size="small"
4
+ :color="value ? 'green' : 'red'"
5
+ :text="value ? 'Yes' : 'No'"
6
+ />
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ const props = defineProps<{
11
+ value: boolean
12
+ }>()
13
+ </script>
@@ -27,8 +27,12 @@
27
27
  import { ref, defineProps, defineEmits } from "vue";
28
28
  import Vue3Signature from "vue3-signature";
29
29
 
30
- defineProps({
30
+ const props = defineProps({
31
31
  modelValue: String,
32
+ hideToast: {
33
+ type: Boolean,
34
+ default: false,
35
+ }
32
36
  });
33
37
 
34
38
  const emit = defineEmits(["update:modelValue"]);
@@ -53,21 +57,29 @@ function showMessage(msg: string, color: string) {
53
57
 
54
58
  function onSave(data: string) {
55
59
  emit("update:modelValue", data);
56
- showMessage("Signature saved successfully.", "success");
60
+ if(!props.hideToast) {
61
+ showMessage("Signature saved successfully.", "success");
62
+ }
57
63
  }
58
64
 
59
65
  function save() {
60
66
  const signatureData = signatureRef.value?.save("image/jpeg");
61
67
  if (signatureData) {
62
68
  emit("update:modelValue", signatureData);
63
- showMessage("Signature saved successfully.", "success");
69
+ if(!props.hideToast) {
70
+ showMessage("Signature saved successfully.", "success");
71
+ }
64
72
  } else {
65
- showMessage("No signature to save.", "error");
73
+ if(!props.hideToast) {
74
+ showMessage("No signature to save.", "error");
75
+ }
66
76
  }
67
77
  }
68
78
 
69
79
  function clear() {
70
80
  signatureRef.value?.clear();
71
- showMessage("Signature cleared.", "success");
81
+ if(!props.hideToast) {
82
+ showMessage("Signature cleared.", "success");
83
+ }
72
84
  }
73
85
  </script>
@@ -272,7 +272,7 @@ const submitting = ref(false);
272
272
 
273
273
  const headers = [
274
274
  { title: "Name", value: "name" },
275
- { title: "Qty", value: "qty" },
275
+ { title: "Available Stock Qty", value: "qty" },
276
276
  { title: "Status", value: "status" },
277
277
  ];
278
278
 
@@ -55,6 +55,22 @@ export default function useAccessManagement() {
55
55
  );
56
56
  }
57
57
 
58
+ function getAllAccessCardsCounts(params: {
59
+ site: string;
60
+ userType: string;
61
+ }) {
62
+ return useNuxtApp().$api<Record<string, any>>(
63
+ `/api/access-management/all-access-cards-counts`,
64
+ {
65
+ method: "GET",
66
+ query: {
67
+ site: params.site,
68
+ userType: params.userType,
69
+ },
70
+ }
71
+ );
72
+ }
73
+
58
74
  function getUserTypeAccessCards(params: {
59
75
  page?: number;
60
76
  limit?: number;
@@ -79,6 +95,59 @@ export default function useAccessManagement() {
79
95
  );
80
96
  }
81
97
 
98
+ function bulkPhysicalAccessCard(params: { site: string; file: File }) {
99
+ const formData = new FormData();
100
+ formData.append("file", params.file);
101
+ return useNuxtApp().$api<Record<string, any>>(
102
+ `/api/access-management/bulk-upload`,
103
+ {
104
+ method: "POST",
105
+ query: { site: params.site },
106
+ body: formData,
107
+ }
108
+ );
109
+ }
110
+
111
+ function getAvailableAccessCards(params: {
112
+ site: string;
113
+ userType: string;
114
+ type: string;
115
+ accessLevel: string;
116
+ liftAccessLevel: string;
117
+ }) {
118
+ return useNuxtApp().$api<Record<string, any>>(
119
+ `/api/access-management/access-and-lift-cards`,
120
+ {
121
+ method: "GET",
122
+ query: {
123
+ site: params.site,
124
+ userType: params.userType,
125
+ type: params.type,
126
+ accessLevel: params.accessLevel,
127
+ liftAccessLevel: params.liftAccessLevel,
128
+ },
129
+ }
130
+ );
131
+ }
132
+
133
+ function assignAccessCard(payload: {
134
+ units: string[];
135
+ quantity: number;
136
+ type: string;
137
+ site: string;
138
+ userType: string;
139
+ accessLevel: string;
140
+ liftAccessLevel: string;
141
+ }) {
142
+ return useNuxtApp().$api<Record<string, any>>(
143
+ `/api/access-management/assign-access-card`,
144
+ {
145
+ method: "POST",
146
+ body: payload,
147
+ }
148
+ );
149
+ }
150
+
82
151
  return {
83
152
  getDoorAccessLevels,
84
153
  getLiftAccessLevels,
@@ -86,5 +155,9 @@ export default function useAccessManagement() {
86
155
  addPhysicalCard,
87
156
  addNonPhysicalCard,
88
157
  getUserTypeAccessCards,
158
+ getAllAccessCardsCounts,
159
+ bulkPhysicalAccessCard,
160
+ assignAccessCard,
161
+ getAvailableAccessCards,
89
162
  };
90
163
  }