@7365admin1/layer-common 3.0.31-staging.14 → 3.0.31-staging.16
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/components/Input/InputPhoneNumberV2.vue +89 -72
- package/components/Input/VehicleNumber.vue +14 -11
- package/components/Nfc/NFCPatrolReportMain.vue +128 -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/PatrolReportTab.vue +5 -3
- package/components/Nfc/PatrolReport/ReportFilters.vue +50 -18
- package/components/VisitorForm.vue +44 -70
- package/composables/useNFCPatrolDailyReport.ts +1 -4
- package/composables/useNFCPatrolMonthlyReport.ts +62 -30
- package/composables/usePeople.ts +15 -0
- package/package.json +1 -1
- package/types/nfc-patrol-report.ts +15 -1
- package/utils/mock-people-response.ts +38 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mt-6">
|
|
3
|
+
<div class="text-h6 font-weight-bold mb-4">
|
|
4
|
+
{{ title }}
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<v-table class="border rounded-lg">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="text-left font-weight-bold">Month</th>
|
|
11
|
+
<th class="text-center font-weight-bold">Checked</th>
|
|
12
|
+
<th class="text-center font-weight-bold">% Checked</th>
|
|
13
|
+
<th class="text-center font-weight-bold">Missed</th>
|
|
14
|
+
<th class="text-center font-weight-bold">% Missed</th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
|
|
18
|
+
<tbody>
|
|
19
|
+
<tr
|
|
20
|
+
v-for="row in rows"
|
|
21
|
+
:key="row.month"
|
|
22
|
+
>
|
|
23
|
+
<td>{{ row.month }}</td>
|
|
24
|
+
|
|
25
|
+
<td class="text-center">
|
|
26
|
+
{{ row.checked }}
|
|
27
|
+
</td>
|
|
28
|
+
|
|
29
|
+
<td class="text-center">
|
|
30
|
+
{{ row.checkedPercentage }}%
|
|
31
|
+
</td>
|
|
32
|
+
|
|
33
|
+
<td class="text-center">
|
|
34
|
+
{{ row.missed }}
|
|
35
|
+
</td>
|
|
36
|
+
|
|
37
|
+
<td class="text-center">
|
|
38
|
+
{{ row.missedPercentage }}%
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
</tbody>
|
|
42
|
+
</v-table>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import type { NFCPatrolMonthlyReportRow } from "../../../types/nfc-patrol-report";
|
|
48
|
+
|
|
49
|
+
defineProps<{
|
|
50
|
+
title: string;
|
|
51
|
+
rows: NFCPatrolMonthlyReportRow[];
|
|
52
|
+
}>();
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.v-table {
|
|
57
|
+
border: 1px solid rgb(var(--v-theme-outline));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
thead th {
|
|
61
|
+
background: transparent;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tbody td {
|
|
67
|
+
height: 56px;
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
:model-value="filters"
|
|
5
5
|
:options="options"
|
|
6
6
|
@update:model-value="$emit('update:filters', $event)"
|
|
7
|
-
@export="$emit('export')"
|
|
7
|
+
@export="(type) => $emit('export', type)"
|
|
8
8
|
/>
|
|
9
|
-
|
|
9
|
+
<div class="report-export">
|
|
10
10
|
<v-divider :thickness="1" class="border-opacity-100" />
|
|
11
11
|
|
|
12
12
|
<template v-if="report">
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
<ReportEmptyState v-else-if="!loading" />
|
|
31
31
|
|
|
32
32
|
<RemarksDialog v-model="dialogRemarks" :remarks="selectedActivityRemarks" />
|
|
33
|
+
</div>
|
|
33
34
|
</div>
|
|
34
35
|
</template>
|
|
35
36
|
|
|
@@ -55,7 +56,7 @@ const props = defineProps<{
|
|
|
55
56
|
|
|
56
57
|
defineEmits<{
|
|
57
58
|
"update:filters": [value: NFCPatrolReportFilters];
|
|
58
|
-
export: [];
|
|
59
|
+
export: [type: "pdf" | "csv"];
|
|
59
60
|
}>();
|
|
60
61
|
|
|
61
62
|
const {
|
|
@@ -77,4 +78,5 @@ watch(
|
|
|
77
78
|
},
|
|
78
79
|
{ immediate: true },
|
|
79
80
|
);
|
|
81
|
+
|
|
80
82
|
</script>
|
|
@@ -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,7 +35,12 @@
|
|
|
30
35
|
/>
|
|
31
36
|
</v-col>
|
|
32
37
|
|
|
33
|
-
<v-col
|
|
38
|
+
<v-col
|
|
39
|
+
v-if="showDateFilter"
|
|
40
|
+
cols="12"
|
|
41
|
+
md="2"
|
|
42
|
+
class="px-md-2 mb-2 mb-md-0"
|
|
43
|
+
>
|
|
34
44
|
<v-text-field
|
|
35
45
|
:model-value="modelValue.date"
|
|
36
46
|
type="date"
|
|
@@ -46,17 +56,31 @@
|
|
|
46
56
|
<v-spacer />
|
|
47
57
|
|
|
48
58
|
<v-col cols="12" md="2" class="pl-md-2">
|
|
49
|
-
<v-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
<v-menu>
|
|
60
|
+
<template #activator="{ props: menuProps }">
|
|
61
|
+
<v-btn
|
|
62
|
+
v-bind="menuProps"
|
|
63
|
+
variant="outlined"
|
|
64
|
+
block
|
|
65
|
+
size="large"
|
|
66
|
+
rounded="lg"
|
|
67
|
+
class="text-none"
|
|
68
|
+
prepend-icon="mdi-export"
|
|
69
|
+
>
|
|
70
|
+
Export
|
|
71
|
+
</v-btn>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<v-list density="compact">
|
|
75
|
+
<v-list-item @click="$emit('export', 'pdf')">
|
|
76
|
+
<v-list-item-title>PDF</v-list-item-title>
|
|
77
|
+
</v-list-item>
|
|
78
|
+
|
|
79
|
+
<v-list-item @click="$emit('export', 'csv')">
|
|
80
|
+
<v-list-item-title>CSV</v-list-item-title>
|
|
81
|
+
</v-list-item>
|
|
82
|
+
</v-list>
|
|
83
|
+
</v-menu>
|
|
60
84
|
</v-col>
|
|
61
85
|
</v-row>
|
|
62
86
|
</template>
|
|
@@ -67,14 +91,22 @@ import type {
|
|
|
67
91
|
NFCPatrolReportFilters,
|
|
68
92
|
} from "../../../types/nfc-patrol-report";
|
|
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) {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<div class="w-100 d-flex justify-space-between ga-2">
|
|
13
13
|
<span class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
|
|
14
14
|
formTitle
|
|
15
|
-
|
|
15
|
+
}}</span>
|
|
16
16
|
<span v-if="withStep2 || withStep3" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
|
|
17
17
|
<span class="text-primary-button">{{ contractorStep }}</span>/{{ withStep3 ? 3 : withStep2 ? 2 : 1 }}</span>
|
|
18
18
|
</div>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
|
|
31
31
|
<span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
|
|
32
32
|
contractorTypeInput
|
|
33
|
-
|
|
33
|
+
}}</strong>" as custom contractor type.
|
|
34
34
|
</v-list-item-title>
|
|
35
35
|
</v-list-item>
|
|
36
36
|
</template>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
|
|
58
58
|
<v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
59
59
|
<template #activator="{ props }">
|
|
60
|
-
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable"
|
|
60
|
+
<InputNRICNumber v-bind="props" v-model="visitor.nric" density="comfortable" @focus="isNricFieldFocused = true" @blur="isNricFieldFocused = false"
|
|
61
61
|
:rules="[requireNRIC ? requiredRule : () => true]" :key="currentAutofillSource + 'nric-key'"
|
|
62
62
|
@update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
|
|
63
63
|
</template>
|
|
@@ -87,14 +87,14 @@
|
|
|
87
87
|
<InputLabel class="text-capitalize" title="Phone Number" required />
|
|
88
88
|
<v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
89
89
|
<template #activator="{ props }">
|
|
90
|
-
<InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]"
|
|
90
|
+
<InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]" @focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false"
|
|
91
91
|
density="comfortable" :key="currentAutofillSource + 'phone-key'"
|
|
92
92
|
@update:model-value="handleUpdatePhoneNumber" />
|
|
93
93
|
</template>
|
|
94
94
|
|
|
95
95
|
<v-list v-if="phoneSearchResults.length" class="pa-1">
|
|
96
96
|
<v-list-item v-for="(item, index) in phoneSearchResults" :key="item._id || index"
|
|
97
|
-
@click="
|
|
97
|
+
@click="selectPhoneNumberSearchResult(item)" class="cursor-pointer nric-search-item">
|
|
98
98
|
<div class="d-flex align-center justify-space-between ga-4 w-100">
|
|
99
99
|
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
100
100
|
<span class="text-subtitle-1 text-no-wrap">NRIC: {{ item.nric || 'N/A' }}</span>
|
|
@@ -113,12 +113,12 @@
|
|
|
113
113
|
<v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false">
|
|
114
114
|
<template #activator="{ props }">
|
|
115
115
|
<InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
|
|
116
|
-
@update:model-value="handleUpdateVehicleNumber" />
|
|
116
|
+
@update:model-value="handleUpdateVehicleNumber" @focus="isPlateNumberFieldFocused = true" @blur="isPlateNumberFieldFocused = false" />
|
|
117
117
|
</template>
|
|
118
118
|
|
|
119
119
|
<v-list v-if="vehicleSearchResults.length" class="pa-1">
|
|
120
120
|
<v-list-item v-for="(item, index) in vehicleSearchResults" :key="item._id || index"
|
|
121
|
-
@click="
|
|
121
|
+
@click="selectPlateNumberSearchResult(item)"
|
|
122
122
|
class="cursor-pointer nric-search-item">
|
|
123
123
|
<div class="d-flex align-center justify-space-between ga-4 w-100">
|
|
124
124
|
<span class="text-subtitle-1">{{ item.name || 'Unknown' }}</span>
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
class="d-flex align-center ga-1">
|
|
160
160
|
<span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
|
|
161
161
|
companyNameInput
|
|
162
|
-
|
|
162
|
+
}}</strong>" as new company.
|
|
163
163
|
</v-list-item-title>
|
|
164
164
|
<v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
|
|
165
165
|
Start typing to search for companies.
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
class="d-flex align-center ga-1">
|
|
190
190
|
<span><v-icon icon="mdi-plus" /></span>Add "<strong>{{
|
|
191
191
|
companyNameInput
|
|
192
|
-
|
|
192
|
+
}}</strong>" as new company.
|
|
193
193
|
</v-list-item-title>
|
|
194
194
|
<v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
|
|
195
195
|
Start typing to search for companies.
|
|
@@ -499,6 +499,7 @@ import useVisitor from "../composables/useVisitor";
|
|
|
499
499
|
import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
|
|
500
500
|
import useAccessManagement from "../composables/useAccessManagement";
|
|
501
501
|
import usePeople from "../composables/usePeople";
|
|
502
|
+
import { mockPhoneNumberSearchResults, mockVehicleNumberSearchResults } from "../utils/mock-people-response";
|
|
502
503
|
|
|
503
504
|
const prop = defineProps({
|
|
504
505
|
type: {
|
|
@@ -554,6 +555,7 @@ const { testConnection } = useWebUsb();
|
|
|
554
555
|
const {
|
|
555
556
|
findPersonByNRIC,
|
|
556
557
|
findPersonByNRICMultipleResult,
|
|
558
|
+
findPersonByPhoneNumberMultipleResult,
|
|
557
559
|
searchCompanyList,
|
|
558
560
|
findUsersByPlateNumber,
|
|
559
561
|
} = usePeople();
|
|
@@ -594,6 +596,9 @@ const passCards = ref<{ _id: string; cardNo: string }[]>([]);
|
|
|
594
596
|
const availableQrCount = ref<number | null>(null);
|
|
595
597
|
const hidQrPrintPayload = ref<string | null>(null);
|
|
596
598
|
const hidQrPrinted = ref(false);
|
|
599
|
+
const isPhoneFieldFocused = ref(false);
|
|
600
|
+
const isPlateNumberFieldFocused = ref(false);
|
|
601
|
+
const isNricFieldFocused = ref(false);
|
|
597
602
|
|
|
598
603
|
const registeredUnitCompanyName = ref("N/A");
|
|
599
604
|
|
|
@@ -759,50 +764,13 @@ const selectedNricSearchResult = ref<Partial<TPeople> | null>(null);
|
|
|
759
764
|
const selectedNricContact = ref("");
|
|
760
765
|
const selectedNricPlate = ref("");
|
|
761
766
|
|
|
762
|
-
type
|
|
767
|
+
type SearchPerson = Omit<Partial<TPeople>, "contact"> & {
|
|
763
768
|
contact?: string | string[];
|
|
764
769
|
contacts?: string | string[];
|
|
765
770
|
visitorPlateNumbers?: string | string[];
|
|
766
771
|
plateNumber?: string | string[];
|
|
767
772
|
};
|
|
768
773
|
|
|
769
|
-
const mockPhoneNumberSearchResults: Partial<NricSearchPerson>[] = [
|
|
770
|
-
{
|
|
771
|
-
_id: "mock-phone-multiple-options",
|
|
772
|
-
name: "James Smith",
|
|
773
|
-
nric: "1323232499",
|
|
774
|
-
contacts: ["653939394", "81234567"],
|
|
775
|
-
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
776
|
-
companyName: ["Mock Company Pte Ltd"],
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
_id: "mock-phone-single-option",
|
|
780
|
-
name: "Jane Tan",
|
|
781
|
-
nric: "S1234567A",
|
|
782
|
-
contacts: ["91234567"],
|
|
783
|
-
visitorPlateNumbers: ["SGB5678B"],
|
|
784
|
-
companyName: ["Single Option Company"],
|
|
785
|
-
},
|
|
786
|
-
];
|
|
787
|
-
|
|
788
|
-
const mockVehicleNumberSearchResults: Partial<NricSearchPerson>[] = [
|
|
789
|
-
{
|
|
790
|
-
_id: "mock-vehicle-multiple-options",
|
|
791
|
-
name: "James Smith",
|
|
792
|
-
nric: "1323232499",
|
|
793
|
-
contacts: ["653939394", "81234567"],
|
|
794
|
-
visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
795
|
-
companyName: ["Mock Company Pte Ltd"],
|
|
796
|
-
},
|
|
797
|
-
{
|
|
798
|
-
_id: "mock-vehicle-single-option",
|
|
799
|
-
name: "Ali Lim",
|
|
800
|
-
nric: "T7654321B",
|
|
801
|
-
contacts: ["98765432"],
|
|
802
|
-
visitorPlateNumbers: ["SKX2468C"],
|
|
803
|
-
companyName: ["Vehicle Single Company"],
|
|
804
|
-
},
|
|
805
|
-
];
|
|
806
774
|
|
|
807
775
|
const formTitle = computed(() => {
|
|
808
776
|
const isContractorForm = prop.type === "contractor";
|
|
@@ -874,7 +842,7 @@ const {
|
|
|
874
842
|
pending: fetchPersonByNRICPending,
|
|
875
843
|
} = useLazyAsyncData(`fetch-person`, () => {
|
|
876
844
|
const NRIC = visitor.nric;
|
|
877
|
-
if (!NRIC || NRIC.length <
|
|
845
|
+
if (!NRIC || NRIC.length < 1) return Promise.resolve(null);
|
|
878
846
|
return findPersonByNRICMultipleResult(NRIC, prop.site);
|
|
879
847
|
});
|
|
880
848
|
|
|
@@ -894,7 +862,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
894
862
|
// contacts: ["653939394", "81234567"],
|
|
895
863
|
// visitorPlateNumbers: ["34566", "SMA1234A"],
|
|
896
864
|
// companyName: ["Mock Company Pte Ltd"],
|
|
897
|
-
// } as Partial<
|
|
865
|
+
// } as Partial<SearchPerson>,
|
|
898
866
|
// {
|
|
899
867
|
// _id: "mock-person-single-option",
|
|
900
868
|
// name: "Jane Tan",
|
|
@@ -902,7 +870,7 @@ watch(fetchPersonByNRICReq, (obj) => {
|
|
|
902
870
|
// contacts: ["91234567"],
|
|
903
871
|
// visitorPlateNumbers: ["SGB5678B"],
|
|
904
872
|
// companyName: ["Single Option Company"],
|
|
905
|
-
// } as Partial<
|
|
873
|
+
// } as Partial<SearchPerson>,
|
|
906
874
|
// ];
|
|
907
875
|
// nricSearchMenu.value = true;
|
|
908
876
|
});
|
|
@@ -947,12 +915,12 @@ function toUniqueStringArray(value: unknown) {
|
|
|
947
915
|
}
|
|
948
916
|
|
|
949
917
|
function getPersonContactOptions(item: Partial<TPeople>) {
|
|
950
|
-
const person = item as
|
|
918
|
+
const person = item as SearchPerson;
|
|
951
919
|
return toUniqueStringArray(person.contacts || person.contact);
|
|
952
920
|
}
|
|
953
921
|
|
|
954
922
|
function getPersonPlateOptions(item: Partial<TPeople>) {
|
|
955
|
-
const person = item as
|
|
923
|
+
const person = item as SearchPerson;
|
|
956
924
|
const visitorPlateNumbers = toUniqueStringArray(person.visitorPlateNumbers);
|
|
957
925
|
if (visitorPlateNumbers.length) return visitorPlateNumbers;
|
|
958
926
|
|
|
@@ -1000,6 +968,12 @@ function applyNricSearchResult(
|
|
|
1000
968
|
function selectNricSearchResult(item: Partial<TPeople>) {
|
|
1001
969
|
selectAutofillSearchResult(item, "nric");
|
|
1002
970
|
}
|
|
971
|
+
function selectPhoneNumberSearchResult(item: Partial<TPeople>) {
|
|
972
|
+
selectAutofillSearchResult(item, "contact");
|
|
973
|
+
}
|
|
974
|
+
function selectPlateNumberSearchResult(item: Partial<TPeople>) {
|
|
975
|
+
selectAutofillSearchResult(item, "vehicleNumber");
|
|
976
|
+
}
|
|
1003
977
|
|
|
1004
978
|
function selectAutofillSearchResult(item: Partial<TPeople>, source: Exclude<AutofillSource, null>) {
|
|
1005
979
|
const contactOptions = getPersonContactOptions(item);
|
|
@@ -1388,46 +1362,46 @@ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500);
|
|
|
1388
1362
|
|
|
1389
1363
|
function handleUpdateNRIC() {
|
|
1390
1364
|
if (skipNricFetch.value) return;
|
|
1391
|
-
if
|
|
1392
|
-
return;
|
|
1365
|
+
if(!isNricFieldFocused.value) return;
|
|
1393
1366
|
debounceFetchNRIC();
|
|
1394
1367
|
}
|
|
1395
1368
|
|
|
1369
|
+
|
|
1370
|
+
const {
|
|
1371
|
+
data: fetchPersonByPhoneNumber,
|
|
1372
|
+
refresh: fetchPersonByPhoneNumberRefresh,
|
|
1373
|
+
pending: fetchPersonByPhoneNumberPending,
|
|
1374
|
+
} = useLazyAsyncData(`fetch-person-phone-number`, () => {
|
|
1375
|
+
const contact = visitor.contact;
|
|
1376
|
+
if (!contact || contact.length < 4) return Promise.resolve(null);
|
|
1377
|
+
return findPersonByPhoneNumberMultipleResult(contact, prop.site);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1396
1380
|
function handleUpdatePhoneNumber() {
|
|
1397
|
-
if
|
|
1398
|
-
return;
|
|
1381
|
+
if(!isPhoneFieldFocused.value) return;
|
|
1399
1382
|
|
|
1400
1383
|
const search = visitor.contact?.trim() || "";
|
|
1401
|
-
if (search.length <
|
|
1384
|
+
if (search.length < 1) {
|
|
1402
1385
|
phoneSearchResults.value = [];
|
|
1403
1386
|
phoneSearchMenu.value = false;
|
|
1404
1387
|
return;
|
|
1405
1388
|
}
|
|
1406
1389
|
|
|
1407
|
-
phoneSearchResults.value = mockPhoneNumberSearchResults
|
|
1408
|
-
getPersonContactOptions(item as Partial<TPeople>).some((contact) =>
|
|
1409
|
-
contact.includes(search)
|
|
1410
|
-
)
|
|
1411
|
-
) as Partial<TPeople>[];
|
|
1390
|
+
phoneSearchResults.value = mockPhoneNumberSearchResults
|
|
1412
1391
|
phoneSearchMenu.value = phoneSearchResults.value.length > 0;
|
|
1413
1392
|
}
|
|
1414
1393
|
|
|
1415
1394
|
function handleUpdateVehicleNumber() {
|
|
1416
|
-
|
|
1417
|
-
return;
|
|
1395
|
+
if(!isPlateNumberFieldFocused.value) return;
|
|
1418
1396
|
|
|
1419
1397
|
const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
|
|
1420
|
-
if (search.length <
|
|
1398
|
+
if (search.length < 1) {
|
|
1421
1399
|
vehicleSearchResults.value = [];
|
|
1422
1400
|
vehicleSearchMenu.value = false;
|
|
1423
1401
|
return;
|
|
1424
1402
|
}
|
|
1425
1403
|
|
|
1426
|
-
vehicleSearchResults.value = mockVehicleNumberSearchResults
|
|
1427
|
-
getPersonPlateOptions(item as Partial<TPeople>).some((plate) =>
|
|
1428
|
-
plate.toUpperCase().includes(search)
|
|
1429
|
-
)
|
|
1430
|
-
) as Partial<TPeople>[];
|
|
1404
|
+
vehicleSearchResults.value = mockVehicleNumberSearchResults
|
|
1431
1405
|
vehicleSearchMenu.value = vehicleSearchResults.value.length > 0;
|
|
1432
1406
|
}
|
|
1433
1407
|
|
|
@@ -63,15 +63,12 @@ export function useNFCPatrolDailyReport(
|
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
65
|
const [logsRes, siteInfo] = await Promise.all([
|
|
66
|
-
getPatrolLogs
|
|
66
|
+
getPatrolLogs({
|
|
67
67
|
page: 1,
|
|
68
68
|
limit: 100,
|
|
69
69
|
site,
|
|
70
70
|
date: filters.date,
|
|
71
71
|
routeId: filters.route,
|
|
72
|
-
routeStartTime: filters.timeRange
|
|
73
|
-
? filters.timeRange.split(" - ")[0]
|
|
74
|
-
: undefined,
|
|
75
72
|
}),
|
|
76
73
|
getSiteById(site),
|
|
77
74
|
]);
|
|
@@ -2,6 +2,8 @@ import type {
|
|
|
2
2
|
NFCPatrolReportFilters,
|
|
3
3
|
NFCPatrolSummaryReport,
|
|
4
4
|
NFCPatrolReportSummaryRow,
|
|
5
|
+
NFCPatrolMonthlyReportRow,
|
|
6
|
+
NFCPatrolMonthlyReport,
|
|
5
7
|
} from "../types/nfc-patrol-report";
|
|
6
8
|
import useNFCPatrolLog from "./useNFCPatrolLog";
|
|
7
9
|
import useSite from "./useSite";
|
|
@@ -12,6 +14,55 @@ interface NFCPatrolLogListResponse {
|
|
|
12
14
|
pageRange?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
const MONTHS = [
|
|
18
|
+
"January",
|
|
19
|
+
"February",
|
|
20
|
+
"March",
|
|
21
|
+
"April",
|
|
22
|
+
"May",
|
|
23
|
+
"June",
|
|
24
|
+
"July",
|
|
25
|
+
"August",
|
|
26
|
+
"September",
|
|
27
|
+
"October",
|
|
28
|
+
"November",
|
|
29
|
+
"December",
|
|
30
|
+
];
|
|
31
|
+
function mapLogsToMonthlyRows(
|
|
32
|
+
logs: Record<string, any>[],
|
|
33
|
+
): NFCPatrolMonthlyReportRow[] {
|
|
34
|
+
const rows = MONTHS.map((month) => ({
|
|
35
|
+
month,
|
|
36
|
+
checked: 0,
|
|
37
|
+
checkedPercentage: 0,
|
|
38
|
+
missed: 0,
|
|
39
|
+
missedPercentage: 0,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
logs.forEach((log) => {
|
|
43
|
+
const monthIndex = new Date(log.date).getMonth();
|
|
44
|
+
|
|
45
|
+
for (const checkpoint of log.checkPoints ?? []) {
|
|
46
|
+
if (checkpoint.status === "Completed") {
|
|
47
|
+
rows[monthIndex].checked++;
|
|
48
|
+
} else if (checkpoint.status === "Skipped") {
|
|
49
|
+
rows[monthIndex].missed++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
rows.forEach((row) => {
|
|
55
|
+
const total = row.checked + row.missed;
|
|
56
|
+
|
|
57
|
+
if (total > 0) {
|
|
58
|
+
row.checkedPercentage = Math.round((row.checked / total) * 100);
|
|
59
|
+
row.missedPercentage = Math.round((row.missed / total) * 100);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
|
|
15
66
|
function buildAddress(address?: Record<string, string>) {
|
|
16
67
|
if (!address) return undefined;
|
|
17
68
|
|
|
@@ -27,23 +78,7 @@ function buildAddress(address?: Record<string, string>) {
|
|
|
27
78
|
return parts.length ? parts.join(", ") : undefined;
|
|
28
79
|
}
|
|
29
80
|
|
|
30
|
-
function mapLogToSummaryRow(
|
|
31
|
-
log: Record<string, any>,
|
|
32
|
-
index: number,
|
|
33
|
-
): NFCPatrolReportSummaryRow {
|
|
34
|
-
const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
|
|
35
|
-
const checked = checkpoints.filter((cp) => cp.status === "Completed").length;
|
|
36
|
-
const missed = checkpoints.filter((cp) => cp.status === "Skipped").length;
|
|
37
81
|
|
|
38
|
-
return {
|
|
39
|
-
id: log._id,
|
|
40
|
-
routeName: `${index + 1}. ${log.route?.name ?? "-"}`,
|
|
41
|
-
securityCheckSchedule: log.route?.startTime ?? "-",
|
|
42
|
-
checked,
|
|
43
|
-
missed,
|
|
44
|
-
status: missed === 0 ? "Completed" : "Incomplete",
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
82
|
|
|
48
83
|
export function useNFCPatrolMonthlyReport(
|
|
49
84
|
filters: NFCPatrolReportFilters,
|
|
@@ -52,12 +87,12 @@ export function useNFCPatrolMonthlyReport(
|
|
|
52
87
|
const { getAll: getPatrolLogs } = useNFCPatrolLog();
|
|
53
88
|
const { getSiteById } = useSite();
|
|
54
89
|
|
|
55
|
-
const report = ref<
|
|
90
|
+
const report = ref<NFCPatrolMonthlyReport | null>(null);
|
|
56
91
|
const loading = ref(false);
|
|
57
92
|
const notFound = ref(false);
|
|
58
93
|
|
|
59
94
|
async function fetchReport() {
|
|
60
|
-
if (!site || !filters.
|
|
95
|
+
if (!site || !filters.route) {
|
|
61
96
|
report.value = null;
|
|
62
97
|
notFound.value = false;
|
|
63
98
|
return;
|
|
@@ -65,19 +100,16 @@ export function useNFCPatrolMonthlyReport(
|
|
|
65
100
|
|
|
66
101
|
loading.value = true;
|
|
67
102
|
notFound.value = false;
|
|
68
|
-
|
|
103
|
+
const currentYear = new Date().getFullYear().toString();
|
|
69
104
|
try {
|
|
70
105
|
const [logsRes, siteInfo] = await Promise.all([
|
|
71
|
-
getPatrolLogs
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? filters.timeRange.split(" - ")[0]
|
|
79
|
-
: undefined,
|
|
80
|
-
type: "month",
|
|
106
|
+
getPatrolLogs({
|
|
107
|
+
page: 1,
|
|
108
|
+
limit: 100,
|
|
109
|
+
site,
|
|
110
|
+
routeId: filters.route,
|
|
111
|
+
date: currentYear,
|
|
112
|
+
type: "month",
|
|
81
113
|
}),
|
|
82
114
|
getSiteById(site),
|
|
83
115
|
]);
|
|
@@ -93,7 +125,7 @@ export function useNFCPatrolMonthlyReport(
|
|
|
93
125
|
address: buildAddress(siteInfo.address),
|
|
94
126
|
},
|
|
95
127
|
title: `Patrol Route Summary: ${routeName}`,
|
|
96
|
-
rows: logs
|
|
128
|
+
rows: mapLogsToMonthlyRows(logs),
|
|
97
129
|
};
|
|
98
130
|
} else {
|
|
99
131
|
report.value = null;
|
package/composables/usePeople.ts
CHANGED
|
@@ -44,6 +44,19 @@ export default function () {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
async function findPersonByPhoneNumberMultipleResult(contact: string, site: string) {
|
|
48
|
+
return await $fetch<Record<any, any>>(`/api/people/contact`, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
query: { contact, site },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function findPersonByPlateNumberMultipleResult(plateNumber: string, site: string) {
|
|
54
|
+
return await $fetch<Record<any, any>>(`/api/people/plate-number`, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
query: { plateNumber, site },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
async function findPersonByContact(
|
|
48
61
|
contact: string
|
|
49
62
|
): Promise<null | Partial<TPeople>> {
|
|
@@ -124,6 +137,8 @@ export default function () {
|
|
|
124
137
|
deleteById,
|
|
125
138
|
findPersonByNRIC,
|
|
126
139
|
findPersonByNRICMultipleResult,
|
|
140
|
+
findPersonByPhoneNumberMultipleResult,
|
|
141
|
+
findPersonByPlateNumberMultipleResult,
|
|
127
142
|
findPersonByContact,
|
|
128
143
|
getPeopleByUnit,
|
|
129
144
|
searchCompanyList,
|