@7365admin1/layer-common 3.0.7 → 3.0.8

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,12 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - d2ab57b: Mask Site Settings Host url
8
+ - 3dc892a: Update Layer-common version
9
+
3
10
  ## 3.0.7
4
11
 
5
12
  ### Patch Changes
@@ -32,6 +32,9 @@
32
32
  @click:row="handleRowClick"
33
33
  style="max-height: 300px"
34
34
  >
35
+ <template #item.host="{ value }">
36
+ {{ maskHostUrl(value) }}
37
+ </template>
35
38
  </v-data-table>
36
39
  </v-card-text>
37
40
 
@@ -103,7 +106,7 @@
103
106
  <v-col cols="12" class="mb-2">
104
107
  Host:
105
108
  <span class="font-weight-bold">
106
- {{ camera.host }}
109
+ {{ maskHostUrl(camera.host) }}
107
110
  </span>
108
111
  </v-col>
109
112
 
@@ -367,6 +370,19 @@ function handleSuccess() {
367
370
  getCameraRefresh();
368
371
  }
369
372
 
373
+ function maskHostUrl(value?: string | null) {
374
+ if (!value) return "";
375
+
376
+ const text = String(value);
377
+ const protocolMatch = text.match(/^([a-z][a-z\d+\-.]*:\/\/)(.*)$/i);
378
+ const prefix = protocolMatch?.[1] ?? "";
379
+ const rest = protocolMatch?.[2] ?? text;
380
+
381
+ if (rest.length <= 8) return `${prefix}${"*".repeat(rest.length)}`;
382
+
383
+ return `${prefix}${rest.slice(0, 3)}****${rest.slice(-3)}`;
384
+ }
385
+
370
386
  const camera = ref<TSiteCamera>({
371
387
  site: "",
372
388
  host: "",
@@ -53,6 +53,7 @@
53
53
  divided
54
54
  variant="outlined"
55
55
  class="bg-white"
56
+ style="border-radius: 8px; border-color: #d9e1e8;"
56
57
  >
57
58
  <v-btn
58
59
  v-for="range in rangeOptions"
@@ -62,8 +63,8 @@
62
63
  size="small"
63
64
  :style="
64
65
  selectedRange === range.value
65
- ? { backgroundColor: '#0b2f47', color: '#ffffff' }
66
- : {}
66
+ ? { backgroundColor: '#e2e8f0', color: '#0b2f47', fontWeight: '600' }
67
+ : { backgroundColor: 'transparent', color: '#64748b' }
67
68
  "
68
69
  >
69
70
  {{ range.label }}
@@ -350,13 +351,7 @@
350
351
  <div
351
352
  class="d-flex align-center ga-2 text-caption text-blue-grey"
352
353
  >
353
- <v-avatar
354
- size="30"
355
- color="blue-grey-lighten-4"
356
- class="text-caption font-weight-bold text-blue-grey-darken-2"
357
- >
358
- {{ getInitials(item.person) }}
359
- </v-avatar>
354
+ <AvatarMain :name="item.person" :size="30" />
360
355
  <span class="text-truncate" style="max-width: 100px">{{
361
356
  item.person
362
357
  }}</span>
@@ -405,14 +400,8 @@
405
400
  rounded="lg"
406
401
  class="pa-3"
407
402
  >
408
- <div class="d-flex align-center ga-3">
409
- <v-avatar
410
- size="34"
411
- color="blue-grey-lighten-4"
412
- class="text-caption font-weight-bold text-blue-grey-darken-2 flex-shrink-0"
413
- >
414
- {{ getInitials(item.name) }}
415
- </v-avatar>
403
+ <div class="d-flex align-start ga-3">
404
+ <AvatarMain :name="item.name" :size="32" />
416
405
  <div class="flex-grow-1" style="min-width: 0">
417
406
  <div
418
407
  class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
@@ -445,59 +434,37 @@
445
434
  action-label="View All"
446
435
  @action="goToDashboardRoute('feedbacks')"
447
436
  >
448
- <div v-if="feedbackItems.length" class="d-flex flex-column ga-3">
449
- <v-card
450
- v-for="item in feedbackItems"
451
- :key="item.id"
452
- flat
453
- border
454
- rounded="lg"
455
- class="pa-3"
456
- >
457
- <div class="d-flex align-center flex-wrap ga-3">
458
- <div class="flex-grow-1" style="min-width: 0">
459
- <div
460
- class="text-body-2 font-weight-bold text-blue-grey-darken-4 text-truncate"
461
- >
462
- {{ item.title }}
463
- </div>
464
- <div class="text-caption text-blue-grey mt-1 text-truncate">
465
- {{ item.subtitle }}
466
- </div>
437
+ <div v-if="feedbackItems.length" class="figma-list mt-2">
438
+ <div v-for="item in feedbackItems" :key="item.id" class="d-flex align-center justify-space-between" style="padding: 16px 20px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 12px; background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.02);">
439
+ <div style="flex: 1; min-width: 140px;">
440
+ <strong style="display: block; font-size: 15px; font-weight: 700; color: #0b2f47; margin-bottom: 4px;">{{ item.title }}</strong>
441
+ <span style="font-size: 13px; color: #94a3b8;">{{ item.subtitle }}</span>
442
+ </div>
443
+
444
+ <div style="flex: 1; display: flex; align-items: center; gap: 12px;">
445
+ <AvatarMain :name="item.person" :size="36" />
446
+ <span style="font-size: 14px; color: #64748b;">{{ item.person }}</span>
447
+ </div>
448
+
449
+ <div style="flex: 1; display: flex; justify-content: center;">
450
+ <div v-if="getStatusTone(item.status) === 'success'" style="background-color: #72e3a1; color: #0f5132; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
451
+ {{ item.status }}
467
452
  </div>
468
- <div
469
- class="d-flex align-center ga-2 text-caption text-blue-grey"
470
- >
471
- <v-avatar
472
- size="30"
473
- color="blue-grey-lighten-4"
474
- class="text-caption font-weight-bold text-blue-grey-darken-2"
475
- >
476
- {{ getInitials(item.person) }}
477
- </v-avatar>
478
- <span class="text-truncate" style="max-width: 100px">{{
479
- item.person
480
- }}</span>
453
+ <div v-else-if="getStatusTone(item.status) === 'warning'" style="background-color: #fde68a; color: #92400e; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
454
+ {{ item.status }}
481
455
  </div>
482
- <v-chip
483
- size="small"
484
- :color="getStatusTone(item.status)"
485
- variant="tonal"
486
- label
487
- >
456
+ <div v-else-if="getStatusTone(item.status) === 'error'" style="background-color: #fecaca; color: #b91c1c; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
488
457
  {{ item.status }}
489
- </v-chip>
490
- <v-btn
491
- variant="outlined"
492
- color="primary"
493
- size="small"
494
- class="text-none font-weight-bold"
495
- @click="goToListItem(item, 'feedbacks')"
496
- >
497
- View
498
- </v-btn>
458
+ </div>
459
+ <div v-else style="background-color: #e2e8f0; color: #64748b; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; text-transform: capitalize;">
460
+ {{ item.status || 'To-Do' }}
461
+ </div>
499
462
  </div>
500
- </v-card>
463
+
464
+ <div style="flex: 0 0 auto; text-align: right;">
465
+ <v-btn variant="outlined" class="text-none font-weight-medium" size="small" color="#3b82f6" style="border-color: #e2e8f0; border-radius: 8px; padding: 0 16px; height: 32px;" @click="goToListItem(item, 'feedbacks')">View</v-btn>
466
+ </div>
467
+ </div>
501
468
  </div>
502
469
  <DashboardEmptyState v-else />
503
470
  </DashboardPanel>
@@ -995,8 +962,8 @@ onUnmounted(() => {
995
962
 
996
963
  const rangeOptions = [
997
964
  { label: "Today", value: "Today" },
998
- { label: "Week", value: "This Week" },
999
- { label: "Month", value: "This Month" },
965
+ { label: "7d", value: "This Week" },
966
+ { label: "30d", value: "This Month" },
1000
967
  ];
1001
968
  const defaultVisitorOptions = [
1002
969
  "All",
@@ -1200,10 +1167,10 @@ const secondarySeries = computed(() =>
1200
1167
  normalizeSeries(activityChart.value.secondary, chartLabels.value.length)
1201
1168
  );
1202
1169
  const primaryLegendLabel = computed(
1203
- () => activityChart.value.primaryLabel || "Work Order"
1170
+ () => "Closed Work Order"
1204
1171
  );
1205
1172
  const secondaryLegendLabel = computed(
1206
- () => activityChart.value.secondaryLabel || "Feedback"
1173
+ () => "Open Work Order"
1207
1174
  );
1208
1175
  const chartMax = computed(() =>
1209
1176
  Math.max(1, ...primarySeries.value, ...secondarySeries.value)
@@ -1882,18 +1849,7 @@ function goToListItem(item: TaskItem | FeedbackItem, routeKey: string) {
1882
1849
  router.push(`/${props.orgId}/${props.site}/${path}/${id}`);
1883
1850
  }
1884
1851
 
1885
- function getInitials(name: string) {
1886
- const parts = String(name || "")
1887
- .trim()
1888
- .split(/\s+/)
1889
- .filter(Boolean);
1890
- if (!parts.length) return "?";
1891
- return parts
1892
- .slice(0, 2)
1893
- .map((part) => part[0])
1894
- .join("")
1895
- .toUpperCase();
1896
- }
1852
+
1897
1853
 
1898
1854
  function formatNumber(value: number) {
1899
1855
  return new Intl.NumberFormat("en-US").format(toNumber(value));
@@ -22,7 +22,7 @@
22
22
  <v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false"
23
23
  class="mt-3" :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id"
24
24
  label="Pass (optional)" variant="outlined" density="compact" :error-messages="errorScanPassMessage"
25
- persistent-hint small-chips>
25
+ persistent-hint small-chips @click="fetchPasses">
26
26
  <template v-slot:no-data>
27
27
  <v-list-item density="compact">
28
28
  <v-list-item-title v-if="passInput">
@@ -160,20 +160,28 @@ const passTypesComputed = computed(() => {
160
160
  }
161
161
  })
162
162
 
163
- const { data: passesData, refresh: refreshPassesData, pending: fetchPassesPending } = await useLazyAsyncData('get-pass-keys', () => {
164
- return getPassKeysByPageSearch({
165
- search: passInput.value,
166
- page: 1,
167
- limit: 500,
168
- passTypes: passTypesComputed.value,
169
- sites: [props.site],
170
- statuses: ["Available"]
171
- })
172
- })
173
-
174
- watch(passesData, (data: any) => {
175
- passItems.value = Array.isArray(data?.items) ? data.items : []
176
- })
163
+ const isPassListLoading = ref(false);
164
+
165
+ async function fetchPasses() {
166
+ // load pass list
167
+ passItems.value = [];
168
+ isPassListLoading.value = true;
169
+ try {
170
+ const result = await getPassKeysByPageSearch({
171
+ page: 1,
172
+ sites: [props.site],
173
+ limit: 10000,
174
+ passTypes: passTypesComputed.value,
175
+ statuses: ["Available"],
176
+ sortBy: { column: "prefixAndName", order: "1" },
177
+ });
178
+ passItems.value = result?.items;
179
+ } catch (error) {
180
+ showMessage(errorConverter(error as string), "error");
181
+ } finally {
182
+ isPassListLoading.value = false;
183
+ }
184
+ }
177
185
 
178
186
  const passItemsFilteredFinal = computed(() => {
179
187
  return passItems.value.filter((item: TPassKey) => {
@@ -27,6 +27,24 @@
27
27
  size="x-small"
28
28
  class="text-capitalize"
29
29
  >{{ submissionStatus }}</v-chip>
30
+ <template v-if="requiresPayment">
31
+ <v-chip
32
+ v-if="initialManagementValues?.paymentStatus"
33
+ color="green"
34
+ variant="flat"
35
+ size="x-small"
36
+ >Payment {{ initialManagementValues.paymentStatus }}</v-chip>
37
+ <v-chip
38
+ v-else
39
+ color="orange"
40
+ variant="flat"
41
+ size="x-small"
42
+ >Payment Not Paid</v-chip>
43
+ <span
44
+ v-if="initialManagementValues?.paymentOrderId"
45
+ class="text-caption text-medium-emphasis"
46
+ >Order: {{ initialManagementValues.paymentOrderId }}</span>
47
+ </template>
30
48
  </div>
31
49
 
32
50
  <!-- Scrollable body -->
@@ -404,6 +422,7 @@ const props = defineProps({
404
422
  submissionUnit: { type: String, default: "" },
405
423
  approvalMode: { type: Boolean, default: false },
406
424
  initialManagementValues: { type: Object as PropType<Record<string, string>>, default: () => ({}) },
425
+ formType: { type: String, default: "" },
407
426
  /** Optional custom submit handler. Receives the collected form values.
408
427
  * If omitted the component submits to /api/online-forms automatically. */
409
428
  submitFn: {
@@ -505,6 +524,21 @@ const submissionStatusColor = computed(() => {
505
524
  }
506
525
  });
507
526
 
527
+ const PAYMENT_REQUIRED_FORM_TYPES = new Set([
528
+ "Application for Electronic Access Card",
529
+ "Application for Booking of BBQ Pit",
530
+ "Application for Moving In/Out & Delivery",
531
+ "Application for Main Entrance RFID Transponder",
532
+ ]);
533
+
534
+ const requiresPayment = computed(() => {
535
+ if (!PAYMENT_REQUIRED_FORM_TYPES.has(props.formType)) return false;
536
+ if (props.formType === "Application for Main Entrance RFID Transponder") {
537
+ return !!(props.initialValues.reasonLost || props.initialValues.reasonDamaged);
538
+ }
539
+ return true;
540
+ });
541
+
508
542
  // Today's date formatted DD MMM YYYY
509
543
  const todayDate = computed(() => {
510
544
  const d = new Date();
@@ -27,6 +27,8 @@
27
27
  persistent-hint
28
28
  small-chips
29
29
  :clearable="props.clearable"
30
+ @click="fetchPasses"
31
+ :loading="isPassListLoading"
30
32
  >
31
33
  <template v-slot:chip="{ props, item }">
32
34
  <v-chip
@@ -56,6 +58,8 @@
56
58
  persistent-hint
57
59
  small-chips
58
60
  :clearable="props.clearable"
61
+ @click="fetchKeys"
62
+ :loading="isKeyListLoading"
59
63
  >
60
64
  <template v-slot:chip="{ props, item }">
61
65
  <v-chip
@@ -83,6 +87,7 @@
83
87
  </v-card-text>
84
88
  </v-card>
85
89
  </v-row>
90
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
86
91
  </template>
87
92
 
88
93
  <script setup lang="ts">
@@ -90,6 +95,7 @@ import type { PropType } from "vue";
90
95
  import type { ValidationRule } from "vuetify/lib/types.mjs";
91
96
  import usePassKey from "../composables/usePassKey";
92
97
  import useKey from "../composables/useKey";
98
+ import { string } from "zod/v4";
93
99
 
94
100
  const props = defineProps({
95
101
  passRules: {
@@ -112,6 +118,10 @@ const props = defineProps({
112
118
  type: String,
113
119
  default: "",
114
120
  },
121
+ passKeys: {
122
+ type: Array,
123
+ default: [],
124
+ },
115
125
  hideKeys: {
116
126
  type: Boolean,
117
127
  default: false,
@@ -158,43 +168,72 @@ const passTypesComputed = computed(() => {
158
168
  }
159
169
  });
160
170
 
161
- const {
162
- data: passesData,
163
- refresh: refreshPassesData,
164
- pending: fetchPassesPending,
165
- } = await useLazyAsyncData("get-pass-keys", () => {
166
- return getPassKeysByPageSearch({
167
- search: passInput.value,
168
- page: 1,
169
- limit: 500,
170
- passTypes: passTypesComputed.value,
171
- sites: [props.site],
172
- statuses: ["Available"],
173
- });
174
- });
171
+ const message = ref("");
172
+ const messageColor = ref("");
173
+ const messageSnackbar = ref(false);
175
174
 
176
- watch(passesData, (data: any) => {
177
- passItems.value = data?.items || [];
178
- });
175
+ function showMessage(msg: string, color: string) {
176
+ message.value = msg;
177
+ messageColor.value = color;
178
+ messageSnackbar.value = true;
179
+ }
179
180
 
180
- const {
181
- data: keysData,
182
- refresh: refreshKeysData,
183
- pending: fetchKeysPending,
184
- } = await useLazyAsyncData("get-keys", () => {
185
- return getPassKeysByPageSearch({
186
- search: keyInput.value,
187
- statuses: ["Available"],
188
- passTypes: ["pass-key"],
189
- page: 1,
190
- limit: 500,
191
- sites: [props.site],
192
- });
193
- });
181
+ const isPassListLoading = ref(false);
194
182
 
195
- watch(keysData, (data: any) => {
196
- keyItems.value = data?.items || [];
197
- });
183
+ async function fetchPasses() {
184
+ // load pass list
185
+ passItems.value = [];
186
+ isPassListLoading.value = true;
187
+ try {
188
+ const result = await getPassKeysByPageSearch({
189
+ page: 1,
190
+ sites: [props.site],
191
+ limit: 10000,
192
+ passTypes: passTypesComputed.value,
193
+ statuses: ["Available"],
194
+ sortBy: { column: "prefixAndName", order: "1" },
195
+ });
196
+ passItems.value = result?.items;
197
+ } catch (error) {
198
+ showMessage(errorConverter(error as string), "error");
199
+ } finally {
200
+ isPassListLoading.value = false;
201
+ }
202
+ }
203
+
204
+ const isKeyListLoading = ref(false);
205
+
206
+ async function fetchKeys() {
207
+ keyItems.value = [];
208
+ isKeyListLoading.value = true;
209
+ try {
210
+ const keys: any = await getPassKeysByPageSearch({
211
+ page: 1,
212
+ sites: [props.site],
213
+ limit: 10000,
214
+ passTypes: ["pass-key"],
215
+ statuses: ["Available"],
216
+ sortBy: { column: "prefixAndName", order: "1" },
217
+ });
218
+ const selectedKeys = keyItems.value.filter((item: any) => {
219
+ if (Array.isArray(props?.passKeys) && props.passKeys.length) {
220
+ return props.passKeys.includes(item._id);
221
+ }
222
+ return false;
223
+ });
224
+ const filteredArray = keys.items.filter((item: any) => {
225
+ if (Array.isArray(props?.passKeys) && props.passKeys.length) {
226
+ return !props.passKeys.includes(item._id);
227
+ }
228
+ return true;
229
+ });
230
+ keyItems.value = [...selectedKeys, ...filteredArray];
231
+ } catch (error) {
232
+ showMessage(errorConverter(error as string), "error");
233
+ } finally {
234
+ isKeyListLoading.value = false;
235
+ }
236
+ }
198
237
 
199
238
  watch(selectedPass, (newVal) => {
200
239
  pass.value = [{ keyId: newVal }];