@7365admin1/layer-common 3.0.31-staging.23 → 3.1.0

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,63 +14,44 @@
14
14
  </v-col>
15
15
  </v-row>
16
16
 
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>
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>
34
21
  </v-tabs>
35
22
 
36
23
  <v-window v-model="activeTab" class="pt-4">
37
24
  <v-window-item value="patrol">
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>
25
+ <PatrolReportTab
26
+ :active="activeTab === 'patrol'"
27
+ :site="site"
28
+ :filters="filters"
29
+ :options="options"
30
+ @update:filters="updateFilters"
31
+ @export="exportReport"
32
+ />
48
33
  </v-window-item>
49
34
 
50
35
  <v-window-item value="daily">
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>
36
+ <DailyReportTab
37
+ :active="activeTab === 'daily'"
38
+ :site="site"
39
+ :filters="filters"
40
+ :options="options"
41
+ @update:filters="updateFilters"
42
+ @export="exportReport"
43
+ />
61
44
  </v-window-item>
62
45
 
63
46
  <v-window-item value="monthly">
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>
47
+ <MonthlyReportTab
48
+ :active="activeTab === 'monthly'"
49
+ :site="site"
50
+ :filters="filters"
51
+ :options="options"
52
+ @update:filters="updateFilters"
53
+ @export="exportReport"
54
+ />
74
55
  </v-window-item>
75
56
  </v-window>
76
57
  </v-col>
@@ -78,20 +59,15 @@
78
59
  </template>
79
60
 
80
61
  <script setup lang="ts">
81
- import html2canvas from "html2canvas";
82
- import jsPDF from "jspdf";
83
-
84
62
  import type {
85
63
  NFCPatrolReportFilters,
86
64
  NFCPatrolReportTab,
87
65
  } from "../../types/nfc-patrol-report";
88
-
66
+ import { useNFCPatrolReportExport } from "../../composables/useNFCPatrolReportExport";
89
67
  import { useNFCPatrolReportFilters } from "../../composables/useNFCPatrolReportFilters";
90
-
91
- import PatrolReportTab from "./PatrolReport/PatrolReportTab.vue";
92
68
  import DailyReportTab from "./PatrolReport/DailyReportTab.vue";
93
69
  import MonthlyReportTab from "./PatrolReport/MonthlyReportTab.vue";
94
-
70
+ import PatrolReportTab from "./PatrolReport/PatrolReportTab.vue";
95
71
 
96
72
  const props = defineProps<{
97
73
  site: string;
@@ -99,16 +75,11 @@ const props = defineProps<{
99
75
  }>();
100
76
 
101
77
  const router = useRouter();
102
-
103
-
104
78
  const activeTab = ref<NFCPatrolReportTab>("patrol");
79
+ const { filters, options, fetchOptions } = useNFCPatrolReportFilters(props.site);
80
+ const { exportReport: exportActiveReport } = useNFCPatrolReportExport();
105
81
 
106
- const { filters, options, fetchOptions } =
107
- useNFCPatrolReportFilters(props.site);
108
-
109
- onMounted(() => {
110
- fetchOptions();
111
- });
82
+ onMounted(fetchOptions);
112
83
 
113
84
  function goBack() {
114
85
  router.back();
@@ -120,72 +91,11 @@ function updateFilters(value: NFCPatrolReportFilters) {
120
91
  filters.date = value.date;
121
92
  }
122
93
 
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;",
94
+ async function exportReport() {
95
+ await exportActiveReport(activeTab.value, {
96
+ route: filters.route,
97
+ timeRange: filters.timeRange,
98
+ date: filters.date,
181
99
  });
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);
190
100
  }
191
101
  </script>
@@ -3,11 +3,10 @@
3
3
  <ReportFilters
4
4
  :model-value="filters"
5
5
  :options="options"
6
- :show-time-filter="false"
7
6
  @update:model-value="$emit('update:filters', $event)"
8
- @export="(type) => $emit('export', type)"
7
+ @export="$emit('export')"
9
8
  />
10
- <div class="report-export">
9
+
11
10
  <v-divider :thickness="1" class="border-opacity-100" />
12
11
 
13
12
  <template v-if="report">
@@ -16,7 +15,6 @@
16
15
  <SummaryReportTable :title="report.title" :rows="report.rows" />
17
16
  </template>
18
17
 
19
-
20
18
  <ReportEmptyState
21
19
  v-else-if="!loading && notFound"
22
20
  title="No Daily Report"
@@ -24,7 +22,6 @@
24
22
  />
25
23
 
26
24
  <ReportEmptyState v-else-if="!loading" />
27
- </div>
28
25
  </div>
29
26
  </template>
30
27
 
@@ -41,23 +38,23 @@ import SummaryReportTable from "./SummaryReportTable.vue";
41
38
 
42
39
  const props = defineProps<{
43
40
  active: boolean;
44
- site: string;
41
+ site: string; // ✅ thêm
45
42
  filters: NFCPatrolReportFilters;
46
43
  options: NFCPatrolReportFilterOptions;
47
44
  }>();
48
45
 
49
46
  defineEmits<{
50
47
  "update:filters": [value: NFCPatrolReportFilters];
51
- export: [type: "pdf" | "csv"];
48
+ export: [];
52
49
  }>();
53
50
 
54
51
  const { report, loading, notFound, fetchReport } = useNFCPatrolDailyReport(
55
52
  props.filters,
56
- props.site,
53
+ props.site, // ✅
57
54
  );
58
55
 
59
56
  watch(
60
- () => [props.filters.route, props.filters.date, props.active],
57
+ () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
61
58
  () => {
62
59
  if (props.active) {
63
60
  void fetchReport();
@@ -65,5 +62,4 @@ watch(
65
62
  },
66
63
  { immediate: true },
67
64
  );
68
-
69
65
  </script>
@@ -3,30 +3,23 @@
3
3
  <ReportFilters
4
4
  :model-value="filters"
5
5
  :options="options"
6
- :show-time-filter="false"
7
- :show-date-filter="false"
8
6
  @update:model-value="$emit('update:filters', $event)"
9
- @export="(type) => $emit('export', type)"
7
+ @export="$emit('export')"
10
8
  />
11
- <div class="report-export">
9
+
12
10
  <v-divider :thickness="1" class="border-opacity-100" />
13
11
 
14
12
  <template v-if="report">
15
13
  <ReportLocationHeader :location="report.location" />
16
14
  <v-divider :thickness="1" class="border-opacity-100 mb-6" />
17
-
18
- <MonthlyReportTable
19
- :title="report.title"
20
- :rows="report.rows"
21
- />
15
+ <SummaryReportTable :title="report.title" :rows="report.rows" />
22
16
  </template>
23
17
 
24
18
  <ReportEmptyState
25
19
  v-else-if="!loading"
26
- title="No Monthly Report"
27
- message="No patrol log found for this route"
20
+ title="No Monthly Report Selected"
21
+ message="Please select a route, time range, and date to view the monthly report"
28
22
  />
29
- </div>
30
23
  </div>
31
24
  </template>
32
25
 
@@ -35,13 +28,11 @@ import type {
35
28
  NFCPatrolReportFilterOptions,
36
29
  NFCPatrolReportFilters,
37
30
  } from "../../../types/nfc-patrol-report";
38
-
39
31
  import { useNFCPatrolMonthlyReport } from "../../../composables/useNFCPatrolMonthlyReport";
40
-
41
32
  import ReportEmptyState from "./ReportEmptyState.vue";
42
33
  import ReportFilters from "./ReportFilters.vue";
43
34
  import ReportLocationHeader from "./ReportLocationHeader.vue";
44
- import MonthlyReportTable from "./MonthlyReportTable.vue";
35
+ import SummaryReportTable from "./SummaryReportTable.vue";
45
36
 
46
37
  const props = defineProps<{
47
38
  active: boolean;
@@ -52,14 +43,14 @@ const props = defineProps<{
52
43
 
53
44
  defineEmits<{
54
45
  "update:filters": [value: NFCPatrolReportFilters];
55
- export: [type: "pdf" | "csv"];
46
+ export: [];
56
47
  }>();
57
48
 
58
- const { report, loading, notFound, fetchReport } =
49
+ const { report, loading, fetchReport } =
59
50
  useNFCPatrolMonthlyReport(props.filters, props.site);
60
51
 
61
52
  watch(
62
- () => [props.filters.route, props.active],
53
+ () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
63
54
  () => {
64
55
  if (props.active) {
65
56
  void fetchReport();
@@ -67,5 +58,4 @@ watch(
67
58
  },
68
59
  { immediate: true },
69
60
  );
70
-
71
61
  </script>
@@ -4,9 +4,9 @@
4
4
  :model-value="filters"
5
5
  :options="options"
6
6
  @update:model-value="$emit('update:filters', $event)"
7
- @export="(type) => $emit('export', type)"
7
+ @export="$emit('export')"
8
8
  />
9
- <div class="report-export">
9
+
10
10
  <v-divider :thickness="1" class="border-opacity-100" />
11
11
 
12
12
  <template v-if="report">
@@ -30,7 +30,6 @@
30
30
  <ReportEmptyState v-else-if="!loading" />
31
31
 
32
32
  <RemarksDialog v-model="dialogRemarks" :remarks="selectedActivityRemarks" />
33
- </div>
34
33
  </div>
35
34
  </template>
36
35
 
@@ -56,7 +55,7 @@ const props = defineProps<{
56
55
 
57
56
  defineEmits<{
58
57
  "update:filters": [value: NFCPatrolReportFilters];
59
- export: [type: "pdf" | "csv"];
58
+ export: [];
60
59
  }>();
61
60
 
62
61
  const {
@@ -78,5 +77,4 @@ watch(
78
77
  },
79
78
  { immediate: true },
80
79
  );
81
-
82
80
  </script>
@@ -16,12 +16,7 @@
16
16
  />
17
17
  </v-col>
18
18
 
19
- <v-col
20
- v-if="showTimeFilter"
21
- cols="12"
22
- md="2"
23
- class="px-md-2 mb-2 mb-md-0"
24
- >
19
+ <v-col cols="12" md="2" class="px-md-2 mb-2 mb-md-0">
25
20
  <v-select
26
21
  :model-value="modelValue.timeRange"
27
22
  :items="options.timeRanges"
@@ -35,12 +30,7 @@
35
30
  />
36
31
  </v-col>
37
32
 
38
- <v-col
39
- v-if="showDateFilter"
40
- cols="12"
41
- md="2"
42
- class="px-md-2 mb-2 mb-md-0"
43
- >
33
+ <v-col cols="12" md="2" class="px-md-2 mb-2 mb-md-0">
44
34
  <v-text-field
45
35
  :model-value="modelValue.date"
46
36
  type="date"
@@ -56,31 +46,17 @@
56
46
  <v-spacer />
57
47
 
58
48
  <v-col cols="12" md="2" class="pl-md-2">
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>
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>
84
60
  </v-col>
85
61
  </v-row>
86
62
  </template>
@@ -91,22 +67,14 @@ import type {
91
67
  NFCPatrolReportFilters,
92
68
  } from "../../../types/nfc-patrol-report";
93
69
 
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
- );;
70
+ const props = defineProps<{
71
+ modelValue: NFCPatrolReportFilters;
72
+ options: NFCPatrolReportFilterOptions;
73
+ }>();
106
74
 
107
75
  const emit = defineEmits<{
108
76
  "update:modelValue": [value: NFCPatrolReportFilters];
109
- export: [type: "pdf" | "csv"];
77
+ export: [];
110
78
  }>();
111
79
 
112
80
  function updateFilter(key: keyof NFCPatrolReportFilters, value: unknown) {
@@ -59,21 +59,6 @@
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
-
77
62
  <v-dialog v-model="photoOptionsDialog" max-width="420">
78
63
  <v-card>
79
64
  <v-card-title class="d-flex align-center pa-4">
@@ -211,8 +196,6 @@ const photos = ref<string[]>([...props.modelValue]);
211
196
  const photosIds = ref<string[]>([...props.photoIds]);
212
197
  const photoOptionsDialog = ref(false);
213
198
  const showCameraDialog = ref(false);
214
- const showPhotoPreview = ref(false);
215
- const selectedPhoto = ref("");
216
199
  const videoRef = ref<HTMLVideoElement | null>(null);
217
200
  const canvasRef = ref<HTMLCanvasElement | null>(null);
218
201
  const fileInputRef = ref<HTMLInputElement | null>(null);
@@ -222,8 +205,10 @@ const uploading = ref(false);
222
205
  const { addFile } = useFile();
223
206
 
224
207
  function viewPhoto(photoData: string) {
225
- selectedPhoto.value = photoData;
226
- showPhotoPreview.value = true;
208
+ const link = document.createElement("a");
209
+ link.href = photoData;
210
+ link.target = "_blank";
211
+ link.click();
227
212
  }
228
213
 
229
214
  watch(
@@ -548,7 +548,7 @@
548
548
  variant="flat"
549
549
  class="text-none sp-invite-submit-btn"
550
550
  height="76"
551
- :disabled="!canSubmitInvite"
551
+ :disabled="!validServiceProvider"
552
552
  :loading="disableServiceProvider"
553
553
  @click="submitServiceProviderInvite()"
554
554
  >
@@ -574,7 +574,6 @@ type ListTab = "active" | "pending" | "inactive";
574
574
 
575
575
  type TableRow = {
576
576
  _id: string;
577
- userId: string;
578
577
  serviceProviderOrgId: string;
579
578
  companyName: string;
580
579
  email: string;
@@ -734,10 +733,11 @@ const {
734
733
  updateStatus: updateServiceProviderStatus,
735
734
  invite,
736
735
  } = useServiceProvider();
737
- const { getAllByUserId } = useMember();
736
+ const { getAll: getAllMembers } = useMember();
738
737
 
739
738
  const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
740
739
  useVerification();
740
+ const { getOrgsByMembership } = useMember();
741
741
 
742
742
  const existingAccount = ref<any>(null);
743
743
  const checkingExistingAccount = ref(false);
@@ -757,10 +757,11 @@ 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();
760
762
 
761
763
  async function checkExistingAccount(email: string) {
762
764
  existingAccount.value = null;
763
- messageServiceProvider.value = "";
764
765
 
765
766
  if (!email.trim() || emailRule(email) !== true) return;
766
767
 
@@ -769,50 +770,30 @@ async function checkExistingAccount(email: string) {
769
770
  try {
770
771
  const user = await getUserByEmail(email);
771
772
 
772
- if (!user?._id) return;
773
-
774
- const members = await getAllByUserId(user._id);
775
-
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
- }
773
+ if (!user?._id) return;
784
774
 
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
- }
775
+ const orgs = await getOrgs({
776
+ user: user._id,
777
+ });
794
778
 
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
- };
779
+ const matchedOrg = orgs.items?.find(
780
+ (org: any) => org.type === serviceProviderInvite.value.app
781
+ );
803
782
 
783
+ if (matchedOrg) {
784
+ existingAccount.value = {
785
+ _id: matchedOrg.value,
786
+ text: matchedOrg.text,
787
+ ...matchedOrg,
788
+ };
789
+ }
804
790
  } catch (error) {
805
791
  existingAccount.value = null;
806
792
  } finally {
807
793
  checkingExistingAccount.value = false;
808
794
  }
809
795
  }
810
- const canSubmitInvite = computed(() => {
811
- return (
812
- validServiceProvider.value &&
813
- !messageServiceProvider.value
814
- );
815
- });
796
+
816
797
  watch(
817
798
  () => serviceProviderInvite.value.email,
818
799
  async (value) => {
@@ -958,7 +939,6 @@ function formatEmailCell(value: unknown): string {
958
939
  function mapProviderRows(raw: Record<string, any>[]): TableRow[] {
959
940
  return raw.map((row) => ({
960
941
  _id: String(row._id ?? ""),
961
- userId: String(row.user ?? ""),
962
942
  serviceProviderOrgId: String(row.serviceProviderOrgId ?? ""),
963
943
  companyName: row.name ?? "-",
964
944
  email: formatEmailCell(row.email),
@@ -981,7 +961,6 @@ function inviteTypeLabel(t: string) {
981
961
  function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
982
962
  return raw.map((row) => ({
983
963
  _id: String(row._id ?? ""),
984
- userId: String(row.user ?? ""),
985
964
  serviceProviderOrgId: String(row.metadata?.serviceProviderOrgId ?? ""),
986
965
  companyName: row.metadata?.name || "-",
987
966
  email: formatEmailCell(row.email),
@@ -1050,12 +1029,16 @@ async function openView(row: TableRow) {
1050
1029
 
1051
1030
  loadingMembers.value = true;
1052
1031
  try {
1053
- const data = await getAllByUserId(row.userId);
1054
-
1032
+ const data = await getAllMembers({
1033
+ page: 1,
1034
+ limit: 1000,
1035
+ org: row.serviceProviderOrgId,
1036
+ status: "active",
1037
+ });
1055
1038
  serviceProviderMembers.value = (data.items ?? []).map((member: any) => ({
1056
- _id: member._id,
1057
- name: member.name || "-",
1058
- role: member.role || "-",
1039
+ _id: String(member._id ?? ""),
1040
+ name: member.name || member.fullName || member.email || "-",
1041
+ role: member.roleName || member.role?.name || "-",
1059
1042
  }));
1060
1043
  } catch (error: any) {
1061
1044
  showMessage(errorConverter(error), "error");