@7365admin1/layer-common 1.10.10 → 1.11.0

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.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 167c359: realease for ir download latest
8
+
3
9
  ## 1.10.10
4
10
 
5
11
  ### Patch Changes
@@ -655,7 +655,7 @@ watch(() => card.value.showAssign, async (newVal) => {
655
655
  });
656
656
  buildingsData.value = response.items || [];
657
657
  buildingItems.value = buildingsData.value.map((building: any) => ({
658
- name: building.name,
658
+ name: building.block,
659
659
  value: building._id,
660
660
  }));
661
661
  } catch (error) {
@@ -22,7 +22,7 @@
22
22
  hide-details="auto"
23
23
  item-title="name"
24
24
  item-value="value"
25
- placeholder="Select building..."
25
+ placeholder="Select block..."
26
26
  persistent-placeholder
27
27
  :rules="[requiredRule]"
28
28
  :loading="buildingLoading"
@@ -98,7 +98,6 @@
98
98
  <InputLabel
99
99
  class="text-capitalize font-weight-bold"
100
100
  title="Access Level"
101
- required
102
101
  />
103
102
  <v-select
104
103
  v-model="form.accessLevel"
@@ -107,7 +106,6 @@
107
106
  hide-details="auto"
108
107
  item-title="name"
109
108
  item-value="no"
110
- :rules="[requiredRule]"
111
109
  :loading="levelsLoading"
112
110
  placeholder="Select access level..."
113
111
  />
@@ -118,7 +116,6 @@
118
116
  <InputLabel
119
117
  class="text-capitalize font-weight-bold"
120
118
  title="Lift Access Level"
121
- required
122
119
  />
123
120
  <v-select
124
121
  v-model="form.liftAccessLevel"
@@ -127,7 +124,6 @@
127
124
  hide-details="auto"
128
125
  item-title="name"
129
126
  item-value="no"
130
- :rules="[requiredRule]"
131
127
  :loading="levelsLoading"
132
128
  placeholder="Select lift access level..."
133
129
  />
@@ -245,8 +241,9 @@ const typeItems = [
245
241
  { label: "Non Physical Access Card", value: "QRCODE" },
246
242
  ];
247
243
 
248
- const accessLevelItems = ref<{ name: string; no: string }[]>([]);
249
- const liftAccessLevelItems = ref<{ name: string; no: string }[]>([]);
244
+ const NO_SELECTION = { name: "No Selection", no: null };
245
+ const accessLevelItems = ref<{ name: string; no: string | null }[]>([]);
246
+ const liftAccessLevelItems = ref<{ name: string; no: string | null }[]>([]);
250
247
  const buildingItems = ref<{ name: string; value: string }[]>([]);
251
248
  const buildingsData = ref<Record<string, any>[]>([]);
252
249
  const levelItems = ref<{ name: string; value: string }[]>([]);
@@ -268,7 +265,7 @@ function maxQuantityRule(v: number) {
268
265
  }
269
266
 
270
267
  async function fetchAvailableCount() {
271
- if (!form.value.accessLevel || !form.value.liftAccessLevel) {
268
+ if (!form.value.accessLevel && !form.value.liftAccessLevel) {
272
269
  availableCount.value = null;
273
270
  return;
274
271
  }
@@ -315,7 +312,7 @@ onMounted(async () => {
315
312
  if (buildingsResult.status === "fulfilled") {
316
313
  buildingsData.value = buildingsResult.value.items || [];
317
314
  buildingItems.value = buildingsData.value.map((b: any) => ({
318
- name: b.name,
315
+ name: b.block,
319
316
  value: b._id,
320
317
  }));
321
318
  }
@@ -329,8 +326,8 @@ onMounted(async () => {
329
326
  _getDoorAccessLevels(acmUrl),
330
327
  _getLiftAccessLevels(acmUrl),
331
328
  ]);
332
- accessLevelItems.value = doorLevels.data ?? [];
333
- liftAccessLevelItems.value = liftLevels.data ?? [];
329
+ accessLevelItems.value = [NO_SELECTION, ...(doorLevels.data ?? [])];
330
+ liftAccessLevelItems.value = [NO_SELECTION, ...(liftLevels.data ?? [])];
334
331
  } catch {
335
332
  emit(
336
333
  "error",
@@ -423,8 +420,8 @@ async function submit() {
423
420
  type: form.value.type,
424
421
  site: siteId.value,
425
422
  userType: form.value.userType,
426
- accessLevel: form.value.accessLevel ?? "",
427
- liftAccessLevel: form.value.liftAccessLevel ?? "",
423
+ accessLevel: form.value.accessLevel,
424
+ liftAccessLevel: form.value.liftAccessLevel,
428
425
  });
429
426
  emit("success");
430
427
  } catch (error: any) {
@@ -158,7 +158,7 @@
158
158
  <qrcode-vue
159
159
  v-for="qrCode in qrCodesToGenerate"
160
160
  :key="qrCode._id"
161
- :value="qrCode.qrData"
161
+ :value="qrCode.cardNo"
162
162
  :size="150"
163
163
  :data-card-id="qrCode._id"
164
164
  />
@@ -340,7 +340,7 @@ async function generateQrCodes() {
340
340
  qrCodesToGenerate.value = filteredCards.map((card) => ({
341
341
  _id: card._id,
342
342
  cardNo: card.cardNo,
343
- qrData: card.qrData,
343
+ qrData: card.cardNo,
344
344
  qrTagCardNo: card.qrTagCardNo ?? "",
345
345
  }));
346
346
 
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div>
3
+ <!-- Chat Messages Skeleton Loader -->
4
+ <v-row no-gutters class="chat-content">
5
+ <v-col
6
+ v-for="(message, index) in chatSkeletons"
7
+ :key="index"
8
+ cols="12"
9
+ class="d-flex my-2"
10
+ :class="message.isRight ? 'justify-end' : 'justify-start'"
11
+ >
12
+ <v-skeleton-loader
13
+ class="rounded-lg border"
14
+ type="article"
15
+ width="400"
16
+ boilerplate
17
+ ></v-skeleton-loader>
18
+ </v-col>
19
+ </v-row>
20
+
21
+ <v-footer class="pa-0 chat-footer" color="background">
22
+ <v-row align="center" justify="space-between" no-gutters class="">
23
+ <v-col cols="9" class="pr-3">
24
+ <v-skeleton-loader
25
+ type="paragraph"
26
+ height="100"
27
+ class="rounded-lg border"
28
+ ></v-skeleton-loader>
29
+ </v-col>
30
+
31
+ <v-col cols="3" class="d-flex justify-end">
32
+ <v-skeleton-loader
33
+ color="background"
34
+ type="button"
35
+ width="150"
36
+ height="150"
37
+ class="rounded-lg"
38
+ ></v-skeleton-loader>
39
+ </v-col>
40
+ </v-row>
41
+ </v-footer>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { ref } from "vue";
47
+
48
+ const chatSkeletons = ref([
49
+ { isRight: false },
50
+ { isRight: true },
51
+ { isRight: false },
52
+ ]);
53
+ </script>
54
+
55
+ <style scoped>
56
+ .chat-content {
57
+ max-height: calc(100vh - (55px + 98px + 100px));
58
+ overflow-y: auto;
59
+ }
60
+
61
+ .chat-footer {
62
+ position: sticky;
63
+ bottom: 0;
64
+ width: 100%;
65
+ background-color: white;
66
+ border-top: 1px solid #ddd;
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ }
71
+ </style>
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <v-row no-gutters class="mb-6">
3
+ <v-col cols="12">
4
+ <h1 class="text-h4 text-md-h3 font-weight-bold">
5
+ Dashboard{{ currentSiteName ? ` - ${currentSiteName}` : "" }}
6
+ </h1>
7
+ </v-col>
8
+ </v-row>
9
+
10
+ <v-row>
11
+ <v-col cols="12">
12
+ <v-progress-linear
13
+ v-if="loading"
14
+ indeterminate
15
+ color="primary"
16
+ class="mb-4"
17
+ />
18
+ </v-col>
19
+ </v-row>
20
+
21
+ <v-row>
22
+ <v-col
23
+ v-for="card in countCardList"
24
+ :key="card.id"
25
+ cols="12"
26
+ sm="6"
27
+ md="6"
28
+ lg="4"
29
+ xl="2"
30
+ >
31
+ <v-card flat border class="fill-height" elevation="0" hover>
32
+ <v-card-text class="pa-4 pa-md-6">
33
+ <v-row no-gutters align="start">
34
+ <v-col cols="8">
35
+ <v-card-subtitle class="text-caption text-uppercase pa-0 mb-2">
36
+ {{ card.label }}
37
+ </v-card-subtitle>
38
+ <v-card-title class="text-h4 text-md-h3 pa-0 mb-3">
39
+ {{ card.value }}
40
+ </v-card-title>
41
+ <v-chip :color="card.chipColor" size="small" variant="flat">
42
+ <v-icon size="14" start>mdi-trending-up</v-icon>
43
+ {{ card.percentage }}
44
+ </v-chip>
45
+ </v-col>
46
+ <v-col cols="4" class="d-flex justify-end">
47
+ <v-avatar :color="card.color" size="56" rounded="lg">
48
+ <v-icon :icon="card.icon" color="white" size="28"></v-icon>
49
+ </v-avatar>
50
+ </v-col>
51
+ </v-row>
52
+ <v-row no-gutters class="mt-4">
53
+ <v-col cols="12">
54
+ <v-select
55
+ :model-value="card.period"
56
+ :items="periodOptions"
57
+ density="compact"
58
+ hide-details
59
+ variant="outlined"
60
+ rounded="lg"
61
+ :disabled="loading"
62
+ @update:model-value="onUpdateCardPeriod(card.key, $event)"
63
+ ></v-select>
64
+ </v-col>
65
+ </v-row>
66
+ </v-card-text>
67
+ </v-card>
68
+ </v-col>
69
+ </v-row>
70
+
71
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
72
+ </template>
73
+
74
+ <script setup lang="ts">
75
+
76
+ const props = defineProps({
77
+ orgId: {
78
+ type: String,
79
+ default: "",
80
+ },
81
+ site: {
82
+ type: String,
83
+ default: "",
84
+ },
85
+ dashboardData: {
86
+ type: Object as () => Record<string, any>,
87
+ default: () => ({}),
88
+ },
89
+ loading: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+ cardPeriods: {
94
+ type: Object as () => TPeriodState,
95
+ required: true,
96
+ },
97
+ currentSiteName: {
98
+ type: String,
99
+ default: "",
100
+ }, cardValues: {
101
+ type: Array as () => TDashboardCardValue[],
102
+ required: true,
103
+ },
104
+ })
105
+
106
+ const emit = defineEmits<{
107
+ updatePeriod: [payload: { key: string; period: string }];
108
+ }>();
109
+
110
+ const periodOptions = ["Today", "This Week", "This Month"];
111
+
112
+ const periodValue: Record<string, TDashboardValues> = {
113
+ Today: "today",
114
+ "This Week": "thisWeek",
115
+ "This Month": "thisMonth",
116
+ };
117
+
118
+ const datePeriod: Record<TDashboardValues, string> = {
119
+ today: "Today",
120
+ thisWeek: "This Week",
121
+ thisMonth: "This Month",
122
+ };
123
+
124
+ const message = ref("");
125
+ const messageSnackbar = ref(false);
126
+ const messageColor = ref("");
127
+
128
+ const countCardList = computed(() => {
129
+ const fetchedData = props.dashboardData || {};
130
+ const hasFetchedData = fetchedData && Object.keys(fetchedData).length > 0;
131
+
132
+ const sourceCards = hasFetchedData
133
+ ? buildCardsFromFetchedData(fetchedData)
134
+ : [];
135
+
136
+ return sourceCards.map((card: any, idx: number) => {
137
+ const periodKey = card.periodKey as keyof TPeriodState;
138
+ const apiPeriod = props.cardPeriods[periodKey] || "today";
139
+ const displayPeriod = datePeriod[apiPeriod] || "Today";
140
+
141
+ return {
142
+ id: idx + 1,
143
+ key: card.key,
144
+ label: card.title || `Card ${idx + 1}`,
145
+ value:
146
+ card.value >= 1000
147
+ ? card.value.toLocaleString()
148
+ : card.value.toString(),
149
+ icon: card.icon,
150
+ color: card.color,
151
+ percentage: `${Number(card.percentage) > 0 ? "+" : ""}${
152
+ card.percentage
153
+ }%`,
154
+ chipColor: card.isPositive ? "success" : "error",
155
+ period: displayPeriod,
156
+ };
157
+ });
158
+ });
159
+
160
+ function buildCardsFromFetchedData(data: Record<string, any>) {
161
+ return props.cardValues.map((mapping) => ({
162
+ key: mapping.key,
163
+ periodKey: mapping.periodKey,
164
+ title: mapping.title,
165
+ value: data[mapping.key]?.count ?? 0,
166
+ percentage: data[mapping.key]?.percentage ?? 0,
167
+ icon: mapping.icon,
168
+ color: mapping.color,
169
+ isPositive: (data[mapping.key]?.percentage ?? 0) >= 0,
170
+ }));
171
+ }
172
+
173
+ function onUpdateCardPeriod(cardKey: string, period: string) {
174
+ emit("updatePeriod", { key: cardKey, period });
175
+ }
176
+ </script>
@@ -82,13 +82,6 @@
82
82
  placeholder="Select cards..."
83
83
  return-object
84
84
  >
85
- <template #item="{ props: itemProps, item }">
86
- <v-list-item v-bind="itemProps">
87
- <template #subtitle>
88
- <span class="text-caption text-grey">{{ item.raw.cardNo }}</span>
89
- </template>
90
- </v-list-item>
91
- </template>
92
85
  </v-select>
93
86
  </div>
94
87
 
@@ -421,6 +414,9 @@ const scanFrame = async () => {
421
414
  addCard(card);
422
415
  closeCameraDialog();
423
416
  return;
417
+ } else if (barcode.rawValue) {
418
+ showSnackbar(`Card "${barcode.rawValue}" not found in available cards.`, "warning");
419
+ return;
424
420
  }
425
421
  }
426
422
  } catch {
@@ -0,0 +1,304 @@
1
+ <template>
2
+ <div>
3
+ <v-file-input
4
+ v-model="files"
5
+ :label="label"
6
+ accept="image/*"
7
+ :prepend-icon="prependIcon"
8
+ hide-details
9
+ show-size
10
+ chips
11
+ multiple
12
+ clearable
13
+ @update:modelValue="handleFileSelect"
14
+ @click:clear="handleClear"
15
+ :hide-input="hasHideInput"
16
+ >
17
+ <template v-slot:append v-if="hasLabel">
18
+ <slot name="append">
19
+ <v-btn color="primary" height="50px" @click="openCameraDialog">
20
+ <v-icon>mdi-camera</v-icon>
21
+ </v-btn>
22
+ </slot>
23
+ </template>
24
+ </v-file-input>
25
+
26
+ <v-row no-gutters v-if="hasLabel">
27
+ <v-col cols="12" class="mt-1">
28
+ <v-chip-group column>
29
+ <template v-for="(file, index) in files" :key="file.name">
30
+ <v-chip
31
+ closable
32
+ class="text-wrap text-caption custom-chip"
33
+ @click:close="removeFile(index)"
34
+ >
35
+ <span class="chip-text">{{ file.name }}</span>
36
+ </v-chip>
37
+ </template>
38
+ </v-chip-group>
39
+ </v-col>
40
+ </v-row>
41
+
42
+ <!-- Camera Dialog -->
43
+ <v-dialog
44
+ v-model="showCameraDialog"
45
+ transition="dialog-bottom-transition"
46
+ width="800"
47
+ max-width="800"
48
+ persistent
49
+ @after-enter="startCamera"
50
+ >
51
+ <v-container
52
+ class="d-flex justify-center"
53
+ max-height="90vh"
54
+ width="800"
55
+ max-width="800"
56
+ >
57
+ <v-card elevation="2" class="d-flex flex-column align-center pa-2">
58
+ <v-toolbar>
59
+ <v-card-title class="text-h5">Take a Picture</v-card-title>
60
+ <v-spacer></v-spacer>
61
+ <v-btn
62
+ color="grey-darken-1"
63
+ icon="mdi-close"
64
+ @click="closeCameraDialog"
65
+ ></v-btn>
66
+ </v-toolbar>
67
+
68
+ <div
69
+ id="reader"
70
+ class="d-flex justify-center align-center"
71
+ style="
72
+ position: relative;
73
+ width: 500px;
74
+ min-width: 400px;
75
+ height: 400px;
76
+ "
77
+ >
78
+ <video
79
+ ref="video"
80
+ style="flex: 1; height: 400px; min-width: 300px"
81
+ class="video-shutter"
82
+ autoplay
83
+ ></video>
84
+ <canvas
85
+ ref="canvas"
86
+ style="flex: 1; height: 400px; min-width: 300px; display: none"
87
+ ></canvas>
88
+ </div>
89
+
90
+ <v-row align="center" justify="center">
91
+ <v-col cols="6">
92
+ <v-btn color="primary" icon class="mt-4" @click="switchCamera">
93
+ <v-icon>mdi-camera-switch</v-icon>
94
+ </v-btn>
95
+ </v-col>
96
+ <v-col cols="6">
97
+ <v-btn
98
+ color="secondary"
99
+ icon
100
+ class="mt-4"
101
+ @click="captureImageFromCamera"
102
+ >
103
+ <v-icon large>mdi-camera-outline</v-icon>
104
+ </v-btn>
105
+ </v-col>
106
+ </v-row>
107
+ </v-card>
108
+ </v-container>
109
+ </v-dialog>
110
+ </div>
111
+ </template>
112
+
113
+ <script setup lang="ts">
114
+ interface FileWithPreview {
115
+ name: string;
116
+ data: File;
117
+ progress: number;
118
+ url: string;
119
+ }
120
+
121
+ const props = defineProps({
122
+ label: {
123
+ type: String,
124
+ default: "Select File",
125
+ },
126
+ prependIcon: {
127
+ type: String,
128
+ default: "mdi-paperclip",
129
+ },
130
+ required: {
131
+ type: Boolean,
132
+ default: true,
133
+ },
134
+ initFiles: {
135
+ type: Array,
136
+ },
137
+ hasLabel: {
138
+ type: Boolean,
139
+ default: true,
140
+ },
141
+ hasHideInput: {
142
+ type: Boolean,
143
+ default: false,
144
+ },
145
+ });
146
+
147
+ const emit = defineEmits<{
148
+ (event: "onFileAttach", payload: Array<{ data: File }>): void;
149
+ (event: "update:files", files: FileWithPreview[]): void;
150
+ (event: "onFileRemoved", payload: { index: number; file: File }): void;
151
+ (event: "onClear"): void;
152
+ }>();
153
+
154
+ const { showUploadedFiles } = useUploadFiles();
155
+
156
+ const files = ref<File[]>([]);
157
+ const attachedFiles = ref<FileWithPreview[]>([]);
158
+ const showCameraDialog = ref(false);
159
+ const video = ref<HTMLVideoElement | null>(null);
160
+ const canvas = ref<HTMLCanvasElement | null>(null);
161
+ const cameraFacingMode = ref<"environment" | "user">("environment");
162
+
163
+ const message = ref("");
164
+ const messageColor = ref("");
165
+ const messageSnackbar = ref(false);
166
+
167
+ function showMessage(msg: string, color: string) {
168
+ message.value = msg;
169
+ messageColor.value = color;
170
+ messageSnackbar.value = true;
171
+ }
172
+
173
+ watchEffect(() => {
174
+ if (Array.isArray(props.initFiles) && props.initFiles.length > 0) {
175
+ files.value = props.initFiles.filter((file) => file && file.name); // Ensure valid files
176
+ }
177
+ });
178
+ const handleFileSelect = async () => {
179
+ if (files.value && files.value.length > 0) {
180
+ const newFiles = files.value.map((file: File) => ({
181
+ name: file.name,
182
+ data: file,
183
+ progress: 0,
184
+ url: URL.createObjectURL(file),
185
+ }));
186
+
187
+ // attachedFiles.value = [...newFiles];
188
+ showUploadedFiles(newFiles);
189
+
190
+ emit("update:files", newFiles);
191
+ } else {
192
+ files.value = [...(props.initFiles as typeof files.value)];
193
+ }
194
+ };
195
+
196
+ const handleClear = () => {
197
+ files.value = [];
198
+ emit("onClear");
199
+ };
200
+
201
+ const openCameraDialog = () => {
202
+ showCameraDialog.value = true;
203
+ };
204
+
205
+ const closeCameraDialog = () => {
206
+ showCameraDialog.value = false;
207
+ stopCamera();
208
+ };
209
+
210
+ const startCamera = async () => {
211
+ try {
212
+ const constraints = {
213
+ video: {
214
+ facingMode: cameraFacingMode.value,
215
+ },
216
+ };
217
+
218
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
219
+ if (video.value) {
220
+ video.value.srcObject = stream;
221
+ video.value.play();
222
+ }
223
+ } catch (error: any) {
224
+ showMessage(`Error accessing camera: ${error.message}`, "error");
225
+ closeCameraDialog();
226
+ }
227
+ };
228
+
229
+ const stopCamera = () => {
230
+ if (video.value) {
231
+ const stream = video.value.srcObject as MediaStream;
232
+ if (stream) {
233
+ stream.getTracks().forEach((track) => track.stop());
234
+ }
235
+ }
236
+ };
237
+
238
+ const switchCamera = async () => {
239
+ await stopCamera();
240
+ cameraFacingMode.value =
241
+ cameraFacingMode.value === "environment" ? "user" : "environment";
242
+ showMessage(
243
+ `Switched to ${
244
+ cameraFacingMode.value === "environment" ? "Back Camera" : "Front Camera"
245
+ }`,
246
+ "error"
247
+ );
248
+ startCamera();
249
+ };
250
+
251
+ const captureImageFromCamera = () => {
252
+ if (!video.value || !canvas.value) return;
253
+
254
+ const context = canvas.value.getContext("2d");
255
+ if (!context) return;
256
+
257
+ // Set canvas dimensions to match video
258
+ canvas.value.width = video.value.videoWidth;
259
+ canvas.value.height = video.value.videoHeight;
260
+
261
+ // Capture the frame
262
+ context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
263
+
264
+ // Convert to file
265
+ canvas.value.toBlob((blob) => {
266
+ if (!blob) return;
267
+
268
+ const file = new File([blob], `camera-capture-${Date.now()}.png`, {
269
+ type: "image/png",
270
+ });
271
+
272
+ files.value = [file];
273
+ handleFileSelect();
274
+ closeCameraDialog();
275
+ }, "image/png");
276
+ };
277
+
278
+ const removeFile = (index) => {
279
+ const removedFile = files.value[index];
280
+ files.value = files.value.filter((_, i) => i !== index);
281
+ emit("onFileRemoved", { index, file: removedFile }); // Emit when a file is removed
282
+ // emit("onFilesUpdated", files.value); // Emit updated files list
283
+ };
284
+
285
+ // Cleanup
286
+ onUnmounted(() => {
287
+ stopCamera();
288
+ });
289
+ </script>
290
+
291
+ <style scoped>
292
+ .custom-chip {
293
+ max-width: 100%;
294
+ height: auto !important;
295
+ white-space: normal;
296
+ padding: 3px 20px;
297
+ }
298
+
299
+ .chip-text {
300
+ word-break: break-word;
301
+ white-space: normal;
302
+ line-height: 1.2;
303
+ }
304
+ </style>