@7365admin1/layer-common 3.1.2 → 3.1.3-staging.39
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/AddEqupmentForm.vue +52 -1
- package/components/CameraMain.vue +5 -6
- package/components/DashboardMain.vue +231 -87
- package/components/EquipmentManagementMain.vue +56 -0
- package/components/InvitationMain.vue +4 -3
- package/components/ManageChecklistMain.vue +44 -38
- package/components/Nfc/NFCPatrolReportMain.vue +146 -38
- package/components/Nfc/PatrolReport/DailyReportTab.vue +10 -6
- package/components/Nfc/PatrolReport/MonthlyReportTab.vue +19 -9
- package/components/Nfc/PatrolReport/MonthlyReportTable.vue +69 -0
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +20 -0
- package/components/Nfc/PatrolReport/PatrolReportTab.vue +5 -3
- package/components/Nfc/PatrolReport/ReportDatePicker.vue +61 -0
- package/components/Nfc/PatrolReport/ReportFilters.vue +52 -20
- package/components/Nfc/PatrolReport/SummaryReportTable.vue +22 -0
- package/components/PhotoUpload.vue +19 -4
- package/components/ServiceProviderMain.vue +59 -25
- package/components/VisitorForm.vue +78 -18
- package/components/VisitorManagement.vue +12 -16
- package/composables/useCleaningSchedulePermission.ts +16 -51
- package/composables/useEquipment.ts +2 -0
- package/composables/useLocalAuth.ts +1 -1
- package/composables/useNFCPatrolDailyReport.ts +3 -4
- package/composables/useNFCPatrolMonthlyReport.ts +62 -30
- package/composables/useNFCPatrolReport.ts +2 -0
- package/composables/useServiceProvider.ts +21 -0
- package/package.json +1 -1
- package/types/customer.d.ts +2 -1
- package/types/equipment.d.ts +5 -1
- package/types/nfc-patrol-report.ts +19 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="d-flex flex-column">
|
|
3
|
+
<v-text-field
|
|
4
|
+
v-bind="$attrs"
|
|
5
|
+
:model-value="formattedDate"
|
|
6
|
+
autocomplete="off"
|
|
7
|
+
placeholder="DD/MM/YY"
|
|
8
|
+
readonly
|
|
9
|
+
@click="openDatePicker"
|
|
10
|
+
>
|
|
11
|
+
<template #append-inner>
|
|
12
|
+
<v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
|
|
13
|
+
</template>
|
|
14
|
+
</v-text-field>
|
|
15
|
+
|
|
16
|
+
<input
|
|
17
|
+
ref="dateInput"
|
|
18
|
+
v-model="date"
|
|
19
|
+
class="native-date-input"
|
|
20
|
+
type="date"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
defineOptions({ inheritAttrs: false });
|
|
27
|
+
|
|
28
|
+
const date = defineModel<string>({ default: "" });
|
|
29
|
+
const dateInput = ref<HTMLInputElement | null>(null);
|
|
30
|
+
|
|
31
|
+
const formattedDate = computed(() => {
|
|
32
|
+
const match = date.value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
33
|
+
|
|
34
|
+
if (!match) return "";
|
|
35
|
+
|
|
36
|
+
const [, year, month, day] = match;
|
|
37
|
+
return `${day}/${month}/${year.slice(-2)}`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function openDatePicker() {
|
|
41
|
+
const input = dateInput.value;
|
|
42
|
+
|
|
43
|
+
if (!input) return;
|
|
44
|
+
|
|
45
|
+
if (input.showPicker) {
|
|
46
|
+
input.showPicker();
|
|
47
|
+
} else {
|
|
48
|
+
input.click();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.native-date-input {
|
|
55
|
+
position: absolute;
|
|
56
|
+
width: 1px;
|
|
57
|
+
height: 1px;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
/>
|
|
17
17
|
</v-col>
|
|
18
18
|
|
|
19
|
-
<v-col
|
|
19
|
+
<v-col
|
|
20
|
+
v-if="showTimeFilter"
|
|
21
|
+
cols="12"
|
|
22
|
+
md="2"
|
|
23
|
+
class="px-md-2 mb-2 mb-md-0"
|
|
24
|
+
>
|
|
20
25
|
<v-select
|
|
21
26
|
:model-value="modelValue.timeRange"
|
|
22
27
|
:items="options.timeRanges"
|
|
@@ -30,10 +35,14 @@
|
|
|
30
35
|
/>
|
|
31
36
|
</v-col>
|
|
32
37
|
|
|
33
|
-
<v-col
|
|
34
|
-
|
|
38
|
+
<v-col
|
|
39
|
+
v-if="showDateFilter"
|
|
40
|
+
cols="12"
|
|
41
|
+
md="2"
|
|
42
|
+
class="px-md-2 mb-2 mb-md-0"
|
|
43
|
+
>
|
|
44
|
+
<ReportDatePicker
|
|
35
45
|
:model-value="modelValue.date"
|
|
36
|
-
type="date"
|
|
37
46
|
variant="outlined"
|
|
38
47
|
density="comfortable"
|
|
39
48
|
rounded="lg"
|
|
@@ -46,17 +55,31 @@
|
|
|
46
55
|
<v-spacer />
|
|
47
56
|
|
|
48
57
|
<v-col cols="12" md="2" class="pl-md-2">
|
|
49
|
-
<v-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
<v-menu>
|
|
59
|
+
<template #activator="{ props: menuProps }">
|
|
60
|
+
<v-btn
|
|
61
|
+
v-bind="menuProps"
|
|
62
|
+
variant="outlined"
|
|
63
|
+
block
|
|
64
|
+
size="large"
|
|
65
|
+
rounded="lg"
|
|
66
|
+
class="text-none"
|
|
67
|
+
prepend-icon="mdi-export"
|
|
68
|
+
>
|
|
69
|
+
Export
|
|
70
|
+
</v-btn>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<v-list density="compact">
|
|
74
|
+
<v-list-item @click="$emit('export', 'pdf')">
|
|
75
|
+
<v-list-item-title>PDF</v-list-item-title>
|
|
76
|
+
</v-list-item>
|
|
77
|
+
|
|
78
|
+
<v-list-item @click="$emit('export', 'csv')">
|
|
79
|
+
<v-list-item-title>CSV</v-list-item-title>
|
|
80
|
+
</v-list-item>
|
|
81
|
+
</v-list>
|
|
82
|
+
</v-menu>
|
|
60
83
|
</v-col>
|
|
61
84
|
</v-row>
|
|
62
85
|
</template>
|
|
@@ -66,15 +89,24 @@ import type {
|
|
|
66
89
|
NFCPatrolReportFilterOptions,
|
|
67
90
|
NFCPatrolReportFilters,
|
|
68
91
|
} from "../../../types/nfc-patrol-report";
|
|
92
|
+
import ReportDatePicker from "./ReportDatePicker.vue";
|
|
69
93
|
|
|
70
|
-
const props =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
const props = withDefaults(
|
|
95
|
+
defineProps<{
|
|
96
|
+
modelValue: NFCPatrolReportFilters;
|
|
97
|
+
options: NFCPatrolReportFilterOptions;
|
|
98
|
+
showTimeFilter?: boolean;
|
|
99
|
+
showDateFilter?: boolean;
|
|
100
|
+
}>(),
|
|
101
|
+
{
|
|
102
|
+
showTimeFilter: true,
|
|
103
|
+
showDateFilter: true,
|
|
104
|
+
},
|
|
105
|
+
);;
|
|
74
106
|
|
|
75
107
|
const emit = defineEmits<{
|
|
76
108
|
"update:modelValue": [value: NFCPatrolReportFilters];
|
|
77
|
-
export: [];
|
|
109
|
+
export: [type: "pdf" | "csv"];
|
|
78
110
|
}>();
|
|
79
111
|
|
|
80
112
|
function updateFilter(key: keyof NFCPatrolReportFilters, value: unknown) {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
<tr>
|
|
9
9
|
<th>Patrol Route Name</th>
|
|
10
10
|
<th>Security Check Schedule</th>
|
|
11
|
+
<th>Security Personnel</th>
|
|
12
|
+
<th>Signature</th>
|
|
11
13
|
<th>Checked</th>
|
|
12
14
|
<th>Missed</th>
|
|
13
15
|
<th>Status</th>
|
|
@@ -20,6 +22,17 @@
|
|
|
20
22
|
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
21
23
|
{{ row.securityCheckSchedule }}
|
|
22
24
|
</td>
|
|
25
|
+
<td>{{ row.guardName || "-" }}</td>
|
|
26
|
+
<td>
|
|
27
|
+
<v-img
|
|
28
|
+
v-if="row.guardSignatureFileId"
|
|
29
|
+
:src="getFileUrl(row.guardSignatureFileId)"
|
|
30
|
+
:alt="`${row.guardName || 'Guard'} signature`"
|
|
31
|
+
class="guard-signature"
|
|
32
|
+
contain
|
|
33
|
+
/>
|
|
34
|
+
<span v-else>-</span>
|
|
35
|
+
</td>
|
|
23
36
|
<td>{{ row.checked }}</td>
|
|
24
37
|
<td>{{ row.missed }}</td>
|
|
25
38
|
<td>
|
|
@@ -45,7 +58,16 @@ defineProps<{
|
|
|
45
58
|
rows: NFCPatrolReportSummaryRow[];
|
|
46
59
|
}>();
|
|
47
60
|
|
|
61
|
+
const { getFileUrl } = useFile();
|
|
62
|
+
|
|
48
63
|
function statusClass(status: NFCPatrolReportSummaryStatus) {
|
|
49
64
|
return status === "Completed" ? "text-success" : "text-error";
|
|
50
65
|
}
|
|
51
66
|
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.guard-signature {
|
|
70
|
+
width: 80px;
|
|
71
|
+
height: 40px;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -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(
|
|
@@ -548,7 +548,7 @@
|
|
|
548
548
|
variant="flat"
|
|
549
549
|
class="text-none sp-invite-submit-btn"
|
|
550
550
|
height="76"
|
|
551
|
-
:disabled="!
|
|
551
|
+
:disabled="!canSubmitInvite"
|
|
552
552
|
:loading="disableServiceProvider"
|
|
553
553
|
@click="submitServiceProviderInvite()"
|
|
554
554
|
>
|
|
@@ -574,6 +574,7 @@ type ListTab = "active" | "pending" | "inactive";
|
|
|
574
574
|
|
|
575
575
|
type TableRow = {
|
|
576
576
|
_id: string;
|
|
577
|
+
userId: string;
|
|
577
578
|
serviceProviderOrgId: string;
|
|
578
579
|
companyName: string;
|
|
579
580
|
email: string;
|
|
@@ -733,11 +734,10 @@ const {
|
|
|
733
734
|
updateStatus: updateServiceProviderStatus,
|
|
734
735
|
invite,
|
|
735
736
|
} = useServiceProvider();
|
|
736
|
-
const { getAll: getAllMembers } = useMember();
|
|
737
|
+
const { getAll: getAllMembers, getAllByUserId } = useMember();
|
|
737
738
|
|
|
738
739
|
const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
|
|
739
740
|
useVerification();
|
|
740
|
-
const { getOrgsByMembership } = useMember();
|
|
741
741
|
|
|
742
742
|
const existingAccount = ref<any>(null);
|
|
743
743
|
const checkingExistingAccount = ref(false);
|
|
@@ -759,41 +759,77 @@ const createInviteDialog = ref(false);
|
|
|
759
759
|
|
|
760
760
|
const { getByUserType } = useMember();
|
|
761
761
|
const { getOrgs } = useOrg();
|
|
762
|
+
const { getByEmail } = useServiceProvider();
|
|
762
763
|
|
|
763
764
|
async function checkExistingAccount(email: string) {
|
|
764
765
|
existingAccount.value = null;
|
|
766
|
+
messageServiceProvider.value = "";
|
|
765
767
|
|
|
766
768
|
if (!email.trim() || emailRule(email) !== true) return;
|
|
767
769
|
|
|
768
770
|
checkingExistingAccount.value = true;
|
|
769
771
|
|
|
770
772
|
try {
|
|
773
|
+
// Check if user exists
|
|
771
774
|
const user = await getUserByEmail(email);
|
|
772
775
|
|
|
773
|
-
|
|
776
|
+
if (!user?._id) return;
|
|
774
777
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
}
|
|
792
|
+
|
|
793
|
+
// Check existing memberships
|
|
794
|
+
const members = await getAllByUserId(user._id);
|
|
778
795
|
|
|
779
|
-
const
|
|
780
|
-
(
|
|
796
|
+
const appMember = (members.items ?? []).find(
|
|
797
|
+
(member: any) => member.type === serviceProviderInvite.value.app
|
|
781
798
|
);
|
|
782
799
|
|
|
783
|
-
if (
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
800
|
+
if (!appMember) return;
|
|
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;
|
|
789
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
|
+
};
|
|
790
821
|
} catch (error) {
|
|
791
822
|
existingAccount.value = null;
|
|
792
823
|
} finally {
|
|
793
824
|
checkingExistingAccount.value = false;
|
|
794
825
|
}
|
|
795
826
|
}
|
|
796
|
-
|
|
827
|
+
const canSubmitInvite = computed(() => {
|
|
828
|
+
return (
|
|
829
|
+
validServiceProvider.value &&
|
|
830
|
+
!messageServiceProvider.value
|
|
831
|
+
);
|
|
832
|
+
});
|
|
797
833
|
watch(
|
|
798
834
|
() => serviceProviderInvite.value.email,
|
|
799
835
|
async (value) => {
|
|
@@ -939,6 +975,7 @@ function formatEmailCell(value: unknown): string {
|
|
|
939
975
|
function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
|
|
940
976
|
return raw.map((row) => ({
|
|
941
977
|
_id: String(row._id ?? ""),
|
|
978
|
+
userId: String(row.user ?? ""),
|
|
942
979
|
serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
|
|
943
980
|
companyName: row.name ?? "-",
|
|
944
981
|
email: formatEmailCell(row.email),
|
|
@@ -961,6 +998,7 @@ function inviteTypeLabel(t: string) {
|
|
|
961
998
|
function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
|
|
962
999
|
return raw.map((row) => ({
|
|
963
1000
|
_id: String(row._id ?? ""),
|
|
1001
|
+
userId: String(row.user ?? ""),
|
|
964
1002
|
serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
|
|
965
1003
|
companyName: row.metadata?.name || "-",
|
|
966
1004
|
email: formatEmailCell(row.email),
|
|
@@ -1029,16 +1067,12 @@ async function openView(row: TableRow) {
|
|
|
1029
1067
|
|
|
1030
1068
|
loadingMembers.value = true;
|
|
1031
1069
|
try {
|
|
1032
|
-
const data = await
|
|
1033
|
-
|
|
1034
|
-
limit: 1000,
|
|
1035
|
-
org: row.serviceProviderOrgId,
|
|
1036
|
-
status: "active",
|
|
1037
|
-
});
|
|
1070
|
+
const data = await getAllByUserId(row.userId);
|
|
1071
|
+
|
|
1038
1072
|
serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
|
|
1039
|
-
_id:
|
|
1040
|
-
name: member.name ||
|
|
1041
|
-
role: member.
|
|
1073
|
+
_id: member._id,
|
|
1074
|
+
name: member.name || "-",
|
|
1075
|
+
role: member.role || "-",
|
|
1042
1076
|
}));
|
|
1043
1077
|
} catch (error: any) {
|
|
1044
1078
|
showMessage(errorConverter(error), "error");
|
|
@@ -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
|
}
|
|
@@ -825,10 +825,8 @@
|
|
|
825
825
|
item-title="label"
|
|
826
826
|
item-value="value"
|
|
827
827
|
v-model="pass.status"
|
|
828
|
-
:disabled="
|
|
829
|
-
|
|
830
|
-
"
|
|
831
|
-
></v-select>
|
|
828
|
+
:disabled="pass.status == 'Removed'"
|
|
829
|
+
/>
|
|
832
830
|
<v-textarea
|
|
833
831
|
v-if="pass.status === 'Lost' || pass.status === 'Damaged'"
|
|
834
832
|
no-resize
|
|
@@ -836,8 +834,7 @@
|
|
|
836
834
|
class="w-100"
|
|
837
835
|
density="compact"
|
|
838
836
|
v-model="pass.remarks"
|
|
839
|
-
|
|
840
|
-
></v-textarea>
|
|
837
|
+
/>
|
|
841
838
|
</v-row>
|
|
842
839
|
</div>
|
|
843
840
|
<div v-if="keyReturnStatuses.length > 0" class="mb-2">
|
|
@@ -864,10 +861,8 @@
|
|
|
864
861
|
item-title="label"
|
|
865
862
|
item-value="value"
|
|
866
863
|
v-model="key.status"
|
|
867
|
-
:disabled="
|
|
868
|
-
|
|
869
|
-
"
|
|
870
|
-
></v-select>
|
|
864
|
+
:disabled="key.status == 'Removed'"
|
|
865
|
+
/>
|
|
871
866
|
<v-textarea
|
|
872
867
|
v-if="key.status === 'Lost' || key.status === 'Damaged'"
|
|
873
868
|
no-resize
|
|
@@ -875,8 +870,7 @@
|
|
|
875
870
|
class="w-100"
|
|
876
871
|
density="compact"
|
|
877
872
|
v-model="key.remarks"
|
|
878
|
-
|
|
879
|
-
></v-textarea>
|
|
873
|
+
/>
|
|
880
874
|
</v-row>
|
|
881
875
|
</div>
|
|
882
876
|
|
|
@@ -886,11 +880,11 @@
|
|
|
886
880
|
color="blue"
|
|
887
881
|
density="comfortable"
|
|
888
882
|
class="text-capitalize"
|
|
889
|
-
:disabled="selectedVisitorObject.checkOut"
|
|
890
883
|
:loading="loading.updatingPassKeys"
|
|
891
884
|
@click.stop="handleUpdatePassKeys"
|
|
892
|
-
>Update Pass/Keys</v-btn
|
|
893
885
|
>
|
|
886
|
+
Update Pass/Keys
|
|
887
|
+
</v-btn>
|
|
894
888
|
</v-row>
|
|
895
889
|
</v-card-text>
|
|
896
890
|
<v-toolbar class="pa-0" density="compact">
|
|
@@ -900,8 +894,9 @@
|
|
|
900
894
|
variant="text"
|
|
901
895
|
block
|
|
902
896
|
@click="dialog.returnPassesKeys = false"
|
|
903
|
-
>Close</v-btn
|
|
904
897
|
>
|
|
898
|
+
Close
|
|
899
|
+
</v-btn>
|
|
905
900
|
</v-col>
|
|
906
901
|
<v-col cols="6">
|
|
907
902
|
<v-btn
|
|
@@ -915,8 +910,9 @@
|
|
|
915
910
|
!canConfirmCheckout || selectedVisitorObject.checkOut
|
|
916
911
|
"
|
|
917
912
|
@click="proceedCheckout"
|
|
918
|
-
>Confirm Checkout</v-btn
|
|
919
913
|
>
|
|
914
|
+
Confirm Checkout
|
|
915
|
+
</v-btn>
|
|
920
916
|
</v-col>
|
|
921
917
|
</v-row>
|
|
922
918
|
</v-toolbar>
|