@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.
@@ -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 cols="12" md="2" class="px-md-2 mb-2 mb-md-0">
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 cols="12" md="2" class="px-md-2 mb-2 mb-md-0">
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-btn
50
- variant="outlined"
51
- block
52
- size="large"
53
- rounded="lg"
54
- class="text-none"
55
- prepend-icon="mdi-export"
56
- @click="$emit('export')"
57
- >
58
- Export
59
- </v-btn>
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 = defineProps<{
71
- modelValue: NFCPatrolReportFilters;
72
- options: NFCPatrolReportFilterOptions;
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
- }}</span>
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
- }}</strong>" as custom contractor type.
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="selectAutofillSearchResult(item, 'contact')" class="cursor-pointer nric-search-item">
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="selectAutofillSearchResult(item, 'vehicleNumber')"
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
- }}</strong>" as new company.
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
- }}</strong>" as new company.
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 NricSearchPerson = Omit<Partial<TPeople>, "contact"> & {
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 < 4) return Promise.resolve(null);
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<NricSearchPerson>,
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<NricSearchPerson>,
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 NricSearchPerson;
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 NricSearchPerson;
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 (currentAutofillSource.value && currentAutofillSource.value !== "nric")
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 (currentAutofillSource.value && currentAutofillSource.value !== "contact")
1398
- return;
1381
+ if(!isPhoneFieldFocused.value) return;
1399
1382
 
1400
1383
  const search = visitor.contact?.trim() || "";
1401
- if (search.length < 4) {
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.filter((item) =>
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
- if (currentAutofillSource.value && currentAutofillSource.value !== "vehicleNumber")
1417
- return;
1395
+ if(!isPlateNumberFieldFocused.value) return;
1418
1396
 
1419
1397
  const search = visitor.plateNumber?.trim()?.toUpperCase() || "";
1420
- if (search.length < 4) {
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.filter((item) =>
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<NFCPatrolLogListResponse>({
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<NFCPatrolSummaryReport | null>(null);
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.date || !filters.route) {
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<NFCPatrolLogListResponse>({
72
- page: 1,
73
- limit: 100,
74
- site,
75
- date: filters.date,
76
- routeId: filters.route,
77
- routeStartTime: filters.timeRange
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.map(mapLogToSummaryRow),
128
+ rows: mapLogsToMonthlyRows(logs),
97
129
  };
98
130
  } else {
99
131
  report.value = null;
@@ -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,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.31-staging.14",
5
+ "version": "3.0.31-staging.16",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {