@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.
@@ -46,136 +46,153 @@
46
46
  :density="density"
47
47
  :disabled="props.disabled"
48
48
  :placeholder="placeholder || currentMask"
49
+ @focus="emit('focus')"
50
+ @blur="emit('blur')"
49
51
  />
50
52
  </v-col>
51
- <span class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">
53
+ <span
54
+ class="text-error text-caption w-100"
55
+ v-if="errorMessage && !hideDetails"
56
+ >
52
57
  {{ errorMessage }}
53
58
  </span>
54
59
  </v-row>
55
60
  </template>
56
61
 
57
62
  <script setup lang="ts">
58
- import { ref, computed, watch, type PropType } from 'vue'
63
+ import { ref, computed, watch, type PropType } from "vue";
59
64
  //@ts-ignore
60
- import phoneMasks from '~/utils/phoneMasks'
61
- import type { ValidationRule } from 'vuetify/lib/types.mjs'
65
+ import phoneMasks from "~/utils/phoneMasks";
66
+ import type { ValidationRule } from "vuetify/lib/types.mjs";
62
67
 
63
68
  const props = defineProps({
64
- modelValue: { type: String as PropType<string>, default: '' },
69
+ modelValue: { type: String as PropType<string>, default: "" },
65
70
  rules: { type: Array as PropType<ValidationRule[]>, default: () => [] },
66
- variant: { type: String as PropType<any>, default: 'outlined' },
67
- density: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
71
+ variant: { type: String as PropType<any>, default: "outlined" },
72
+ density: {
73
+ type: String as PropType<"default" | "comfortable" | "compact">,
74
+ default: "default",
75
+ },
68
76
  placeholder: { type: String },
69
77
  hideDetails: { type: Boolean, default: false },
70
78
  loading: { type: Boolean, default: false },
71
79
  readOnly: { type: Boolean, default: false },
72
80
  disabled: { type: Boolean, default: false },
73
- })
81
+ });
74
82
 
75
- const emit = defineEmits(['update:modelValue'])
83
+ const emit = defineEmits(["update:modelValue", "focus", "blur"]);
76
84
 
77
85
  type TPhoneMask = {
78
- name: string
79
- flag: string
80
- code: string
81
- dial_code: string
82
- regex: string
83
- }
86
+ name: string;
87
+ flag: string;
88
+ code: string;
89
+ dial_code: string;
90
+ regex: string;
91
+ };
84
92
 
85
93
  // Main reactive values
86
- const phone = ref(props.modelValue)
87
- const input = ref('')
88
- const selectedCode = ref('SG')
89
- const countries = phoneMasks
90
- const errorMessage = ref('')
91
- const maskRef = ref()
92
- const maskKey = ref(0)
93
-
94
+ const phone = ref(props.modelValue);
95
+ const input = ref("");
96
+ const selectedCode = ref("SG");
97
+ const countries = phoneMasks;
98
+ const errorMessage = ref("");
99
+ const maskRef = ref();
100
+ const maskKey = ref(0);
94
101
 
95
102
  const currentMask = computed(() => {
96
- const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
97
- if (!country) return '############'
98
- return generateMaskFromRegex(country.regex)
99
- })
100
-
103
+ const country = countries.find(
104
+ (c: TPhoneMask) => c.code === selectedCode.value
105
+ );
106
+ if (!country) return "############";
107
+ return generateMaskFromRegex(country.regex);
108
+ });
101
109
 
102
110
  const phonePrefix = computed(() => {
103
- const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
104
- return country?.dial_code || ''
105
- })
111
+ const country = countries.find(
112
+ (c: TPhoneMask) => c.code === selectedCode.value
113
+ );
114
+ return country?.dial_code || "";
115
+ });
106
116
 
107
117
  const countriesByLongestDialCode = computed(() => {
108
- return [...countries].sort((a: TPhoneMask, b: TPhoneMask) => b.dial_code.length - a.dial_code.length)
109
- })
118
+ return [...countries].sort(
119
+ (a: TPhoneMask, b: TPhoneMask) => b.dial_code.length - a.dial_code.length
120
+ );
121
+ });
110
122
 
111
123
  function generateMaskFromRegex(regex: string): string {
112
- let pattern = regex.replace(/^\^|\$$/g, '')
113
- pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, '')
114
- pattern = pattern.replace(/\+?\d{1,4}/, '')
115
- pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)))
116
- pattern = pattern.replace(/\\d/g, '#')
117
- pattern = pattern.replace(/\\/g, '')
118
- pattern = pattern.replace(/\(\?:/g, '')
119
- return pattern.trim()
124
+ let pattern = regex.replace(/^\^|\$$/g, "");
125
+ pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, "");
126
+ pattern = pattern.replace(/\+?\d{1,4}/, "");
127
+ pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) =>
128
+ "#".repeat(Number(count))
129
+ );
130
+ pattern = pattern.replace(/\\d/g, "#");
131
+ pattern = pattern.replace(/\\/g, "");
132
+ pattern = pattern.replace(/\(\?:/g, "");
133
+ return pattern.trim();
120
134
  }
121
135
 
122
136
  const validatePhone = (): boolean | string => {
123
- if (props.readOnly) return true
124
- if (!phone.value) return true
137
+ if (props.readOnly) return true;
138
+ if (!phone.value) return true;
125
139
 
126
- const country = countries.find((c: TPhoneMask) => c.code === selectedCode.value)
127
- if (!country) return true
140
+ const country = countries.find(
141
+ (c: TPhoneMask) => c.code === selectedCode.value
142
+ );
143
+ if (!country) return true;
128
144
 
129
- const regex = new RegExp(country.regex)
130
- const isValid = regex.test(phone.value)
131
- errorMessage.value = isValid ? '' : `Invalid ${country.name} phone number`
132
- return isValid
133
- }
145
+ const regex = new RegExp(country.regex);
146
+ const isValid = regex.test(phone.value);
147
+ errorMessage.value = isValid ? "" : `Invalid ${country.name} phone number`;
148
+ return isValid;
149
+ };
134
150
 
135
151
  function findCountryFromPhone(value: string): TPhoneMask | undefined {
136
- return countriesByLongestDialCode.value.find((c: TPhoneMask) => value.startsWith(c.dial_code))
152
+ return countriesByLongestDialCode.value.find((c: TPhoneMask) =>
153
+ value.startsWith(c.dial_code)
154
+ );
137
155
  }
138
156
 
139
157
  function syncInputFromPhone(value: string) {
140
- const prefix = phonePrefix.value
141
- if (!value) input.value = ''
142
- else input.value = value.startsWith(prefix) ? value.slice(prefix.length) : value
158
+ const prefix = phonePrefix.value;
159
+ if (!value) input.value = "";
160
+ else
161
+ input.value = value.startsWith(prefix) ? value.slice(prefix.length) : value;
143
162
  }
144
163
 
145
164
  function handleUpdateCountry() {
146
- const prefix = phonePrefix.value
147
- phone.value = input.value ? prefix + input.value : ''
165
+ const prefix = phonePrefix.value;
166
+ phone.value = input.value ? prefix + input.value : "";
148
167
  }
149
168
 
150
-
151
169
  watch(input, (newInput) => {
152
- const prefix = phonePrefix.value
153
- if (!newInput) phone.value = ''
154
- else phone.value = prefix + newInput
155
- })
170
+ const prefix = phonePrefix.value;
171
+ if (!newInput) phone.value = "";
172
+ else phone.value = prefix + newInput;
173
+ });
156
174
 
157
175
  watch(
158
176
  () => props.modelValue,
159
177
  (val) => {
160
- phone.value = val || ''
178
+ phone.value = val || "";
161
179
  },
162
180
  { immediate: true }
163
- )
164
-
181
+ );
165
182
 
166
183
  watch(
167
184
  phone,
168
185
  (newVal) => {
169
- const country = newVal ? findCountryFromPhone(newVal) : undefined
170
- if (country && country.code !== selectedCode.value) selectedCode.value = country.code
171
- syncInputFromPhone(newVal)
172
- emit('update:modelValue', newVal)
186
+ const country = newVal ? findCountryFromPhone(newVal) : undefined;
187
+ if (country && country.code !== selectedCode.value)
188
+ selectedCode.value = country.code;
189
+ syncInputFromPhone(newVal);
190
+ emit("update:modelValue", newVal);
173
191
  },
174
192
  { immediate: true }
175
- )
176
-
193
+ );
177
194
 
178
195
  watch(selectedCode, () => {
179
- maskKey.value++
180
- })
196
+ maskKey.value++;
197
+ });
181
198
  </script>
@@ -6,6 +6,8 @@
6
6
  :placeholder="placeholder"
7
7
  :counter="maxlength"
8
8
  @input="onInput"
9
+ @focus="emit('focus')"
10
+ @blur="emit('blur')"
9
11
  outlined
10
12
  :disabled="disabled"
11
13
  :readonly="readonly"
@@ -14,36 +16,37 @@
14
16
  </template>
15
17
 
16
18
  <script setup>
19
+ const emit = defineEmits(["focus", "blur"]);
17
20
 
18
21
  const props = defineProps({
19
22
  placeholder: {
20
23
  type: String,
21
- default: 'Vehicle Number'
24
+ default: "Vehicle Number",
22
25
  },
23
26
  rules: {
24
27
  type: Array,
25
- default: () => []
28
+ default: () => [],
26
29
  },
27
30
  maxlength: {
28
31
  type: [Number, String],
29
- default: false
32
+ default: false,
30
33
  },
31
34
  disabled: {
32
35
  type: Boolean,
33
36
  },
34
37
  readonly: {
35
38
  type: Boolean,
36
- }
37
- })
39
+ },
40
+ });
38
41
 
39
- const model = defineModel({required: true })
42
+ const model = defineModel({ required: true });
40
43
 
41
44
  function onInput(event) {
42
- const value = typeof event === 'string' ? event : event?.target?.value || ''
45
+ const value = typeof event === "string" ? event : event?.target?.value || "";
43
46
 
44
- let formatted = value.replace(/[^A-Za-z0-9]/g, '')
45
- formatted = formatted.toUpperCase()
47
+ let formatted = value.replace(/[^A-Za-z0-9]/g, "");
48
+ formatted = formatted.toUpperCase();
46
49
 
47
- model.value = formatted
50
+ model.value = formatted;
48
51
  }
49
- </script>
52
+ </script>
@@ -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>