@7365admin1/layer-common 3.1.3-staging.45 → 3.1.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.
@@ -34,14 +34,3 @@ jobs:
34
34
 
35
35
  - name: Publish to npm with staging tag
36
36
  run: npm publish --tag staging --registry https://registry.npmjs.org
37
-
38
- - name: Notify Discord on failure
39
- if: failure()
40
- env:
41
- DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
42
- run: |
43
- MESSAGE="@everyone :rotating_light: **Publish Staging** failed in \`${{ github.repository }}\` (\`${{ github.workflow }}\`)
44
- ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
45
- curl -s -X POST -H "Content-Type: application/json" \
46
- -d "$(jq -n --arg content "$MESSAGE" '{content: $content}')" \
47
- "$DISCORD_WEBHOOK_URL"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 3700e81: Release for 08-04-2026
8
+
3
9
  ## 3.1.2
4
10
 
5
11
  ### Patch Changes
@@ -48,7 +48,7 @@
48
48
  <v-list-item class="cursor-pointer" @click="openMediaViewer(item._id, item.mimeType)">
49
49
  <template #prepend>
50
50
  <v-avatar size="36" rounded="sm" class="mr-3">
51
- <v-img :src="imageCover(item._id)" cover>
51
+ <v-img :src="getFileUrl(item._id)" cover>
52
52
  <template #error>
53
53
  <v-icon color="green" size="20">mdi-image</v-icon>
54
54
  </template>
@@ -97,7 +97,7 @@
97
97
  </v-row>
98
98
 
99
99
  <!-- Media Carousel (images + videos) -->
100
- <ImageCarousel v-model="showCarousel" :active-file-id="activeFileId" :files="carouselFiles" :anpr="anpr" :site="site" />
100
+ <ImageCarousel v-model="showCarousel" :active-file-id="activeFileId" :files="carouselFiles" />
101
101
 
102
102
  <!-- Document Viewer -->
103
103
  <DocumentViewer v-model="showDocumentViewer" :file-id="activeFileId" :file-name="activeFileName"
@@ -123,18 +123,10 @@ const props = defineProps({
123
123
  showFileTypeTitle: {
124
124
  type: Boolean,
125
125
  default: false,
126
- },
127
- anpr: {
128
- type: Boolean,
129
- default: false
130
- },
131
- site: {
132
- type: String,
133
- default: ""
134
126
  }
135
127
  });
136
128
 
137
- const { getFileUrl, urlToFile, getFileById, getFileUrlAnpr } = useFile();
129
+ const { getFileUrl, urlToFile, getFileById } = useFile();
138
130
 
139
131
  const rawFileArray = ref<{ file: File, _id: string, preview: boolean, mimeType: string }[]>([]);
140
132
  const loading = ref(true);
@@ -167,12 +159,6 @@ function openDocumentViewer(id: string, name: string, mimeType: string) {
167
159
  showDocumentViewer.value = true;
168
160
  }
169
161
 
170
- const imageCover = (id: string) => {
171
- if(props.anpr && props.site){
172
- return getFileUrlAnpr(`${props.site}/${id}`)
173
- } else return getFileUrl(id)
174
- }
175
-
176
162
  function getFileIcon(mimeType: string): string {
177
163
  if (mimeType.startsWith('image/')) return 'mdi-image';
178
164
  if (mimeType.startsWith('video/')) return 'mdi-video';
@@ -220,11 +206,7 @@ watchEffect(async () => {
220
206
 
221
207
  try {
222
208
  // Try to get the actual file with proper MIME type detection from blob
223
- // const fileUrl = getFileUrl(item.id);
224
- let fileUrl = ""
225
- if(props.anpr && props.site){
226
- fileUrl = getFileUrlAnpr(`${props.site}/${item.id}`)
227
- } else fileUrl = getFileUrl(item.id);
209
+ const fileUrl = getFileUrl(item.id);
228
210
  file = await urlToFile(fileUrl, item.name);
229
211
  mimeType = file.type || 'application/octet-stream';
230
212
 
@@ -984,44 +984,60 @@
984
984
  <div
985
985
  v-for="item in formattedActivePatrolItems"
986
986
  :key="item.id || item._id"
987
- class="active-patrol-row"
987
+ class="d-flex align-center justify-space-between"
988
+ style="
989
+ padding: 16px 20px;
990
+ border: 1px solid #e2e8f0;
991
+ border-radius: 12px;
992
+ margin-bottom: 12px;
993
+ background-color: #ffffff;
994
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
995
+ "
988
996
  >
989
- <div class="active-patrol-main">
990
- <strong>{{ item.title || item.name }}</strong>
991
- <span>{{ item.subtitle || item.location || "" }}</span>
997
+ <div style="flex: 1; min-width: 140px">
998
+ <strong
999
+ style="
1000
+ display: block;
1001
+ font-size: 15px;
1002
+ font-weight: 700;
1003
+ color: #0b2f47;
1004
+ margin-bottom: 4px;
1005
+ "
1006
+ >{{ item.title || item.name }}</strong
1007
+ >
1008
+ <span style="font-size: 13px; color: #94a3b8">{{
1009
+ item.subtitle || item.location || ""
1010
+ }}</span>
992
1011
  </div>
993
1012
 
994
- <div class="active-patrol-assignees">
995
- <div class="active-patrol-assignee">
996
- <AvatarMain :name="item.primaryAssignee" :size="24" />
997
- <span>{{ item.primaryAssignee }}</span>
998
- </div>
999
- <span
1000
- v-if="item.extraAssigneeCount > 0"
1001
- class="active-patrol-more-pill active-patrol-tooltip"
1002
- :data-tooltip="item.extraAssignees.join(', ')"
1003
- >
1004
- +{{ item.extraAssigneeCount }}
1005
- </span>
1013
+ <div
1014
+ style="flex: 1; display: flex; align-items: center; gap: 12px"
1015
+ >
1016
+ <AvatarMain
1017
+ :name="item.person || item.assignedTo"
1018
+ :size="36"
1019
+ />
1020
+ <span style="font-size: 14px; color: #64748b">{{
1021
+ item.person || item.assignedTo
1022
+ }}</span>
1006
1023
  </div>
1007
1024
 
1008
- <div class="active-patrol-statuses">
1025
+ <div style="flex: 1; display: flex; justify-content: center">
1009
1026
  <div
1010
- class="active-patrol-status-pill"
1011
- :style="getPatrolStatusStyle(item.primaryStatus)"
1027
+ :style="getPatrolStatusStyle(item.status)"
1028
+ style="
1029
+ padding: 6px 16px;
1030
+ border-radius: 20px;
1031
+ font-size: 13px;
1032
+ font-weight: 500;
1033
+ text-transform: capitalize;
1034
+ "
1012
1035
  >
1013
- {{ item.primaryStatus }}
1036
+ {{ item.status }}
1014
1037
  </div>
1015
- <span
1016
- v-if="item.extraStatusCount > 0"
1017
- class="active-patrol-more-pill active-patrol-tooltip"
1018
- :data-tooltip="item.extraStatuses.join(', ')"
1019
- >
1020
- +{{ item.extraStatusCount }}
1021
- </span>
1022
1038
  </div>
1023
1039
 
1024
- <div class="active-patrol-action">
1040
+ <div style="flex: 0 0 auto; text-align: right">
1025
1041
  <v-btn
1026
1042
  variant="outlined"
1027
1043
  class="text-none font-weight-medium"
@@ -1529,16 +1545,30 @@ const { userAppRole } = useLocalSetup();
1529
1545
  const { downloadPDF } = usePDFDownload();
1530
1546
  const route = useRoute();
1531
1547
 
1548
+ const dashboardWidgetPermissions: Record<string, string> = {
1549
+ activity: "dashboard:view-activity",
1550
+ recentActivity: "dashboard:view-activity",
1551
+ taskSchedule: "dashboard:view-task-schedule",
1552
+ staffStatus: "dashboard:view-attendance-widget",
1553
+ attendance: "dashboard:view-attendance-widget",
1554
+ feedbacks: "dashboard:view-feedbacks-widget",
1555
+ workOrderStatus: "dashboard:view-work-order-status",
1556
+ activePatrol: "dashboard:view-active-patrol",
1557
+ upcomingEvents: "dashboard:view-upcoming-events",
1558
+ todayReminders: "dashboard:view-today-reminders",
1559
+ todayAttentions: "dashboard:view-today-attentions",
1560
+ };
1561
+
1532
1562
  const propertyWidgetPermissions: Record<string, string[]> = {
1533
1563
  workOrders: [
1534
- "work_orders:see-all-work-orders",
1564
+ "workOrder:see-all-work-orders",
1535
1565
  "work-order:see-all-work-orders",
1536
1566
  ],
1537
1567
  incidents: ["incident-reports:see-incident-reports"],
1538
1568
  visitors: ["visitorManagement:see-all-visitor"],
1539
1569
  facilityBookings: ["facility-booking-mgmt:see-all-facility-booking"],
1540
1570
  activity: [
1541
- "work_orders:see-all-work-orders",
1571
+ "workOrder:see-all-work-orders",
1542
1572
  "work-order:see-all-work-orders",
1543
1573
  ],
1544
1574
  upcomingEvents: ["event-mgmt:see-all-event"],
@@ -1548,7 +1578,7 @@ const propertyWidgetPermissions: Record<string, string[]> = {
1548
1578
  "facility-booking-mgmt:see-all-facility-booking",
1549
1579
  ],
1550
1580
  workOrderStatus: [
1551
- "work_orders:see-all-work-orders",
1581
+ "workOrder:see-all-work-orders",
1552
1582
  "work-order:see-all-work-orders",
1553
1583
  ],
1554
1584
  feedbacks: ["feedbacks:see-all-feedback"],
@@ -1573,14 +1603,14 @@ const moduleWidgetPermissions: Record<string, string[]> = {
1573
1603
  "work_order:see-all-work-orders",
1574
1604
  "work-order:see-all-work-orders",
1575
1605
  "work-orders:see-all-work-orders",
1576
- "work_orders:see-all-work-orders",
1577
1606
  ],
1578
- recentActivity: [
1579
- "work_orders:see-all-work-orders",
1580
- "work_order:see-all-work-orders",
1581
- "work-order:see-all-work-orders",
1582
- "work-orders:see-all-work-orders",
1583
- "work_orders:see-all-work-orders",
1607
+ taskSchedule: [
1608
+ "cleaning-schedule-mgmt:see-all-schedules",
1609
+ "schedule-task-mgmt:see-all-schedule-tasks",
1610
+ ],
1611
+ taskCompleted: [
1612
+ "cleaning-schedule-mgmt:see-all-schedules",
1613
+ "schedule-task-mgmt:see-all-schedule-tasks",
1584
1614
  ],
1585
1615
  staffStatus: ["attendance-mgmt:see-all-attendance"],
1586
1616
  attendance: ["attendance-mgmt:see-all-attendance"],
@@ -1603,14 +1633,9 @@ const moduleWidgetPermissions: Record<string, string[]> = {
1603
1633
  activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs"],
1604
1634
  };
1605
1635
 
1606
- function hasSchedulePermission(permissions: string[], action: string) {
1607
- return permissions.some((permission) =>
1608
- permission.endsWith(`-schedule-mgmt:${action}`)
1609
- );
1610
- }
1611
-
1612
1636
  function canViewWidget(key: string) {
1613
1637
  const permissions = userAppRole.value?.permissions;
1638
+ const dashboardPermission = dashboardWidgetPermissions[key];
1614
1639
  const modulePermissions = isPropertyMode.value
1615
1640
  ? propertyWidgetPermissions[key]
1616
1641
  : moduleWidgetPermissions[key];
@@ -1618,22 +1643,20 @@ function canViewWidget(key: string) {
1618
1643
  if (!permissions) return true;
1619
1644
  if (permissions.includes("*")) return true;
1620
1645
 
1621
- if (
1622
- !isPropertyMode.value &&
1623
- ["taskSchedule", "taskCompleted"].includes(key)
1624
- ) {
1625
- return hasSchedulePermission(permissions, "see-all-schedules");
1626
- }
1627
-
1628
1646
  if (isPropertyMode.value && modulePermissions) {
1629
1647
  return modulePermissions.some((permission) =>
1630
1648
  permissions.includes(permission)
1631
1649
  );
1632
1650
  }
1633
1651
 
1634
- if (modulePermissions) {
1635
- return modulePermissions.some((permission) =>
1636
- permissions.includes(permission)
1652
+ if (dashboardPermission || modulePermissions) {
1653
+ return (
1654
+ (dashboardPermission && permissions.includes(dashboardPermission)) ||
1655
+ Boolean(
1656
+ modulePermissions?.some((permission) =>
1657
+ permissions.includes(permission)
1658
+ )
1659
+ )
1637
1660
  );
1638
1661
  }
1639
1662
 
@@ -2751,23 +2774,18 @@ const activePatrolItems = computed<any[]>(
2751
2774
 
2752
2775
  const formattedActivePatrolItems = computed<any[]>(() => {
2753
2776
  return activePatrolItems.value.map((item) => {
2754
- const statuses = normalizeListValue(item.status || "Pending");
2755
- const assignees = normalizeListValue(
2756
- item.person || item.assignedTo || "Unassigned"
2757
- );
2758
-
2777
+ let status = item.status;
2778
+ if (Array.isArray(status)) {
2779
+ status = status.join(", ");
2780
+ }
2781
+ let person = item.person || item.assignedTo || "Unassigned";
2782
+ if (Array.isArray(person)) {
2783
+ person = person.join(", ");
2784
+ }
2759
2785
  return {
2760
2786
  ...item,
2761
- status: statuses.join(", "),
2762
- statuses,
2763
- primaryStatus: statuses[0] || "Pending",
2764
- extraStatuses: statuses.slice(1),
2765
- extraStatusCount: Math.max(0, statuses.length - 1),
2766
- person: assignees.join(", "),
2767
- assignees,
2768
- primaryAssignee: assignees[0] || "Unassigned",
2769
- extraAssignees: assignees.slice(1),
2770
- extraAssigneeCount: Math.max(0, assignees.length - 1),
2787
+ status,
2788
+ person,
2771
2789
  };
2772
2790
  });
2773
2791
  });
@@ -3937,20 +3955,6 @@ function normalizeStatus(value: any) {
3937
3955
  return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3938
3956
  }
3939
3957
 
3940
- function normalizeListValue(value: any) {
3941
- if (Array.isArray(value)) {
3942
- return value
3943
- .flatMap((entry) => normalizeListValue(entry))
3944
- .map((entry) => String(entry).trim())
3945
- .filter(Boolean);
3946
- }
3947
-
3948
- return String(value || "")
3949
- .split(",")
3950
- .map((entry) => entry.trim())
3951
- .filter(Boolean);
3952
- }
3953
-
3954
3958
  function getStatusTone(status: any) {
3955
3959
  const s = String(status || "").toLowerCase();
3956
3960
  if (
@@ -4418,151 +4422,6 @@ function formatDashboardTime(value?: string) {
4418
4422
  border: 2px solid #f9c97c;
4419
4423
  }
4420
4424
 
4421
- .active-patrol-row {
4422
- align-items: center;
4423
- background-color: #ffffff;
4424
- border: 1px solid #e2e8f0;
4425
- border-radius: 8px;
4426
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
4427
- display: grid;
4428
- gap: 10px;
4429
- grid-template-columns: 150px minmax(0, 1fr) max-content max-content;
4430
- margin-bottom: 12px;
4431
- min-height: 64px;
4432
- overflow: visible;
4433
- padding: 12px 14px;
4434
- }
4435
-
4436
- .active-patrol-main {
4437
- min-width: 0;
4438
- }
4439
-
4440
- .active-patrol-main strong {
4441
- color: #0b2f47;
4442
- display: block;
4443
- font-size: 13px;
4444
- font-weight: 700;
4445
- line-height: 1.25;
4446
- margin-bottom: 3px;
4447
- overflow-wrap: anywhere;
4448
- }
4449
-
4450
- .active-patrol-main span {
4451
- color: #94a3b8;
4452
- display: block;
4453
- font-size: 11px;
4454
- line-height: 1.25;
4455
- overflow-wrap: anywhere;
4456
- }
4457
-
4458
- .active-patrol-assignees {
4459
- align-items: center;
4460
- display: flex;
4461
- gap: 6px;
4462
- min-width: 0;
4463
- }
4464
-
4465
- .active-patrol-assignee {
4466
- align-items: center;
4467
- background: #f8fafc;
4468
- border: 1px solid #e7edf3;
4469
- border-radius: 999px;
4470
- display: inline-flex;
4471
- flex: 1 1 auto;
4472
- gap: 6px;
4473
- max-width: 100%;
4474
- min-width: 0;
4475
- padding: 2px 8px 2px 3px;
4476
- }
4477
-
4478
- .active-patrol-assignee span {
4479
- color: #475569;
4480
- font-size: 11px;
4481
- font-weight: 600;
4482
- line-height: 1.2;
4483
- min-width: 0;
4484
- overflow: hidden;
4485
- text-overflow: ellipsis;
4486
- white-space: nowrap;
4487
- }
4488
-
4489
- .active-patrol-statuses {
4490
- align-items: center;
4491
- display: flex;
4492
- gap: 6px;
4493
- justify-content: flex-start;
4494
- min-width: 0;
4495
- }
4496
-
4497
- .active-patrol-status-pill {
4498
- border-radius: 20px;
4499
- font-size: 11px;
4500
- font-weight: 600;
4501
- line-height: 1.2;
4502
- padding: 5px 10px;
4503
- text-transform: capitalize;
4504
- white-space: nowrap;
4505
- }
4506
-
4507
- .active-patrol-more-pill {
4508
- align-items: center;
4509
- background: #eef3f8;
4510
- border: 1px solid #dbe4ed;
4511
- border-radius: 999px;
4512
- color: #607386;
4513
- display: inline-flex;
4514
- flex: 0 0 auto;
4515
- font-size: 11px;
4516
- font-weight: 800;
4517
- height: 24px;
4518
- justify-content: center;
4519
- line-height: 1;
4520
- min-width: 30px;
4521
- padding: 0 8px;
4522
- position: relative;
4523
- white-space: nowrap;
4524
- }
4525
-
4526
- .active-patrol-action {
4527
- justify-self: end;
4528
- text-align: right;
4529
- }
4530
-
4531
- .active-patrol-tooltip::after {
4532
- background: #0f2638;
4533
- border-radius: 6px;
4534
- bottom: calc(100% + 8px);
4535
- box-shadow: 0 8px 24px rgba(15, 38, 56, 0.18);
4536
- color: #ffffff;
4537
- content: attr(data-tooltip);
4538
- font-size: 11px;
4539
- font-weight: 600;
4540
- left: 50%;
4541
- line-height: 1.35;
4542
- max-width: 260px;
4543
- opacity: 0;
4544
- padding: 7px 9px;
4545
- pointer-events: none;
4546
- position: absolute;
4547
- text-align: left;
4548
- transform: translate(-50%, 4px);
4549
- transition: opacity 0.14s ease, transform 0.14s ease;
4550
- white-space: normal;
4551
- width: max-content;
4552
- z-index: 5;
4553
- }
4554
-
4555
- .active-patrol-tooltip:hover::after {
4556
- opacity: 1;
4557
- transform: translate(-50%, 0);
4558
- }
4559
-
4560
- @media (max-width: 960px) {
4561
- .active-patrol-row {
4562
- grid-template-columns: 120px minmax(0, 1fr) max-content max-content;
4563
- }
4564
- }
4565
-
4566
4425
  .figma-icon-badge {
4567
4426
  align-items: center;
4568
4427
  border-radius: 50%;
@@ -7,7 +7,7 @@
7
7
  <template v-if="fileTypes?.[x] === 'image'">
8
8
  <v-carousel-item height="100%" width="100%" rounded="lg">
9
9
  <v-row no-gutters class="w-100 h-100" align="center">
10
- <v-img :lazy-src="getFinalUrl(x)" :src="getFinalUrl(x)" width="70%" height="70%"
10
+ <v-img :lazy-src="getFileUrl(x)" :src="getFileUrl(x)" width="70%" height="70%"
11
11
  :alt="'Image Viewer Card -' + index"></v-img>
12
12
  </v-row>
13
13
  <template v-slot:placeholder>
@@ -21,7 +21,7 @@
21
21
  <v-carousel-item>
22
22
  <v-row no-gutters class="h-100 w-100" align="center" justify="center">
23
23
  <video width="80%" height="80%" controls>
24
- <source :src="getFinalUrl(x)" />
24
+ <source :src="getFileUrl(x)" />
25
25
  </video>
26
26
  </v-row>
27
27
  <template v-slot:placeholder>
@@ -78,19 +78,11 @@ const props = defineProps({
78
78
  files: {
79
79
  type: Array as PropType<string[]>,
80
80
  default: []
81
- },
82
- site: {
83
- type: String,
84
- default: ""
85
- },
86
- anpr: {
87
- type: Boolean,
88
- default: false
89
81
  }
90
82
  });
91
83
 
92
84
 
93
- const { getFileUrl, urlToFile, getFileUrlAnpr } = useFile()
85
+ const { getFileUrl, urlToFile } = useFile()
94
86
  const overlay = defineModel({ required: true, default: false });
95
87
  const activeIndex = ref(0);
96
88
  const fileTypes = ref<Record<string, "image" | "video" | "other">>({});
@@ -115,13 +107,10 @@ async function resolveFileTypes() {
115
107
 
116
108
  for (const x of props.files) {
117
109
  try {
118
- let url = ""
119
- if (props.anpr && props.site) {
120
- url = getFileUrlAnpr(`${props.site}/${x}`)
121
- } else url = getFileUrl(x);
110
+ const url = getFileUrl(x);
122
111
  const file = await urlToFile(url, x);
123
112
  const type = file?.type;
124
-
113
+
125
114
 
126
115
  if (type?.startsWith("video")) fileTypes.value[x] = "video";
127
116
  else if (type?.startsWith("image")) fileTypes.value[x] = "image";
@@ -140,13 +129,6 @@ watch(
140
129
  );
141
130
 
142
131
 
143
- const getFinalUrl = (id: string) => {
144
- if (props.anpr && props.site) {
145
- return getFileUrlAnpr(`${props.site}/${id}`)
146
- } else return getFileUrl(id)
147
- }
148
-
149
-
150
132
  </script>
151
133
 
152
134
  <style scoped>
@@ -304,38 +304,31 @@
304
304
 
305
305
  <v-divider />
306
306
 
307
- <v-toolbar class="pa-0" density="compact">
308
- <v-row no-gutters>
309
- <v-col cols="6" class="pa-0">
310
- <v-btn
311
- block
312
- variant="text"
313
- class="text-none"
314
- size="large"
315
- height="48"
316
- :disabled="rejectModal.submitting"
317
- @click="closeRejectDialog"
318
- >
319
- Cancel
320
- </v-btn>
321
- </v-col>
322
-
323
- <v-col cols="6" class="pa-0">
324
- <v-btn
325
- block
326
- variant="flat"
327
- color="black"
328
- class="text-none font-weight-bold rounded-0"
329
- height="48"
330
- :loading="rejectModal.submitting"
331
- :disabled="!isRejectModalValid || rejectModal.submitting"
332
- @click="submitRejectDialog"
333
- >
334
- Confirm
335
- </v-btn>
336
- </v-col>
337
- </v-row>
338
- </v-toolbar>
307
+ <v-card-actions class="pa-0">
308
+ <v-btn
309
+ color="grey"
310
+ variant="flat"
311
+ rounded="0"
312
+ class="flex-grow-1 text-none"
313
+ height="48"
314
+ :disabled="rejectModal.submitting"
315
+ @click="closeRejectDialog"
316
+ >
317
+ Cancel
318
+ </v-btn>
319
+ <v-btn
320
+ color="error"
321
+ variant="flat"
322
+ rounded="0"
323
+ class="flex-grow-1 text-none"
324
+ height="48"
325
+ :loading="rejectModal.submitting"
326
+ :disabled="!isRejectModalValid || rejectModal.submitting"
327
+ @click="submitRejectDialog"
328
+ >
329
+ Confirm
330
+ </v-btn>
331
+ </v-card-actions>
339
332
  </v-card>
340
333
  </v-dialog>
341
334
 
@@ -59,21 +59,6 @@
59
59
  </v-col>
60
60
  </v-row>
61
61
 
62
- <v-dialog v-model="showPhotoPreview" max-width="900">
63
- <v-card>
64
- <v-card-actions class="pa-2 justify-end">
65
- <v-btn
66
- icon="mdi-close"
67
- variant="text"
68
- @click="showPhotoPreview = false"
69
- />
70
- </v-card-actions>
71
- <v-card-text class="pa-2 pt-0">
72
- <v-img :src="selectedPhoto" contain max-height="80vh" />
73
- </v-card-text>
74
- </v-card>
75
- </v-dialog>
76
-
77
62
  <v-dialog v-model="photoOptionsDialog" max-width="420">
78
63
  <v-card>
79
64
  <v-card-title class="d-flex align-center pa-4">
@@ -211,8 +196,6 @@ const photos = ref<string[]>([...props.modelValue]);
211
196
  const photosIds = ref<string[]>([...props.photoIds]);
212
197
  const photoOptionsDialog = ref(false);
213
198
  const showCameraDialog = ref(false);
214
- const showPhotoPreview = ref(false);
215
- const selectedPhoto = ref("");
216
199
  const videoRef = ref<HTMLVideoElement | null>(null);
217
200
  const canvasRef = ref<HTMLCanvasElement | null>(null);
218
201
  const fileInputRef = ref<HTMLInputElement | null>(null);
@@ -222,8 +205,10 @@ const uploading = ref(false);
222
205
  const { addFile } = useFile();
223
206
 
224
207
  function viewPhoto(photoData: string) {
225
- selectedPhoto.value = photoData;
226
- showPhotoPreview.value = true;
208
+ const link = document.createElement("a");
209
+ link.href = photoData;
210
+ link.target = "_blank";
211
+ link.click();
227
212
  }
228
213
 
229
214
  watch(
@@ -19,7 +19,7 @@
19
19
  :error-messages="errorMessages"
20
20
  :readonly="!edit"
21
21
  :hint="
22
- edit && 'Select permissions'
22
+ edit ? 'Select permissions' : 'Permissions are not editable'
23
23
  "
24
24
  persistent-hint
25
25
  />
@@ -734,7 +734,7 @@ const {
734
734
  updateStatus: updateServiceProviderStatus,
735
735
  invite,
736
736
  } = useServiceProvider();
737
- const { getAll: getAllMembers, getAllByUserId } = useMember();
737
+ const { getAllByUserId } = useMember();
738
738
 
739
739
  const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
740
740
  useVerification();
@@ -757,9 +757,6 @@ const createInviteDialog = ref(false);
757
757
  // const loadingInviteRoles = ref(false);
758
758
  // const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
759
759
 
760
- const { getByUserType } = useMember();
761
- const { getOrgs } = useOrg();
762
- const { getByEmail } = useServiceProvider();
763
760
 
764
761
  async function checkExistingAccount(email: string) {
765
762
  existingAccount.value = null;
@@ -770,54 +767,40 @@ async function checkExistingAccount(email: string) {
770
767
  checkingExistingAccount.value = true;
771
768
 
772
769
  try {
773
- // Check if user exists
774
770
  const user = await getUserByEmail(email);
775
771
 
776
772
  if (!user?._id) return;
777
773
 
778
- // Check if this service provider already exists in current org/site
779
- const serviceProviders = await getByEmail({
780
- email,
781
- orgId: props.orgId,
782
- siteId: props.siteId,
783
- });
784
-
785
- console.log("serviceProviders", serviceProviders);
786
-
787
- if (serviceProviders?.length) {
788
- messageServiceProvider.value =
789
- "This service provider already exists.";
790
- return;
791
- }
774
+ const members = await getAllByUserId(user._id);
792
775
 
793
- // Check existing memberships
794
- const members = await getAllByUserId(user._id);
776
+ const appMember = (members.items ?? []).find(
777
+ (member: any) => member.type === serviceProviderInvite.value.app
778
+ );
795
779
 
796
- const appMember = (members.items ?? []).find(
797
- (member: any) => member.type === serviceProviderInvite.value.app
798
- );
780
+ if (!appMember) {
781
+ existingAccount.value = null;
782
+ return;
783
+ }
799
784
 
800
- if (!appMember) return;
785
+ if (
786
+ appMember.org === props.orgId &&
787
+ (appMember.siteId || "") === (props.siteId || "")
788
+ ) {
789
+ existingAccount.value = null;
790
+ messageServiceProvider.value =
791
+ "This user is already a member of this service provider.";
792
+ return;
793
+ }
801
794
 
802
- // Already belongs to the same org/site
803
- if (
804
- appMember.org === props.orgId &&
805
- (appMember.siteId || "") === (props.siteId || "")
806
- ) {
807
- messageServiceProvider.value =
808
- "This user is already a member of this service provider.";
809
- return;
810
- }
795
+ existingAccount.value = {
796
+ _id: appMember.org,
797
+ text: appMember.orgName,
798
+ org: appMember.org,
799
+ orgName: appMember.orgName,
800
+ siteId: appMember.siteId,
801
+ siteName: appMember.siteName,
802
+ };
811
803
 
812
- // Existing account in another org/site
813
- existingAccount.value = {
814
- _id: appMember.org,
815
- text: appMember.orgName,
816
- org: appMember.org,
817
- orgName: appMember.orgName,
818
- siteId: appMember.siteId,
819
- siteName: appMember.siteName,
820
- };
821
804
  } catch (error) {
822
805
  existingAccount.value = null;
823
806
  } finally {
@@ -36,17 +36,10 @@
36
36
  </template>
37
37
 
38
38
  <v-list v-if="nricSearchResults.length" class="pa-1 phone-search-results">
39
- <div class="search-results-scroll">
40
- <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
41
- @click="selectNricSearchResult(item)" class="cursor-pointer nric-search-item">
42
- <VisitorSearchResultSummary :item="item" />
43
- </v-list-item>
44
- </div>
45
- <div class="search-results-close pa-1">
46
- <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('nric')">
47
- Close
48
- </v-btn>
49
- </div>
39
+ <v-list-item v-for="(item, index) in nricSearchResults" :key="item._id || index"
40
+ @click="selectNricSearchResult(item)" class="cursor-pointer nric-search-item">
41
+ <VisitorSearchResultSummary :item="item" />
42
+ </v-list-item>
50
43
  </v-list>
51
44
  </v-menu>
52
45
  </v-col>
@@ -65,17 +58,10 @@
65
58
  </template>
66
59
 
67
60
  <v-list v-if="phoneSearchResults.length" class="pa-1 phone-search-results">
68
- <div class="search-results-scroll">
69
- <v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
70
- @click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
71
- <VisitorSearchResultSummary :item="item" />
72
- </v-list-item>
73
- </div>
74
- <div class="search-results-close pa-1">
75
- <v-btn block variant="flat" color="red" @click.stop="closeAutofillSearchResults('contact')">
76
- Close
77
- </v-btn>
78
- </div>
61
+ <v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
62
+ @click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
63
+ <VisitorSearchResultSummary :item="item" />
64
+ </v-list-item>
79
65
  </v-list>
80
66
  </v-menu>
81
67
  </v-col>
@@ -91,23 +77,15 @@
91
77
  <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
92
78
  <template #activator="{ props }">
93
79
  <InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
94
- @update:model-value="handleUpdateVehicleNumber" @focus="handleFocusPlateNumber"
80
+ @update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true"
95
81
  @blur="isPlateNumberFieldFocused = false" />
96
82
  </template>
97
83
 
98
84
  <v-list v-if="vehicleSearchResults.length" class="pa-1 phone-search-results">
99
- <div class="search-results-scroll">
100
- <v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
101
- @click="selectPlateNumberSearchResult(item)" class="cursor-pointer nric-search-item">
102
- <VisitorSearchResultSummary :item="item" />
103
- </v-list-item>
104
- </div>
105
- <div class="search-results-close pa-1">
106
- <v-btn block variant="flat" color="red"
107
- @click.stop="closeAutofillSearchResults('vehicleNumber')">
108
- Close
109
- </v-btn>
110
- </div>
85
+ <v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
86
+ @click="selectPlateNumberSearchResult(item)" class="cursor-pointer nric-search-item">
87
+ <VisitorSearchResultSummary :item="item" />
88
+ </v-list-item>
111
89
  </v-list>
112
90
  </v-menu>
113
91
  </v-col>
@@ -330,10 +308,10 @@
330
308
 
331
309
  <div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
332
310
  <span>{{ selectedNricSearchResult.name || "Unknown" }}</span>
333
- <span v-if="!visitor.nric || currentAutofillSource === 'nric'" class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
311
+ <span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
334
312
  </div>
335
313
 
336
- <template v-if="(!visitor.contact || currentAutofillSource === 'contact') && selectedNricContactOptions.length >= 1">
314
+ <template v-if="!visitor.contact && selectedNricContactOptions.length >= 1">
337
315
  <h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
338
316
  <v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
339
317
  <v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
@@ -341,7 +319,7 @@
341
319
  </v-radio-group>
342
320
  </template>
343
321
 
344
- <template v-if="(!visitor.plateNumber || currentAutofillSource === 'vehicleNumber') && selectedNricPlateOptions.length >= 1">
322
+ <template v-if="!visitor.plateNumber && selectedNricPlateOptions.length >= 1">
345
323
  <h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
346
324
  <v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
347
325
  <v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
@@ -1066,8 +1044,8 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
1066
1044
  const selectedPlate =
1067
1045
  findMatchingPlateOption(plateOptions, visitor.plateNumber) || plateOptions[0] || "";
1068
1046
 
1069
- const needsContactChoice = (!visitor.contact?.trim() || source === 'contact') && contactOptions.length > 1;
1070
- const needsPlateChoice = (!visitor.plateNumber?.trim() || source === 'vehicleNumber') && plateOptions.length > 1;
1047
+ const needsContactChoice = !visitor.contact?.trim() && contactOptions.length > 1;
1048
+ const needsPlateChoice = !visitor.plateNumber?.trim() && plateOptions.length > 1;
1071
1049
 
1072
1050
  if (needsContactChoice || needsPlateChoice) {
1073
1051
  nricSearchMenu.value = false;
@@ -1088,23 +1066,6 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
1088
1066
  });
1089
1067
  }
1090
1068
 
1091
- function closeAutofillSearchResults(source: Exclude<AutofillSource, null>) {
1092
- if (source === "nric") {
1093
- nricSearchMenu.value = false;
1094
- nricSearchResults.value = [];
1095
- }
1096
-
1097
- if (source === "contact") {
1098
- phoneSearchMenu.value = false;
1099
- phoneSearchResults.value = [];
1100
- }
1101
-
1102
- if (source === "vehicleNumber") {
1103
- vehicleSearchMenu.value = false;
1104
- vehicleSearchResults.value = [];
1105
- }
1106
- }
1107
-
1108
1069
  function closeNricAutofillOptions() {
1109
1070
  dialog.nricAutofillOptions = false;
1110
1071
  selectedNricSearchResult.value = null;
@@ -1542,15 +1503,6 @@ function handleUpdateVehicleNumber() {
1542
1503
  debounceFetchPlateNumber();
1543
1504
  }
1544
1505
 
1545
- const handleFocusPlateNumber = () => {
1546
- isPlateNumberFieldFocused.value = true;
1547
- if (prop.mode === 'register') {
1548
-
1549
- handleUpdateVehicleNumber();
1550
- }
1551
- };
1552
-
1553
-
1554
1506
  function backToSelection() {
1555
1507
  emit("back");
1556
1508
  message.value = "";
@@ -2156,22 +2108,10 @@ onMounted(() => {
2156
2108
  }
2157
2109
 
2158
2110
  .phone-search-results {
2159
- display: flex;
2160
- flex-direction: column;
2161
2111
  max-height: 400px;
2162
- overflow: hidden;
2163
- }
2164
-
2165
- .search-results-scroll {
2166
- flex: 1 1 auto;
2167
- min-height: 0;
2168
2112
  overflow-y: auto;
2169
2113
  }
2170
2114
 
2171
- .search-results-close {
2172
- flex: 0 0 auto;
2173
- }
2174
-
2175
2115
  .nric-options-card {
2176
2116
  border-radius: 16px !important;
2177
2117
  }
@@ -675,24 +675,6 @@
675
675
  />
676
676
  </div>
677
677
  </span>
678
- <span
679
- v-else-if="
680
- key === 'snapshotEntryImage' || key === 'snapshotExitImage'
681
- "
682
- class="d-flex flex-column"
683
- >
684
- <strong class="w-full">{{ label }}:</strong>
685
- <div class="d-flex flex-wrap ga-2">
686
- <AttachmentsViewer
687
- :files="
688
- mappedSnapshots([selectedVisitorObject.snapshotEntryImage])
689
- "
690
- anpr
691
- :site="siteId"
692
- title="Snapshot"
693
- />
694
- </div>
695
- </span>
696
678
 
697
679
  <span
698
680
  v-else-if="selectedVisitorObject[key]"
@@ -843,8 +825,10 @@
843
825
  item-title="label"
844
826
  item-value="value"
845
827
  v-model="pass.status"
846
- :disabled="pass.status == 'Removed'"
847
- />
828
+ :disabled="
829
+ selectedVisitorObject.checkOut || pass.status == 'Removed'
830
+ "
831
+ ></v-select>
848
832
  <v-textarea
849
833
  v-if="pass.status === 'Lost' || pass.status === 'Damaged'"
850
834
  no-resize
@@ -852,7 +836,8 @@
852
836
  class="w-100"
853
837
  density="compact"
854
838
  v-model="pass.remarks"
855
- />
839
+ :disabled="selectedVisitorObject.checkOut"
840
+ ></v-textarea>
856
841
  </v-row>
857
842
  </div>
858
843
  <div v-if="keyReturnStatuses.length > 0" class="mb-2">
@@ -879,8 +864,10 @@
879
864
  item-title="label"
880
865
  item-value="value"
881
866
  v-model="key.status"
882
- :disabled="key.status == 'Removed'"
883
- />
867
+ :disabled="
868
+ selectedVisitorObject.checkOut || key.status == 'Removed'
869
+ "
870
+ ></v-select>
884
871
  <v-textarea
885
872
  v-if="key.status === 'Lost' || key.status === 'Damaged'"
886
873
  no-resize
@@ -888,7 +875,8 @@
888
875
  class="w-100"
889
876
  density="compact"
890
877
  v-model="key.remarks"
891
- />
878
+ :disabled="selectedVisitorObject.checkOut"
879
+ ></v-textarea>
892
880
  </v-row>
893
881
  </div>
894
882
 
@@ -898,11 +886,11 @@
898
886
  color="blue"
899
887
  density="comfortable"
900
888
  class="text-capitalize"
889
+ :disabled="selectedVisitorObject.checkOut"
901
890
  :loading="loading.updatingPassKeys"
902
891
  @click.stop="handleUpdatePassKeys"
892
+ >Update Pass/Keys</v-btn
903
893
  >
904
- Update Pass/Keys
905
- </v-btn>
906
894
  </v-row>
907
895
  </v-card-text>
908
896
  <v-toolbar class="pa-0" density="compact">
@@ -912,9 +900,8 @@
912
900
  variant="text"
913
901
  block
914
902
  @click="dialog.returnPassesKeys = false"
903
+ >Close</v-btn
915
904
  >
916
- Close
917
- </v-btn>
918
905
  </v-col>
919
906
  <v-col cols="6">
920
907
  <v-btn
@@ -928,9 +915,8 @@
928
915
  !canConfirmCheckout || selectedVisitorObject.checkOut
929
916
  "
930
917
  @click="proceedCheckout"
918
+ >Confirm Checkout</v-btn
931
919
  >
932
- Confirm Checkout
933
- </v-btn>
934
920
  </v-col>
935
921
  </v-row>
936
922
  </v-toolbar>
@@ -1456,15 +1442,6 @@ function mappedAttachments(attachments: string[]) {
1456
1442
  });
1457
1443
  }
1458
1444
 
1459
- function mappedSnapshots(attachments: string[]) {
1460
- return attachments.map((x, index) => {
1461
- return {
1462
- id: x,
1463
- name: `Snapshot - ${x}`,
1464
- };
1465
- });
1466
- }
1467
-
1468
1445
  function normalizeDateOnly(value: string) {
1469
1446
  if (!value) return "";
1470
1447
  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
@@ -1,23 +1,58 @@
1
1
  export function useCleaningSchedulePermission() {
2
+ const { hasPermission } = usePermission();
3
+ const { permissions } = useCleaningPermission();
4
+
2
5
  const { userAppRole } = useLocalSetup();
3
6
 
4
- const can = (action: string) =>
5
- computed(() => {
6
- const permissions = userAppRole.value?.permissions;
7
- if (!permissions) return false;
8
- if (permissions.includes("*")) return true;
9
- return permissions.some((permission) =>
10
- permission.endsWith(`-schedule-mgmt:${action}`),
11
- );
12
- });
7
+ const canViewSchedules = computed(() => {
8
+ if (!userAppRole.value) return false;
9
+ if (userAppRole.value.permissions.includes("*")) return true;
10
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "see-all-schedules");
11
+ });
12
+
13
+ const canViewScheduleDetails = computed(() => {
14
+ if (!userAppRole.value) return false;
15
+ if (userAppRole.value.permissions.includes("*")) return true;
16
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "see-schedule-details");
17
+ });
18
+
19
+ const canDownloadSchedule = computed(() => {
20
+ if (!userAppRole.value) return false;
21
+ if (userAppRole.value.permissions.includes("*")) return true;
22
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "download-schedule");
23
+ });
24
+
25
+ const canManageScheduleTasks = computed(() => {
26
+ if (!userAppRole.value) return false;
27
+ if (userAppRole.value.permissions.includes("*")) return true;
28
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "manage-schedule-tasks");
29
+ });
30
+
31
+ const canGenerateChecklist = computed(() => {
32
+ if (!userAppRole.value) return false;
33
+ if (userAppRole.value.permissions.includes("*")) return true;
34
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "generate-checklist");
35
+ });
36
+
37
+ const canViewHistory = computed(() => {
38
+ if (!userAppRole.value) return false;
39
+ if (userAppRole.value.permissions.includes("*")) return true;
40
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "view-history");
41
+ });
42
+
43
+ const canAddRemarks = computed(() => {
44
+ if (!userAppRole.value) return false;
45
+ if (userAppRole.value.permissions.includes("*")) return true;
46
+ return hasPermission(userAppRole.value, permissions, "cleaning-schedule-mgmt", "add-remarks");
47
+ });
13
48
 
14
49
  return {
15
- canViewSchedules: can("see-all-schedules"),
16
- canViewScheduleDetails: can("see-schedule-details"),
17
- canDownloadSchedule: can("download-schedule"),
18
- canManageScheduleTasks: can("manage-schedule-tasks"),
19
- canGenerateChecklist: can("generate-checklist"),
20
- canViewHistory: can("view-history"),
21
- canAddRemarks: can("add-remarks"),
50
+ canViewSchedules,
51
+ canViewScheduleDetails,
52
+ canDownloadSchedule,
53
+ canManageScheduleTasks,
54
+ canGenerateChecklist,
55
+ canViewHistory,
56
+ canAddRemarks,
22
57
  };
23
58
  }
@@ -236,26 +236,6 @@ export default function useServiceProvider() {
236
236
  );
237
237
  }
238
238
 
239
- async function getByEmail({
240
- email = "",
241
- orgId = "",
242
- siteId = "",
243
- serviceProviderOrgId = "",
244
- } = {}) {
245
- return useNuxtApp().$api<Record<string, any>>(
246
- "/api/service-providers/email",
247
- {
248
- method: "GET",
249
- query: {
250
- email,
251
- ...(orgId ? { orgId } : {}),
252
- ...(siteId ? { siteId } : {}),
253
- ...(serviceProviderOrgId ? { serviceProviderOrgId } : {}),
254
- },
255
- }
256
- );
257
- }
258
-
259
239
  return {
260
240
  serviceProviders,
261
241
  serviceProvider,
@@ -274,6 +254,5 @@ export default function useServiceProvider() {
274
254
  add,
275
255
  addViaInvite,
276
256
  updateStatus,
277
- getByEmail,
278
257
  };
279
258
  }
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.1.3-staging.45",
5
+ "version": "3.1.3",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {