@7365admin1/layer-common 3.0.10 → 3.0.11

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
+ ## 3.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - fd1ebe1: Update Layer Version to 3.0.11
8
+
3
9
  ## 3.0.10
4
10
 
5
11
  ### Patch Changes
@@ -62,15 +62,18 @@ const { menuItem: hidFaceReaderMenu } = useHidFaceReaderMenu({
62
62
  cacheKey: "layout-navigation-hid-site",
63
63
  });
64
64
 
65
+ const { canManageEntryPass } = useSettingsPermission();
66
+
65
67
  const navigationItems = computed(() => {
66
68
  const items = [...(props.navigationItems as TNavigationItem[])];
67
69
 
68
70
  if (
71
+ canManageEntryPass.value &&
69
72
  hidFaceReaderMenu.value &&
70
73
  !items.some((item) => item.title === hidFaceReaderMenu.value?.title)
71
74
  ) {
72
75
  const insertAfterIndex = items.findIndex(
73
- (item) => item.title === "Pass & Key Mgmt",
76
+ (item) => item.title === "Pass & Key Mgmt"
74
77
  );
75
78
 
76
79
  if (insertAfterIndex >= 0) {
@@ -258,13 +258,14 @@
258
258
  <span class="text-caption text-medium-emphasis d-block mb-1">{{ block.label }}</span>
259
259
  <div
260
260
  v-if="initialValues[block.key]"
261
- style="
262
- display: inline-block;
263
- border-radius: 8px;
264
- background: #111111;
265
- padding: 8px;
266
- max-width: 100%;
267
- "
261
+ :style="{
262
+ display: 'inline-block',
263
+ borderRadius: '8px',
264
+ background: sigBgLight[block.key] === false ? '#1e1e1e' : '#ffffff',
265
+ border: sigBgLight[block.key] === false ? '1px solid #334155' : '1px solid #e0e0e0',
266
+ padding: '8px',
267
+ maxWidth: '100%',
268
+ }"
268
269
  >
269
270
  <img
270
271
  :src="initialValues[block.key]"
@@ -443,6 +444,68 @@ const { currentUser } = useLocalAuth();
443
444
  const validForm = ref(false);
444
445
  const submitting = ref(false);
445
446
  const message = ref("");
447
+
448
+ // Detect signature background: true = light (black ink), false = dark (white ink)
449
+ const sigBgLight = ref<Record<string, boolean>>({});
450
+
451
+ function detectSigBackground(key: string, dataUrl: string) {
452
+ if (!dataUrl?.startsWith("data:image")) return;
453
+ const img = new Image();
454
+ img.onload = () => {
455
+ const SIZE = 32;
456
+ const canvas = document.createElement("canvas");
457
+ canvas.width = SIZE;
458
+ canvas.height = SIZE;
459
+ const ctx = canvas.getContext("2d");
460
+ if (!ctx) return;
461
+ ctx.drawImage(img, 0, 0, SIZE, SIZE);
462
+ const d = ctx.getImageData(0, 0, SIZE, SIZE).data;
463
+
464
+ let opaqueTotal = 0, opaqueCount = 0;
465
+ let inkTotal = 0, inkCount = 0;
466
+ for (let i = 0; i < d.length; i += 4) {
467
+ const alpha = d[i + 3];
468
+ const lum = (d[i] + d[i + 1] + d[i + 2]) / 3;
469
+ if (alpha > 128) {
470
+ opaqueTotal += lum;
471
+ opaqueCount++;
472
+ // Ink pixels are the non-background strokes (not very transparent)
473
+ inkTotal += lum;
474
+ inkCount++;
475
+ }
476
+ }
477
+
478
+ if (opaqueCount === 0) {
479
+ // Fully transparent PNG — can't detect, default to white bg
480
+ sigBgLight.value[key] = true;
481
+ return;
482
+ }
483
+
484
+ const opaqueRatio = opaqueCount / (d.length / 4);
485
+ if (opaqueRatio > 0.5) {
486
+ // Mostly opaque PNG — has a solid background; use overall brightness
487
+ sigBgLight.value[key] = opaqueTotal / opaqueCount > 128;
488
+ } else {
489
+ // Mostly transparent — ink-only PNG; check ink color
490
+ // Dark ink (< 128) = black strokes = was light mode → show white bg
491
+ // Light ink (≥ 128) = white strokes = was dark mode → show dark bg
492
+ sigBgLight.value[key] = inkTotal / inkCount < 128;
493
+ }
494
+ };
495
+ img.src = dataUrl;
496
+ }
497
+
498
+ watch(
499
+ () => props.initialValues,
500
+ (vals) => {
501
+ for (const [key, val] of Object.entries(vals)) {
502
+ if (typeof val === "string" && val.startsWith("data:image")) {
503
+ detectSigBackground(key, val);
504
+ }
505
+ }
506
+ },
507
+ { immediate: true, deep: true }
508
+ );
446
509
  const values = ref<Record<string, any>>({});
447
510
 
448
511
  const selectedAction = ref("");
@@ -101,7 +101,7 @@
101
101
  <v-text-field v-model="location" density="comfortable" readonly />
102
102
  </v-col>
103
103
 
104
- <v-col v-if="!visitorInfo.checkIn" cols="12">
104
+ <v-col cols="12">
105
105
  <InputLabel class="text-capitalize" title="Purpose" />
106
106
  <v-textarea
107
107
  v-model="visitorInfo.purpose"
@@ -253,7 +253,20 @@ const emit = defineEmits(["check-in-out", "close"]);
253
253
 
254
254
  const { standardFormatDateTime } = useUtils();
255
255
 
256
- const visitorInfo = ref(prop.visitorData);
256
+ const visitorInfo = ref<Partial<TVisitor>>({});
257
+
258
+ watch(
259
+ () => prop.visitorData,
260
+ (visitorData) => {
261
+ visitorInfo.value = {
262
+ ...visitorData,
263
+ visitorPass: Array.isArray(visitorData?.visitorPass)
264
+ ? [...visitorData.visitorPass]
265
+ : [],
266
+ };
267
+ },
268
+ { immediate: true }
269
+ );
257
270
 
258
271
  const visitorNric = computed({
259
272
  get: () =>
@@ -557,7 +557,7 @@
557
557
  color="success"
558
558
  text="Checkin"
559
559
  :disabled="isVisitorDataFromScannedQRCodeCheckingInOut"
560
- @click="handleCheckin(selectedVisitorObject)"
560
+ @click="handleCheckin(selectedVisitorDataObject)"
561
561
  :loading="isVisitorDataFromScannedQRCodeCheckingInOut"
562
562
  />
563
563
  </span>
@@ -1298,6 +1298,7 @@ const formattedFields = {
1298
1298
  block: "Block",
1299
1299
  level: "Level",
1300
1300
  unit: "Unit",
1301
+ purpose: "Purpose",
1301
1302
  checkIn: "Check In",
1302
1303
  snapshotEntryImage: "Entry Image",
1303
1304
  checkOut: "Check Out",
@@ -1481,18 +1482,32 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
1481
1482
  try {
1482
1483
  isVisitorDataFromScannedQRCodeCheckingInOut.value = true;
1483
1484
  if (type != "checkOut") {
1485
+ const visitorId =
1486
+ visitorData._id ??
1487
+ visitorDataFromScannedQRCode.value._id ??
1488
+ selectedVisitorId.value;
1489
+
1490
+ if (!visitorId) {
1491
+ showMessage("Unable to check in visitor: missing visitor ID.", "error");
1492
+ return;
1493
+ }
1494
+
1495
+ const visitorPass = Array.isArray(visitorData.visitorPass)
1496
+ ? visitorData.visitorPass
1497
+ : [];
1498
+
1484
1499
  await visitorDataFromScannedQRCodeCheckIn({
1485
1500
  type,
1486
1501
  filter: {
1487
- _id: visitorData._id,
1502
+ _id: visitorId,
1488
1503
  },
1489
1504
  update: {
1490
1505
  updatedBy: currentUser.value._id,
1491
- visitorPass: visitorData.visitorPass.find(
1506
+ visitorPass: visitorPass.find(
1492
1507
  (i: Record<string, any>) => i.keyId === null
1493
1508
  )
1494
1509
  ? undefined
1495
- : visitorData.visitorPass,
1510
+ : visitorPass,
1496
1511
  nric: visitorData.nric ?? "",
1497
1512
  plateNumber: visitorData.plateNumber ?? "",
1498
1513
  },
@@ -26,6 +26,7 @@ export default function () {
26
26
  "level",
27
27
  "unit",
28
28
  "unitName",
29
+ "purpose",
29
30
  "remarks",
30
31
  ],
31
32
  contractor: [
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.10",
5
+ "version": "3.0.11",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -58,6 +58,7 @@ declare type TVisitorPayload = Pick<
58
58
  | "deliveryType"
59
59
  | "nric"
60
60
  | "contact"
61
+ | "purpose"
61
62
  | "remarks"
62
63
  | "attachments"
63
64
  | "org"