@7365admin1/layer-common 3.1.3 → 3.1.4-staging.46
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/.github/workflows/publish-staging.yml +11 -0
- package/components/AttachmentsViewer.vue +22 -4
- package/components/DashboardMain.vue +228 -87
- package/components/ImageCarousel.vue +23 -5
- package/components/ManageChecklistMain.vue +32 -25
- package/components/MemberMain.vue +17 -15
- package/components/PhotoUpload.vue +19 -4
- package/components/RolePermissionFormPreviewUpdate.vue +1 -1
- package/components/ServiceProviderMain.vue +43 -26
- package/components/VisitorForm.vue +78 -18
- package/components/VisitorManagement.vue +39 -16
- package/composables/useCleaningSchedulePermission.ts +16 -51
- package/composables/useServiceProvider.ts +21 -0
- package/package.json +1 -1
|
@@ -34,3 +34,14 @@ 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"
|
|
@@ -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="
|
|
51
|
+
<v-img :src="imageCover(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" />
|
|
100
|
+
<ImageCarousel v-model="showCarousel" :active-file-id="activeFileId" :files="carouselFiles" :anpr="anpr" :site="site" />
|
|
101
101
|
|
|
102
102
|
<!-- Document Viewer -->
|
|
103
103
|
<DocumentViewer v-model="showDocumentViewer" :file-id="activeFileId" :file-name="activeFileName"
|
|
@@ -123,10 +123,18 @@ 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: ""
|
|
126
134
|
}
|
|
127
135
|
});
|
|
128
136
|
|
|
129
|
-
const { getFileUrl, urlToFile, getFileById } = useFile();
|
|
137
|
+
const { getFileUrl, urlToFile, getFileById, getFileUrlAnpr } = useFile();
|
|
130
138
|
|
|
131
139
|
const rawFileArray = ref<{ file: File, _id: string, preview: boolean, mimeType: string }[]>([]);
|
|
132
140
|
const loading = ref(true);
|
|
@@ -159,6 +167,12 @@ function openDocumentViewer(id: string, name: string, mimeType: string) {
|
|
|
159
167
|
showDocumentViewer.value = true;
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
const imageCover = (id: string) => {
|
|
171
|
+
if(props.anpr && props.site){
|
|
172
|
+
return getFileUrlAnpr(`${props.site}/${id}`)
|
|
173
|
+
} else return getFileUrl(id)
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
function getFileIcon(mimeType: string): string {
|
|
163
177
|
if (mimeType.startsWith('image/')) return 'mdi-image';
|
|
164
178
|
if (mimeType.startsWith('video/')) return 'mdi-video';
|
|
@@ -206,7 +220,11 @@ watchEffect(async () => {
|
|
|
206
220
|
|
|
207
221
|
try {
|
|
208
222
|
// Try to get the actual file with proper MIME type detection from blob
|
|
209
|
-
const fileUrl = getFileUrl(item.id);
|
|
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);
|
|
210
228
|
file = await urlToFile(fileUrl, item.name);
|
|
211
229
|
mimeType = file.type || 'application/octet-stream';
|
|
212
230
|
|
|
@@ -984,60 +984,44 @@
|
|
|
984
984
|
<div
|
|
985
985
|
v-for="item in formattedActivePatrolItems"
|
|
986
986
|
:key="item.id || item._id"
|
|
987
|
-
class="
|
|
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
|
-
"
|
|
987
|
+
class="active-patrol-row"
|
|
996
988
|
>
|
|
997
|
-
<div
|
|
998
|
-
<strong
|
|
999
|
-
|
|
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>
|
|
989
|
+
<div class="active-patrol-main">
|
|
990
|
+
<strong>{{ item.title || item.name }}</strong>
|
|
991
|
+
<span>{{ item.subtitle || item.location || "" }}</span>
|
|
1011
992
|
</div>
|
|
1012
993
|
|
|
1013
|
-
<div
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
item.
|
|
1022
|
-
|
|
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>
|
|
1023
1006
|
</div>
|
|
1024
1007
|
|
|
1025
|
-
<div
|
|
1008
|
+
<div class="active-patrol-statuses">
|
|
1026
1009
|
<div
|
|
1027
|
-
|
|
1028
|
-
style="
|
|
1029
|
-
padding: 6px 16px;
|
|
1030
|
-
border-radius: 20px;
|
|
1031
|
-
font-size: 13px;
|
|
1032
|
-
font-weight: 500;
|
|
1033
|
-
text-transform: capitalize;
|
|
1034
|
-
"
|
|
1010
|
+
class="active-patrol-status-pill"
|
|
1011
|
+
:style="getPatrolStatusStyle(item.primaryStatus)"
|
|
1035
1012
|
>
|
|
1036
|
-
{{ item.
|
|
1013
|
+
{{ item.primaryStatus }}
|
|
1037
1014
|
</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>
|
|
1038
1022
|
</div>
|
|
1039
1023
|
|
|
1040
|
-
<div
|
|
1024
|
+
<div class="active-patrol-action">
|
|
1041
1025
|
<v-btn
|
|
1042
1026
|
variant="outlined"
|
|
1043
1027
|
class="text-none font-weight-medium"
|
|
@@ -1545,30 +1529,16 @@ const { userAppRole } = useLocalSetup();
|
|
|
1545
1529
|
const { downloadPDF } = usePDFDownload();
|
|
1546
1530
|
const route = useRoute();
|
|
1547
1531
|
|
|
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
|
-
|
|
1562
1532
|
const propertyWidgetPermissions: Record<string, string[]> = {
|
|
1563
1533
|
workOrders: [
|
|
1564
|
-
"
|
|
1534
|
+
"work_orders:see-all-work-orders",
|
|
1565
1535
|
"work-order:see-all-work-orders",
|
|
1566
1536
|
],
|
|
1567
1537
|
incidents: ["incident-reports:see-incident-reports"],
|
|
1568
1538
|
visitors: ["visitorManagement:see-all-visitor"],
|
|
1569
1539
|
facilityBookings: ["facility-booking-mgmt:see-all-facility-booking"],
|
|
1570
1540
|
activity: [
|
|
1571
|
-
"
|
|
1541
|
+
"work_orders:see-all-work-orders",
|
|
1572
1542
|
"work-order:see-all-work-orders",
|
|
1573
1543
|
],
|
|
1574
1544
|
upcomingEvents: ["event-mgmt:see-all-event"],
|
|
@@ -1578,7 +1548,7 @@ const propertyWidgetPermissions: Record<string, string[]> = {
|
|
|
1578
1548
|
"facility-booking-mgmt:see-all-facility-booking",
|
|
1579
1549
|
],
|
|
1580
1550
|
workOrderStatus: [
|
|
1581
|
-
"
|
|
1551
|
+
"work_orders:see-all-work-orders",
|
|
1582
1552
|
"work-order:see-all-work-orders",
|
|
1583
1553
|
],
|
|
1584
1554
|
feedbacks: ["feedbacks:see-all-feedback"],
|
|
@@ -1603,14 +1573,14 @@ const moduleWidgetPermissions: Record<string, string[]> = {
|
|
|
1603
1573
|
"work_order:see-all-work-orders",
|
|
1604
1574
|
"work-order:see-all-work-orders",
|
|
1605
1575
|
"work-orders:see-all-work-orders",
|
|
1576
|
+
"work_orders:see-all-work-orders",
|
|
1606
1577
|
],
|
|
1607
|
-
|
|
1608
|
-
"
|
|
1609
|
-
"
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
"
|
|
1613
|
-
"schedule-task-mgmt:see-all-schedule-tasks",
|
|
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",
|
|
1614
1584
|
],
|
|
1615
1585
|
staffStatus: ["attendance-mgmt:see-all-attendance"],
|
|
1616
1586
|
attendance: ["attendance-mgmt:see-all-attendance"],
|
|
@@ -1633,9 +1603,14 @@ const moduleWidgetPermissions: Record<string, string[]> = {
|
|
|
1633
1603
|
activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs"],
|
|
1634
1604
|
};
|
|
1635
1605
|
|
|
1606
|
+
function hasSchedulePermission(permissions: string[], action: string) {
|
|
1607
|
+
return permissions.some((permission) =>
|
|
1608
|
+
permission.endsWith(`-schedule-mgmt:${action}`)
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1636
1612
|
function canViewWidget(key: string) {
|
|
1637
1613
|
const permissions = userAppRole.value?.permissions;
|
|
1638
|
-
const dashboardPermission = dashboardWidgetPermissions[key];
|
|
1639
1614
|
const modulePermissions = isPropertyMode.value
|
|
1640
1615
|
? propertyWidgetPermissions[key]
|
|
1641
1616
|
: moduleWidgetPermissions[key];
|
|
@@ -1643,20 +1618,22 @@ function canViewWidget(key: string) {
|
|
|
1643
1618
|
if (!permissions) return true;
|
|
1644
1619
|
if (permissions.includes("*")) return true;
|
|
1645
1620
|
|
|
1621
|
+
if (
|
|
1622
|
+
!isPropertyMode.value &&
|
|
1623
|
+
["taskSchedule", "taskCompleted"].includes(key)
|
|
1624
|
+
) {
|
|
1625
|
+
return hasSchedulePermission(permissions, "see-all-schedules");
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1646
1628
|
if (isPropertyMode.value && modulePermissions) {
|
|
1647
1629
|
return modulePermissions.some((permission) =>
|
|
1648
1630
|
permissions.includes(permission)
|
|
1649
1631
|
);
|
|
1650
1632
|
}
|
|
1651
1633
|
|
|
1652
|
-
if (
|
|
1653
|
-
return (
|
|
1654
|
-
|
|
1655
|
-
Boolean(
|
|
1656
|
-
modulePermissions?.some((permission) =>
|
|
1657
|
-
permissions.includes(permission)
|
|
1658
|
-
)
|
|
1659
|
-
)
|
|
1634
|
+
if (modulePermissions) {
|
|
1635
|
+
return modulePermissions.some((permission) =>
|
|
1636
|
+
permissions.includes(permission)
|
|
1660
1637
|
);
|
|
1661
1638
|
}
|
|
1662
1639
|
|
|
@@ -2774,18 +2751,23 @@ const activePatrolItems = computed<any[]>(
|
|
|
2774
2751
|
|
|
2775
2752
|
const formattedActivePatrolItems = computed<any[]>(() => {
|
|
2776
2753
|
return activePatrolItems.value.map((item) => {
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
if (Array.isArray(person)) {
|
|
2783
|
-
person = person.join(", ");
|
|
2784
|
-
}
|
|
2754
|
+
const statuses = normalizeListValue(item.status || "Pending");
|
|
2755
|
+
const assignees = normalizeListValue(
|
|
2756
|
+
item.person || item.assignedTo || "Unassigned"
|
|
2757
|
+
);
|
|
2758
|
+
|
|
2785
2759
|
return {
|
|
2786
2760
|
...item,
|
|
2787
|
-
status,
|
|
2788
|
-
|
|
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),
|
|
2789
2771
|
};
|
|
2790
2772
|
});
|
|
2791
2773
|
});
|
|
@@ -3955,6 +3937,20 @@ function normalizeStatus(value: any) {
|
|
|
3955
3937
|
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3956
3938
|
}
|
|
3957
3939
|
|
|
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
|
+
|
|
3958
3954
|
function getStatusTone(status: any) {
|
|
3959
3955
|
const s = String(status || "").toLowerCase();
|
|
3960
3956
|
if (
|
|
@@ -4422,6 +4418,151 @@ function formatDashboardTime(value?: string) {
|
|
|
4422
4418
|
border: 2px solid #f9c97c;
|
|
4423
4419
|
}
|
|
4424
4420
|
|
|
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
|
+
|
|
4425
4566
|
.figma-icon-badge {
|
|
4426
4567
|
align-items: center;
|
|
4427
4568
|
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="
|
|
10
|
+
<v-img :lazy-src="getFinalUrl(x)" :src="getFinalUrl(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="
|
|
24
|
+
<source :src="getFinalUrl(x)" />
|
|
25
25
|
</video>
|
|
26
26
|
</v-row>
|
|
27
27
|
<template v-slot:placeholder>
|
|
@@ -78,11 +78,19 @@ 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
|
|
81
89
|
}
|
|
82
90
|
});
|
|
83
91
|
|
|
84
92
|
|
|
85
|
-
const { getFileUrl, urlToFile } = useFile()
|
|
93
|
+
const { getFileUrl, urlToFile, getFileUrlAnpr } = useFile()
|
|
86
94
|
const overlay = defineModel({ required: true, default: false });
|
|
87
95
|
const activeIndex = ref(0);
|
|
88
96
|
const fileTypes = ref<Record<string, "image" | "video" | "other">>({});
|
|
@@ -107,10 +115,13 @@ async function resolveFileTypes() {
|
|
|
107
115
|
|
|
108
116
|
for (const x of props.files) {
|
|
109
117
|
try {
|
|
110
|
-
|
|
118
|
+
let url = ""
|
|
119
|
+
if (props.anpr && props.site) {
|
|
120
|
+
url = getFileUrlAnpr(`${props.site}/${x}`)
|
|
121
|
+
} else url = getFileUrl(x);
|
|
111
122
|
const file = await urlToFile(url, x);
|
|
112
123
|
const type = file?.type;
|
|
113
|
-
|
|
124
|
+
|
|
114
125
|
|
|
115
126
|
if (type?.startsWith("video")) fileTypes.value[x] = "video";
|
|
116
127
|
else if (type?.startsWith("image")) fileTypes.value[x] = "image";
|
|
@@ -129,6 +140,13 @@ watch(
|
|
|
129
140
|
);
|
|
130
141
|
|
|
131
142
|
|
|
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
|
+
|
|
132
150
|
</script>
|
|
133
151
|
|
|
134
152
|
<style scoped>
|
|
@@ -304,31 +304,38 @@
|
|
|
304
304
|
|
|
305
305
|
<v-divider />
|
|
306
306
|
|
|
307
|
-
<v-
|
|
308
|
-
<v-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
class="
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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>
|
|
332
339
|
</v-card>
|
|
333
340
|
</v-dialog>
|
|
334
341
|
|
|
@@ -149,21 +149,23 @@
|
|
|
149
149
|
</template>
|
|
150
150
|
|
|
151
151
|
<template #footer>
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
152
|
+
<div class="d-flex justify-center ga-3 w-100">
|
|
153
|
+
<v-btn
|
|
154
|
+
variant="text"
|
|
155
|
+
@click="confirmDialog = false"
|
|
156
|
+
:disabled="updateLoading"
|
|
157
|
+
>
|
|
158
|
+
Close
|
|
159
|
+
</v-btn>
|
|
160
|
+
<v-btn
|
|
161
|
+
color="primary"
|
|
162
|
+
variant="flat"
|
|
163
|
+
@click="handleUpdateMemberStatus"
|
|
164
|
+
:loading="updateLoading"
|
|
165
|
+
>
|
|
166
|
+
{{ updateActionText }} Member
|
|
167
|
+
</v-btn>
|
|
168
|
+
</div>
|
|
167
169
|
</template>
|
|
168
170
|
</ConfirmDialog>
|
|
169
171
|
|
|
@@ -59,6 +59,21 @@
|
|
|
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
|
+
|
|
62
77
|
<v-dialog v-model="photoOptionsDialog" max-width="420">
|
|
63
78
|
<v-card>
|
|
64
79
|
<v-card-title class="d-flex align-center pa-4">
|
|
@@ -196,6 +211,8 @@ const photos = ref<string[]>([...props.modelValue]);
|
|
|
196
211
|
const photosIds = ref<string[]>([...props.photoIds]);
|
|
197
212
|
const photoOptionsDialog = ref(false);
|
|
198
213
|
const showCameraDialog = ref(false);
|
|
214
|
+
const showPhotoPreview = ref(false);
|
|
215
|
+
const selectedPhoto = ref("");
|
|
199
216
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
|
200
217
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
201
218
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
@@ -205,10 +222,8 @@ const uploading = ref(false);
|
|
|
205
222
|
const { addFile } = useFile();
|
|
206
223
|
|
|
207
224
|
function viewPhoto(photoData: string) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
link.target = "_blank";
|
|
211
|
-
link.click();
|
|
225
|
+
selectedPhoto.value = photoData;
|
|
226
|
+
showPhotoPreview.value = true;
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
watch(
|
|
@@ -734,7 +734,7 @@ const {
|
|
|
734
734
|
updateStatus: updateServiceProviderStatus,
|
|
735
735
|
invite,
|
|
736
736
|
} = useServiceProvider();
|
|
737
|
-
const { getAllByUserId } = useMember();
|
|
737
|
+
const { getAll: getAllMembers, getAllByUserId } = useMember();
|
|
738
738
|
|
|
739
739
|
const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
|
|
740
740
|
useVerification();
|
|
@@ -757,6 +757,9 @@ 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();
|
|
760
763
|
|
|
761
764
|
async function checkExistingAccount(email: string) {
|
|
762
765
|
existingAccount.value = null;
|
|
@@ -767,40 +770,54 @@ async function checkExistingAccount(email: string) {
|
|
|
767
770
|
checkingExistingAccount.value = true;
|
|
768
771
|
|
|
769
772
|
try {
|
|
773
|
+
// Check if user exists
|
|
770
774
|
const user = await getUserByEmail(email);
|
|
771
775
|
|
|
772
776
|
if (!user?._id) return;
|
|
773
777
|
|
|
774
|
-
|
|
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
|
+
});
|
|
775
784
|
|
|
776
|
-
|
|
777
|
-
(member: any) => member.type === serviceProviderInvite.value.app
|
|
778
|
-
);
|
|
785
|
+
console.log("serviceProviders", serviceProviders);
|
|
779
786
|
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
787
|
+
if (serviceProviders?.length) {
|
|
788
|
+
messageServiceProvider.value =
|
|
789
|
+
"This service provider already exists.";
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
784
792
|
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
}
|
|
793
|
+
// Check existing memberships
|
|
794
|
+
const members = await getAllByUserId(user._id);
|
|
794
795
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
siteId: appMember.siteId,
|
|
801
|
-
siteName: appMember.siteName,
|
|
802
|
-
};
|
|
796
|
+
const appMember = (members.items ?? []).find(
|
|
797
|
+
(member: any) => member.type === serviceProviderInvite.value.app
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
if (!appMember) return;
|
|
803
801
|
|
|
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
|
+
}
|
|
811
|
+
|
|
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
|
+
};
|
|
804
821
|
} catch (error) {
|
|
805
822
|
existingAccount.value = null;
|
|
806
823
|
} finally {
|
|
@@ -36,10 +36,17 @@
|
|
|
36
36
|
</template>
|
|
37
37
|
|
|
38
38
|
<v-list v-if="nricSearchResults.length" class="pa-1 phone-search-results">
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>
|
|
43
50
|
</v-list>
|
|
44
51
|
</v-menu>
|
|
45
52
|
</v-col>
|
|
@@ -58,10 +65,17 @@
|
|
|
58
65
|
</template>
|
|
59
66
|
|
|
60
67
|
<v-list v-if="phoneSearchResults.length" class="pa-1 phone-search-results">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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>
|
|
65
79
|
</v-list>
|
|
66
80
|
</v-menu>
|
|
67
81
|
</v-col>
|
|
@@ -77,15 +91,23 @@
|
|
|
77
91
|
<v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
78
92
|
<template #activator="{ props }">
|
|
79
93
|
<InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
|
|
80
|
-
@update:model-value="handleUpdateVehicleNumber" @focus="
|
|
94
|
+
@update:model-value="handleUpdateVehicleNumber" @focus="handleFocusPlateNumber"
|
|
81
95
|
@blur="isPlateNumberFieldFocused = false" />
|
|
82
96
|
</template>
|
|
83
97
|
|
|
84
98
|
<v-list v-if="vehicleSearchResults.length" class="pa-1 phone-search-results">
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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>
|
|
89
111
|
</v-list>
|
|
90
112
|
</v-menu>
|
|
91
113
|
</v-col>
|
|
@@ -308,10 +330,10 @@
|
|
|
308
330
|
|
|
309
331
|
<div class="nric-selected-person d-flex align-center justify-space-between ga-3 mb-5">
|
|
310
332
|
<span>{{ selectedNricSearchResult.name || "Unknown" }}</span>
|
|
311
|
-
<span class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
|
|
333
|
+
<span v-if="!visitor.nric || currentAutofillSource === 'nric'" class="text-no-wrap">NRIC: {{ selectedNricSearchResult.nric || "N/A" }}</span>
|
|
312
334
|
</div>
|
|
313
335
|
|
|
314
|
-
<template v-if="!visitor.contact && selectedNricContactOptions.length >= 1">
|
|
336
|
+
<template v-if="(!visitor.contact || currentAutofillSource === 'contact') && selectedNricContactOptions.length >= 1">
|
|
315
337
|
<h3 class="nric-options-section-title font-weight-bold mb-2">Contact</h3>
|
|
316
338
|
<v-radio-group v-model="selectedNricContact" hide-details color="success" class="nric-option-group">
|
|
317
339
|
<v-radio v-for="contact in selectedNricContactOptions" :key="contact" :label="contact" :value="contact"
|
|
@@ -319,7 +341,7 @@
|
|
|
319
341
|
</v-radio-group>
|
|
320
342
|
</template>
|
|
321
343
|
|
|
322
|
-
<template v-if="!visitor.plateNumber && selectedNricPlateOptions.length >= 1">
|
|
344
|
+
<template v-if="(!visitor.plateNumber || currentAutofillSource === 'vehicleNumber') && selectedNricPlateOptions.length >= 1">
|
|
323
345
|
<h3 class="nric-options-section-title font-weight-bold mb-2 mt-5">Vehicle</h3>
|
|
324
346
|
<v-radio-group v-model="selectedNricPlate" hide-details color="success" class="nric-option-group">
|
|
325
347
|
<v-radio v-for="plate in selectedNricPlateOptions" :key="plate" :label="plate" :value="plate"
|
|
@@ -1044,8 +1066,8 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
|
|
|
1044
1066
|
const selectedPlate =
|
|
1045
1067
|
findMatchingPlateOption(plateOptions, visitor.plateNumber) || plateOptions[0] || "";
|
|
1046
1068
|
|
|
1047
|
-
const needsContactChoice = !visitor.contact?.trim() && contactOptions.length > 1;
|
|
1048
|
-
const needsPlateChoice = !visitor.plateNumber?.trim() && plateOptions.length > 1;
|
|
1069
|
+
const needsContactChoice = (!visitor.contact?.trim() || source === 'contact') && contactOptions.length > 1;
|
|
1070
|
+
const needsPlateChoice = (!visitor.plateNumber?.trim() || source === 'vehicleNumber') && plateOptions.length > 1;
|
|
1049
1071
|
|
|
1050
1072
|
if (needsContactChoice || needsPlateChoice) {
|
|
1051
1073
|
nricSearchMenu.value = false;
|
|
@@ -1066,6 +1088,23 @@ function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<Auto
|
|
|
1066
1088
|
});
|
|
1067
1089
|
}
|
|
1068
1090
|
|
|
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
|
+
|
|
1069
1108
|
function closeNricAutofillOptions() {
|
|
1070
1109
|
dialog.nricAutofillOptions = false;
|
|
1071
1110
|
selectedNricSearchResult.value = null;
|
|
@@ -1503,6 +1542,15 @@ function handleUpdateVehicleNumber() {
|
|
|
1503
1542
|
debounceFetchPlateNumber();
|
|
1504
1543
|
}
|
|
1505
1544
|
|
|
1545
|
+
const handleFocusPlateNumber = () => {
|
|
1546
|
+
isPlateNumberFieldFocused.value = true;
|
|
1547
|
+
if (prop.mode === 'register') {
|
|
1548
|
+
|
|
1549
|
+
handleUpdateVehicleNumber();
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
|
|
1506
1554
|
function backToSelection() {
|
|
1507
1555
|
emit("back");
|
|
1508
1556
|
message.value = "";
|
|
@@ -2108,10 +2156,22 @@ onMounted(() => {
|
|
|
2108
2156
|
}
|
|
2109
2157
|
|
|
2110
2158
|
.phone-search-results {
|
|
2159
|
+
display: flex;
|
|
2160
|
+
flex-direction: column;
|
|
2111
2161
|
max-height: 400px;
|
|
2162
|
+
overflow: hidden;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
.search-results-scroll {
|
|
2166
|
+
flex: 1 1 auto;
|
|
2167
|
+
min-height: 0;
|
|
2112
2168
|
overflow-y: auto;
|
|
2113
2169
|
}
|
|
2114
2170
|
|
|
2171
|
+
.search-results-close {
|
|
2172
|
+
flex: 0 0 auto;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2115
2175
|
.nric-options-card {
|
|
2116
2176
|
border-radius: 16px !important;
|
|
2117
2177
|
}
|
|
@@ -675,6 +675,24 @@
|
|
|
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>
|
|
678
696
|
|
|
679
697
|
<span
|
|
680
698
|
v-else-if="selectedVisitorObject[key]"
|
|
@@ -825,10 +843,8 @@
|
|
|
825
843
|
item-title="label"
|
|
826
844
|
item-value="value"
|
|
827
845
|
v-model="pass.status"
|
|
828
|
-
:disabled="
|
|
829
|
-
|
|
830
|
-
"
|
|
831
|
-
></v-select>
|
|
846
|
+
:disabled="pass.status == 'Removed'"
|
|
847
|
+
/>
|
|
832
848
|
<v-textarea
|
|
833
849
|
v-if="pass.status === 'Lost' || pass.status === 'Damaged'"
|
|
834
850
|
no-resize
|
|
@@ -836,8 +852,7 @@
|
|
|
836
852
|
class="w-100"
|
|
837
853
|
density="compact"
|
|
838
854
|
v-model="pass.remarks"
|
|
839
|
-
|
|
840
|
-
></v-textarea>
|
|
855
|
+
/>
|
|
841
856
|
</v-row>
|
|
842
857
|
</div>
|
|
843
858
|
<div v-if="keyReturnStatuses.length > 0" class="mb-2">
|
|
@@ -864,10 +879,8 @@
|
|
|
864
879
|
item-title="label"
|
|
865
880
|
item-value="value"
|
|
866
881
|
v-model="key.status"
|
|
867
|
-
:disabled="
|
|
868
|
-
|
|
869
|
-
"
|
|
870
|
-
></v-select>
|
|
882
|
+
:disabled="key.status == 'Removed'"
|
|
883
|
+
/>
|
|
871
884
|
<v-textarea
|
|
872
885
|
v-if="key.status === 'Lost' || key.status === 'Damaged'"
|
|
873
886
|
no-resize
|
|
@@ -875,8 +888,7 @@
|
|
|
875
888
|
class="w-100"
|
|
876
889
|
density="compact"
|
|
877
890
|
v-model="key.remarks"
|
|
878
|
-
|
|
879
|
-
></v-textarea>
|
|
891
|
+
/>
|
|
880
892
|
</v-row>
|
|
881
893
|
</div>
|
|
882
894
|
|
|
@@ -886,11 +898,11 @@
|
|
|
886
898
|
color="blue"
|
|
887
899
|
density="comfortable"
|
|
888
900
|
class="text-capitalize"
|
|
889
|
-
:disabled="selectedVisitorObject.checkOut"
|
|
890
901
|
:loading="loading.updatingPassKeys"
|
|
891
902
|
@click.stop="handleUpdatePassKeys"
|
|
892
|
-
>Update Pass/Keys</v-btn
|
|
893
903
|
>
|
|
904
|
+
Update Pass/Keys
|
|
905
|
+
</v-btn>
|
|
894
906
|
</v-row>
|
|
895
907
|
</v-card-text>
|
|
896
908
|
<v-toolbar class="pa-0" density="compact">
|
|
@@ -900,8 +912,9 @@
|
|
|
900
912
|
variant="text"
|
|
901
913
|
block
|
|
902
914
|
@click="dialog.returnPassesKeys = false"
|
|
903
|
-
>Close</v-btn
|
|
904
915
|
>
|
|
916
|
+
Close
|
|
917
|
+
</v-btn>
|
|
905
918
|
</v-col>
|
|
906
919
|
<v-col cols="6">
|
|
907
920
|
<v-btn
|
|
@@ -915,8 +928,9 @@
|
|
|
915
928
|
!canConfirmCheckout || selectedVisitorObject.checkOut
|
|
916
929
|
"
|
|
917
930
|
@click="proceedCheckout"
|
|
918
|
-
>Confirm Checkout</v-btn
|
|
919
931
|
>
|
|
932
|
+
Confirm Checkout
|
|
933
|
+
</v-btn>
|
|
920
934
|
</v-col>
|
|
921
935
|
</v-row>
|
|
922
936
|
</v-toolbar>
|
|
@@ -1442,6 +1456,15 @@ function mappedAttachments(attachments: string[]) {
|
|
|
1442
1456
|
});
|
|
1443
1457
|
}
|
|
1444
1458
|
|
|
1459
|
+
function mappedSnapshots(attachments: string[]) {
|
|
1460
|
+
return attachments.map((x, index) => {
|
|
1461
|
+
return {
|
|
1462
|
+
id: x,
|
|
1463
|
+
name: `Snapshot - ${x}`,
|
|
1464
|
+
};
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1445
1468
|
function normalizeDateOnly(value: string) {
|
|
1446
1469
|
if (!value) return "";
|
|
1447
1470
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
|
@@ -1,58 +1,23 @@
|
|
|
1
1
|
export function useCleaningSchedulePermission() {
|
|
2
|
-
const { hasPermission } = usePermission();
|
|
3
|
-
const { permissions } = useCleaningPermission();
|
|
4
|
-
|
|
5
2
|
const { userAppRole } = useLocalSetup();
|
|
6
3
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
});
|
|
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
|
+
});
|
|
48
13
|
|
|
49
14
|
return {
|
|
50
|
-
canViewSchedules,
|
|
51
|
-
canViewScheduleDetails,
|
|
52
|
-
canDownloadSchedule,
|
|
53
|
-
canManageScheduleTasks,
|
|
54
|
-
canGenerateChecklist,
|
|
55
|
-
canViewHistory,
|
|
56
|
-
canAddRemarks,
|
|
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"),
|
|
57
22
|
};
|
|
58
23
|
}
|
|
@@ -236,6 +236,26 @@ 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
|
+
|
|
239
259
|
return {
|
|
240
260
|
serviceProviders,
|
|
241
261
|
serviceProvider,
|
|
@@ -254,5 +274,6 @@ export default function useServiceProvider() {
|
|
|
254
274
|
add,
|
|
255
275
|
addViaInvite,
|
|
256
276
|
updateStatus,
|
|
277
|
+
getByEmail,
|
|
257
278
|
};
|
|
258
279
|
}
|