@7365admin1/layer-common 3.0.31-staging.15 → 3.0.31-staging.17

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.
@@ -14,44 +14,63 @@
14
14
  </v-col>
15
15
  </v-row>
16
16
 
17
- <v-tabs v-model="activeTab" class="mb-6" bg-color="transparent" grow>
18
- <v-tab value="patrol" class="text-none border-thin">Patrol Report</v-tab>
19
- <v-tab value="daily" class="text-none border-thin">Daily Report</v-tab>
20
- <v-tab value="monthly" class="text-none border-thin">Monthly Report</v-tab>
17
+ <v-tabs
18
+ v-model="activeTab"
19
+ class="mb-6"
20
+ bg-color="transparent"
21
+ grow
22
+ >
23
+ <v-tab value="patrol" class="text-none border-thin">
24
+ Patrol Report
25
+ </v-tab>
26
+
27
+ <v-tab value="daily" class="text-none border-thin">
28
+ Daily Report
29
+ </v-tab>
30
+
31
+ <v-tab value="monthly" class="text-none border-thin">
32
+ Monthly Report
33
+ </v-tab>
21
34
  </v-tabs>
22
35
 
23
36
  <v-window v-model="activeTab" class="pt-4">
24
37
  <v-window-item value="patrol">
25
- <PatrolReportTab
26
- :active="activeTab === 'patrol'"
27
- :site="site"
28
- :filters="filters"
29
- :options="options"
30
- @update:filters="updateFilters"
31
- @export="exportReport"
32
- />
38
+ <div v-if="activeTab === 'patrol'">
39
+ <PatrolReportTab
40
+ :active="true"
41
+ :site="site"
42
+ :filters="filters"
43
+ :options="options"
44
+ @update:filters="updateFilters"
45
+ @export="exportReport"
46
+ />
47
+ </div>
33
48
  </v-window-item>
34
49
 
35
50
  <v-window-item value="daily">
36
- <DailyReportTab
37
- :active="activeTab === 'daily'"
38
- :site="site"
39
- :filters="filters"
40
- :options="options"
41
- @update:filters="updateFilters"
42
- @export="exportReport"
43
- />
51
+ <div v-if="activeTab === 'daily'">
52
+ <DailyReportTab
53
+ :active="true"
54
+ :site="site"
55
+ :filters="filters"
56
+ :options="options"
57
+ @update:filters="updateFilters"
58
+ @export="exportReport"
59
+ />
60
+ </div>
44
61
  </v-window-item>
45
62
 
46
63
  <v-window-item value="monthly">
47
- <MonthlyReportTab
48
- :active="activeTab === 'monthly'"
49
- :site="site"
50
- :filters="filters"
51
- :options="options"
52
- @update:filters="updateFilters"
53
- @export="exportReport"
54
- />
64
+ <div v-if="activeTab === 'monthly'">
65
+ <MonthlyReportTab
66
+ :active="true"
67
+ :site="site"
68
+ :filters="filters"
69
+ :options="options"
70
+ @update:filters="updateFilters"
71
+ @export="exportReport"
72
+ />
73
+ </div>
55
74
  </v-window-item>
56
75
  </v-window>
57
76
  </v-col>
@@ -59,15 +78,20 @@
59
78
  </template>
60
79
 
61
80
  <script setup lang="ts">
81
+ import html2canvas from "html2canvas";
82
+ import jsPDF from "jspdf";
83
+
62
84
  import type {
63
85
  NFCPatrolReportFilters,
64
86
  NFCPatrolReportTab,
65
87
  } from "../../types/nfc-patrol-report";
66
- import { useNFCPatrolReportExport } from "../../composables/useNFCPatrolReportExport";
88
+
67
89
  import { useNFCPatrolReportFilters } from "../../composables/useNFCPatrolReportFilters";
90
+
91
+ import PatrolReportTab from "./PatrolReport/PatrolReportTab.vue";
68
92
  import DailyReportTab from "./PatrolReport/DailyReportTab.vue";
69
93
  import MonthlyReportTab from "./PatrolReport/MonthlyReportTab.vue";
70
- import PatrolReportTab from "./PatrolReport/PatrolReportTab.vue";
94
+
71
95
 
72
96
  const props = defineProps<{
73
97
  site: string;
@@ -75,11 +99,16 @@ const props = defineProps<{
75
99
  }>();
76
100
 
77
101
  const router = useRouter();
102
+
103
+
78
104
  const activeTab = ref<NFCPatrolReportTab>("patrol");
79
- const { filters, options, fetchOptions } = useNFCPatrolReportFilters(props.site);
80
- const { exportReport: exportActiveReport } = useNFCPatrolReportExport();
81
105
 
82
- onMounted(fetchOptions);
106
+ const { filters, options, fetchOptions } =
107
+ useNFCPatrolReportFilters(props.site);
108
+
109
+ onMounted(() => {
110
+ fetchOptions();
111
+ });
83
112
 
84
113
  function goBack() {
85
114
  router.back();
@@ -91,11 +120,72 @@ function updateFilters(value: NFCPatrolReportFilters) {
91
120
  filters.date = value.date;
92
121
  }
93
122
 
94
- async function exportReport() {
95
- await exportActiveReport(activeTab.value, {
96
- route: filters.route,
97
- timeRange: filters.timeRange,
98
- date: filters.date,
123
+ async function exportReport(type: "pdf" | "csv") {
124
+ const report = document.querySelector(
125
+ ".report-export",
126
+ ) as HTMLElement | null;
127
+
128
+ if (!report) return;
129
+
130
+ if (type === "pdf") {
131
+ const canvas = await html2canvas(report, {
132
+ scale: 2,
133
+ useCORS: true,
134
+ backgroundColor: "#fff",
135
+ });
136
+
137
+ const pdf = new jsPDF("p", "mm", "a4");
138
+
139
+ const pdfWidth = pdf.internal.pageSize.getWidth();
140
+ const pdfHeight = pdf.internal.pageSize.getHeight();
141
+
142
+ const imgWidth = pdfWidth;
143
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
144
+
145
+ const imgData = canvas.toDataURL("image/png");
146
+
147
+ let heightLeft = imgHeight;
148
+ let position = 0;
149
+
150
+ pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
151
+
152
+ heightLeft -= pdfHeight;
153
+
154
+ while (heightLeft > 0) {
155
+ position -= pdfHeight;
156
+
157
+ pdf.addPage();
158
+ pdf.addImage(imgData, "PNG", 0, position, imgWidth, imgHeight);
159
+
160
+ heightLeft -= pdfHeight;
161
+ }
162
+
163
+ pdf.save(`${activeTab.value}-report.pdf`);
164
+ return;
165
+ }
166
+
167
+ const table = report.querySelector("table");
168
+
169
+ if (!table) return;
170
+
171
+ const csv = Array.from(table.querySelectorAll("tr"))
172
+ .map((row) =>
173
+ Array.from(row.querySelectorAll("th,td"))
174
+ .map((cell) => `"${cell.textContent?.trim().replace(/"/g, '""') ?? ""}"`)
175
+ .join(","),
176
+ )
177
+ .join("\n");
178
+
179
+ const blob = new Blob([csv], {
180
+ type: "text/csv;charset=utf-8;",
99
181
  });
182
+
183
+ const link = document.createElement("a");
184
+
185
+ link.href = URL.createObjectURL(blob);
186
+ link.download = `${activeTab.value}-report.csv`;
187
+ link.click();
188
+
189
+ URL.revokeObjectURL(link.href);
100
190
  }
101
191
  </script>
@@ -3,10 +3,11 @@
3
3
  <ReportFilters
4
4
  :model-value="filters"
5
5
  :options="options"
6
+ :show-time-filter="false"
6
7
  @update:model-value="$emit('update:filters', $event)"
7
- @export="$emit('export')"
8
+ @export="(type) => $emit('export', type)"
8
9
  />
9
-
10
+ <div class="report-export">
10
11
  <v-divider :thickness="1" class="border-opacity-100" />
11
12
 
12
13
  <template v-if="report">
@@ -15,6 +16,7 @@
15
16
  <SummaryReportTable :title="report.title" :rows="report.rows" />
16
17
  </template>
17
18
 
19
+
18
20
  <ReportEmptyState
19
21
  v-else-if="!loading && notFound"
20
22
  title="No Daily Report"
@@ -22,6 +24,7 @@
22
24
  />
23
25
 
24
26
  <ReportEmptyState v-else-if="!loading" />
27
+ </div>
25
28
  </div>
26
29
  </template>
27
30
 
@@ -38,23 +41,23 @@ import SummaryReportTable from "./SummaryReportTable.vue";
38
41
 
39
42
  const props = defineProps<{
40
43
  active: boolean;
41
- site: string; // ✅ thêm
44
+ site: string;
42
45
  filters: NFCPatrolReportFilters;
43
46
  options: NFCPatrolReportFilterOptions;
44
47
  }>();
45
48
 
46
49
  defineEmits<{
47
50
  "update:filters": [value: NFCPatrolReportFilters];
48
- export: [];
51
+ export: [type: "pdf" | "csv"];
49
52
  }>();
50
53
 
51
54
  const { report, loading, notFound, fetchReport } = useNFCPatrolDailyReport(
52
55
  props.filters,
53
- props.site, // ✅
56
+ props.site,
54
57
  );
55
58
 
56
59
  watch(
57
- () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
60
+ () => [props.filters.route, props.filters.date, props.active],
58
61
  () => {
59
62
  if (props.active) {
60
63
  void fetchReport();
@@ -62,4 +65,5 @@ watch(
62
65
  },
63
66
  { immediate: true },
64
67
  );
68
+
65
69
  </script>
@@ -3,23 +3,30 @@
3
3
  <ReportFilters
4
4
  :model-value="filters"
5
5
  :options="options"
6
+ :show-time-filter="false"
7
+ :show-date-filter="false"
6
8
  @update:model-value="$emit('update:filters', $event)"
7
- @export="$emit('export')"
9
+ @export="(type) => $emit('export', type)"
8
10
  />
9
-
11
+ <div class="report-export">
10
12
  <v-divider :thickness="1" class="border-opacity-100" />
11
13
 
12
14
  <template v-if="report">
13
15
  <ReportLocationHeader :location="report.location" />
14
16
  <v-divider :thickness="1" class="border-opacity-100 mb-6" />
15
- <SummaryReportTable :title="report.title" :rows="report.rows" />
17
+
18
+ <MonthlyReportTable
19
+ :title="report.title"
20
+ :rows="report.rows"
21
+ />
16
22
  </template>
17
23
 
18
24
  <ReportEmptyState
19
25
  v-else-if="!loading"
20
- title="No Monthly Report Selected"
21
- message="Please select a route, time range, and date to view the monthly report"
26
+ title="No Monthly Report"
27
+ message="No patrol log found for this route"
22
28
  />
29
+ </div>
23
30
  </div>
24
31
  </template>
25
32
 
@@ -28,11 +35,13 @@ import type {
28
35
  NFCPatrolReportFilterOptions,
29
36
  NFCPatrolReportFilters,
30
37
  } from "../../../types/nfc-patrol-report";
38
+
31
39
  import { useNFCPatrolMonthlyReport } from "../../../composables/useNFCPatrolMonthlyReport";
40
+
32
41
  import ReportEmptyState from "./ReportEmptyState.vue";
33
42
  import ReportFilters from "./ReportFilters.vue";
34
43
  import ReportLocationHeader from "./ReportLocationHeader.vue";
35
- import SummaryReportTable from "./SummaryReportTable.vue";
44
+ import MonthlyReportTable from "./MonthlyReportTable.vue";
36
45
 
37
46
  const props = defineProps<{
38
47
  active: boolean;
@@ -43,14 +52,14 @@ const props = defineProps<{
43
52
 
44
53
  defineEmits<{
45
54
  "update:filters": [value: NFCPatrolReportFilters];
46
- export: [];
55
+ export: [type: "pdf" | "csv"];
47
56
  }>();
48
57
 
49
- const { report, loading, fetchReport } =
58
+ const { report, loading, notFound, fetchReport } =
50
59
  useNFCPatrolMonthlyReport(props.filters, props.site);
51
60
 
52
61
  watch(
53
- () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
62
+ () => [props.filters.route, props.active],
54
63
  () => {
55
64
  if (props.active) {
56
65
  void fetchReport();
@@ -58,4 +67,5 @@ watch(
58
67
  },
59
68
  { immediate: true },
60
69
  );
70
+
61
71
  </script>
@@ -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) {
@@ -548,7 +548,7 @@
548
548
  variant="flat"
549
549
  class="text-none sp-invite-submit-btn"
550
550
  height="76"
551
- :disabled="!validServiceProvider"
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 { 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);
@@ -757,11 +757,10 @@ 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
760
 
763
761
  async function checkExistingAccount(email: string) {
764
762
  existingAccount.value = null;
763
+ messageServiceProvider.value = "";
765
764
 
766
765
  if (!email.trim() || emailRule(email) !== true) return;
767
766
 
@@ -770,30 +769,50 @@ async function checkExistingAccount(email: string) {
770
769
  try {
771
770
  const user = await getUserByEmail(email);
772
771
 
773
- if (!user?._id) return;
772
+ if (!user?._id) return;
774
773
 
775
- const orgs = await getOrgs({
776
- user: user._id,
777
- });
774
+ const members = await getAllByUserId(user._id);
778
775
 
779
- const matchedOrg = orgs.items?.find(
780
- (org: any) => org.type === serviceProviderInvite.value.app
781
- );
776
+ const appMember = (members.items ?? []).find(
777
+ (member: any) => member.type === serviceProviderInvite.value.app
778
+ );
779
+
780
+ if (!appMember) {
781
+ existingAccount.value = null;
782
+ return;
783
+ }
784
+
785
+ if (
786
+ appMember.org === props.orgId &&
787
+ (appMember.siteId || "") === (props.siteId || "")
788
+ ) {
789
+ existingAccount.value = null;
790
+ messageServiceProvider.value =
791
+ "This user is already a member of this service provider.";
792
+ return;
793
+ }
794
+
795
+ existingAccount.value = {
796
+ _id: appMember.org,
797
+ text: appMember.orgName,
798
+ org: appMember.org,
799
+ orgName: appMember.orgName,
800
+ siteId: appMember.siteId,
801
+ siteName: appMember.siteName,
802
+ };
782
803
 
783
- if (matchedOrg) {
784
- existingAccount.value = {
785
- _id: matchedOrg.value,
786
- text: matchedOrg.text,
787
- ...matchedOrg,
788
- };
789
- }
790
804
  } catch (error) {
791
805
  existingAccount.value = null;
792
806
  } finally {
793
807
  checkingExistingAccount.value = false;
794
808
  }
795
809
  }
796
-
810
+ const canSubmitInvite = computed(() => {
811
+ return (
812
+ validServiceProvider.value &&
813
+ !messageServiceProvider.value
814
+ );
815
+ });
797
816
  watch(
798
817
  () => serviceProviderInvite.value.email,
799
818
  async (value) => {
@@ -939,6 +958,7 @@ function formatEmailCell(value: unknown): string {
939
958
  function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
940
959
  return raw.map((row) => ({
941
960
  _id: String(row._id ?? ""),
961
+ userId: String(row.user ?? ""),
942
962
  serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
943
963
  companyName: row.name ?? "-",
944
964
  email: formatEmailCell(row.email),
@@ -961,6 +981,7 @@ function inviteTypeLabel(t: string) {
961
981
  function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
962
982
  return raw.map((row) => ({
963
983
  _id: String(row._id ?? ""),
984
+ userId: String(row.user ?? ""),
964
985
  serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
965
986
  companyName: row.metadata?.name || "-",
966
987
  email: formatEmailCell(row.email),
@@ -1029,16 +1050,12 @@ async function openView(row: TableRow) {
1029
1050
 
1030
1051
  loadingMembers.value = true;
1031
1052
  try {
1032
- const data = await getAllMembers({
1033
- page: 1,
1034
- limit: 1000,
1035
- org: row.serviceProviderOrgId,
1036
- status: "active",
1037
- });
1053
+ const data = await getAllByUserId(row.userId);
1054
+
1038
1055
  serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
1039
- _id: String(member._id ?? ""),
1040
- name: member.name || member.fullName || member.email || "-",
1041
- role: member.roleName || member.role?.name || "-",
1056
+ _id: member._id,
1057
+ name: member.name || "-",
1058
+ role: member.role || "-",
1042
1059
  }));
1043
1060
  } catch (error: any) {
1044
1061
  showMessage(errorConverter(error), "error");
@@ -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;
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.15",
5
+ "version": "3.0.31-staging.17",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -26,7 +26,7 @@ export interface NFCPatrolReportFilterOptions {
26
26
 
27
27
  export interface NFCPatrolReportLocation {
28
28
  name?: string;
29
- address?: string;
29
+ address?: string;
30
30
  }
31
31
 
32
32
  export interface NFCPatrolRouteSummary {
@@ -77,3 +77,17 @@ export interface NFCPatrolSummaryReport {
77
77
  title: string;
78
78
  rows: NFCPatrolReportSummaryRow[];
79
79
  }
80
+
81
+ export interface NFCPatrolMonthlyReportRow {
82
+ month: string;
83
+ checked: number;
84
+ checkedPercentage: number;
85
+ missed: number;
86
+ missedPercentage: number;
87
+ }
88
+
89
+ export interface NFCPatrolMonthlyReport {
90
+ location: NFCPatrolReportLocation;
91
+ title: string;
92
+ rows: NFCPatrolMonthlyReportRow[];
93
+ }