tantee-nuxt-commons 0.0.173 → 0.0.175

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.
Files changed (35) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +9 -1
  3. package/dist/runtime/components/Alert.vue +1 -0
  4. package/dist/runtime/components/device/IdCardButton.vue +83 -0
  5. package/dist/runtime/components/device/IdCardWebSocket.vue +195 -0
  6. package/dist/runtime/components/device/Scanner.vue +338 -0
  7. package/dist/runtime/components/form/ActionPad.vue +1 -1
  8. package/dist/runtime/components/form/Dialog.vue +1 -1
  9. package/dist/runtime/components/form/EditPad.vue +1 -1
  10. package/dist/runtime/components/form/Iterator.vue +0 -2
  11. package/dist/runtime/components/form/Pad.vue +71 -2
  12. package/dist/runtime/components/form/Table.vue +4 -1
  13. package/dist/runtime/components/form/TableData.vue +4 -1
  14. package/dist/runtime/components/label/DateCount.vue +117 -13
  15. package/dist/runtime/components/model/Autocomplete.vue +4 -1
  16. package/dist/runtime/components/model/Combobox.vue +4 -1
  17. package/dist/runtime/components/model/Select.vue +4 -1
  18. package/dist/runtime/components/pdf/View.vue +42 -32
  19. package/dist/runtime/composables/api.d.ts +4 -4
  20. package/dist/runtime/composables/api.js +33 -21
  21. package/dist/runtime/composables/clientConfig.d.ts +15 -0
  22. package/dist/runtime/composables/clientConfig.js +79 -0
  23. package/dist/runtime/composables/document/templateFormTable.js +4 -1
  24. package/dist/runtime/composables/hostAgent.d.ts +260 -0
  25. package/dist/runtime/composables/hostAgent.js +74 -0
  26. package/dist/runtime/composables/hostAgentWs.d.ts +272 -0
  27. package/dist/runtime/composables/hostAgentWs.js +145 -0
  28. package/dist/runtime/composables/localStorageModel.d.ts +38 -0
  29. package/dist/runtime/composables/localStorageModel.js +88 -0
  30. package/dist/runtime/plugins/clientConfig.d.ts +2 -0
  31. package/dist/runtime/plugins/clientConfig.js +22 -0
  32. package/dist/runtime/plugins/default.d.ts +2 -0
  33. package/dist/runtime/plugins/default.js +50 -0
  34. package/dist/runtime/types/clientConfig.d.ts +13 -0
  35. package/package.json +3 -2
@@ -34,6 +34,7 @@ interface Props {
34
34
  parentTemplates?: string|string[]
35
35
  dirtyClass?: string
36
36
  dirtyOnCreate?: boolean
37
+ sanitizeDelay?: number
37
38
  }
38
39
 
39
40
  const props = withDefaults(defineProps<Props>(), {
@@ -43,7 +44,8 @@ const props = withDefaults(defineProps<Props>(), {
43
44
  decoration: () => { return {} },
44
45
  parentTemplates: (): string[] => [],
45
46
  dirtyClass: "form-data-dirty",
46
- dirtyOnCreate: false
47
+ dirtyOnCreate: false,
48
+ sanitizeDelay: 2000,
47
49
  })
48
50
 
49
51
  const emit = defineEmits(['update:modelValue'])
@@ -87,7 +89,7 @@ function isBlankString(v: unknown): v is string {
87
89
  return isString(v) && v.trim().length === 0
88
90
  }
89
91
 
90
- const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, 500)
92
+ const sanitizeBlankStrings = debounce(sanitizeBlankStringsRaw, props.sanitizeDelay)
91
93
 
92
94
  function sanitizeBlankStringsRaw(val: any, original?: any): void {
93
95
  if (!original && props.originalData) {
@@ -125,6 +127,71 @@ function sanitizeBlankStringsRaw(val: any, original?: any): void {
125
127
  }
126
128
  }
127
129
 
130
+ function autoSanitizedDisplay(item: any, separator: string = ","): string | undefined {
131
+ const isEmptyScalar = (v: any) =>
132
+ v === null ||
133
+ v === undefined ||
134
+ (typeof v === "string" && v.trim() === "") ||
135
+ (typeof v === "number" && Number.isNaN(v));
136
+
137
+ const toStr = (v: any): string | undefined => {
138
+ if (isEmptyScalar(v)) return undefined;
139
+ if (typeof v === "string") return v.trim();
140
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") return String(v);
141
+ if (typeof v === "symbol") return v.description ?? String(v);
142
+ if (typeof v === "function") return v.name ? `[Function ${v.name}]` : "[Function]";
143
+ return undefined;
144
+ };
145
+
146
+ // 1) empty -> undefined
147
+ if (isEmptyScalar(item)) return undefined;
148
+
149
+ // 2) array -> recurse + join
150
+ if (Array.isArray(item)) {
151
+ const parts = item
152
+ .map((x) => autoSanitizedDisplay(x, separator))
153
+ .filter((s): s is string => typeof s === "string" && s.trim() !== "");
154
+ return parts.length ? parts.join(separator) : undefined;
155
+ }
156
+
157
+ // simple scalars
158
+ const scalar = toStr(item);
159
+ if (scalar !== undefined) return scalar;
160
+
161
+ // 3) object
162
+ if (typeof item === "object") {
163
+ // 3.1 label
164
+ if ("label" in item) {
165
+ const v = autoSanitizedDisplay((item as any).label, separator) ?? toStr((item as any).label);
166
+ if (v !== undefined) return v;
167
+ }
168
+ // 3.2 value
169
+ if ("value" in item) {
170
+ const v = autoSanitizedDisplay((item as any).value, separator) ?? toStr((item as any).value);
171
+ if (v !== undefined) return v;
172
+ }
173
+
174
+ // 3.3 stringify attributes as key:value (recurse values)
175
+ const entries = Object.entries(item as Record<string, any>)
176
+ .map(([k, v]) => {
177
+ const rendered = autoSanitizedDisplay(v, separator) ?? toStr(v);
178
+ if (rendered === undefined || rendered.trim() === "") return undefined;
179
+ return `${k}:${rendered}`;
180
+ })
181
+ .filter((x): x is string => typeof x === "string" && x.trim() !== "");
182
+
183
+ return entries.length ? entries.join(separator) : undefined;
184
+ }
185
+
186
+ // fallback
187
+ try {
188
+ const s = String(item);
189
+ return s.trim() ? s : undefined;
190
+ } catch {
191
+ return undefined;
192
+ }
193
+ }
194
+
128
195
  watch(formData, (newValue) => {
129
196
  sanitizeBlankStrings(newValue)
130
197
  emit('update:modelValue', newValue)
@@ -219,6 +286,7 @@ function buildFormComponent() {
219
286
  resetValidate: () => formPadTemplate.value.resetValidate(),
220
287
  isValid,
221
288
  ...templateScriptFunction.value(props, ctx, ...Object.values(vueFunctions)),
289
+ autoSanitizedDisplay
222
290
  }
223
291
  },
224
292
  template: componentTemplate,
@@ -279,6 +347,7 @@ defineExpose({
279
347
  :disabled="disabled"
280
348
  :readonly="readonly"
281
349
  :class="$attrs.class"
350
+ autocomplete="off"
282
351
  >
283
352
  <template #default="formProvided">
284
353
  <slot
@@ -5,6 +5,7 @@ import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch, us
5
5
  import {omit} from 'lodash-es'
6
6
  import {useDialog} from "../../composables/dialog"
7
7
  import type {FormDialogCallback} from '../../types/formDialog'
8
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false,
@@ -32,7 +33,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
32
33
  stringFields?: Array<string>
33
34
  }
34
35
 
35
- const props = withDefaults(defineProps<Props>(), {
36
+ const props = withDefaults(defineProps<Props & PersistSlimProps>(), {
36
37
  noDataText: 'ไม่พบข้อมูล',
37
38
  dialogFullscreen: false,
38
39
  modelKey: 'id',
@@ -59,6 +60,8 @@ const items = ref<Record<string, any>[]>([])
59
60
  const search = ref<string>()
60
61
  const currentItem = ref<Record<string, any> | undefined>(undefined)
61
62
 
63
+ useLocalStorageModel(items,props)
64
+
62
65
  function setSearch(keyword: string) {
63
66
  search.value = keyword
64
67
  }
@@ -5,6 +5,7 @@ import {computed, defineOptions,defineExpose, ref, useAttrs, watch, useTemplateR
5
5
  import {cloneDeep, isEqual, omit, isArray, isString} from 'lodash-es'
6
6
  import {templateItemToString} from "../../composables/document/template";
7
7
  import {templateToHeader} from "../../composables/document/templateFormTable";
8
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false,
@@ -22,7 +23,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
22
23
  disableApplyToAll?: boolean | string | string[]
23
24
  }
24
25
 
25
- const props = withDefaults(defineProps<Props>(), {
26
+ const props = withDefaults(defineProps<Props & PersistSlimProps>(), {
26
27
  noDataText: 'ไม่พบข้อมูล',
27
28
  modelKey: 'id',
28
29
  toolbarColor: 'primary',
@@ -40,6 +41,8 @@ const inputRef = useTemplateRef<VInput>("inputRef")
40
41
  const itemsInternal = ref<Record<string, any>[]>([])
41
42
  const itemsApplyAll = ref<Record<string, any>>({})
42
43
 
44
+ useLocalStorageModel(itemsInternal,props)
45
+
43
46
  const computedDisableApplyToAll = computed(()=>{
44
47
  if (isString(props.disableApplyToAll)) {
45
48
  return props.disableApplyToAll.split(',').map(i=>i.trim())
@@ -2,39 +2,143 @@
2
2
  import { DateTime } from "luxon";
3
3
  import { computed } from "vue";
4
4
 
5
- type Locale = "th" | "en" | "en-US" | "th-TH";
5
+ type Unit = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds';
6
+ type Locale = 'th' | 'en' | 'en-US' | 'th-TH';
6
7
 
7
8
  interface Props {
8
9
  modelValue: DateTime;
9
10
  endDate?: DateTime;
10
11
  locale?: Locale;
11
- zeroBase?: boolean; // true = start at 0, false = start at 1
12
+ showSuffix?: boolean;
13
+ units?: Unit[];
14
+ zeroBase?: boolean; // true = start at 0, false = start at 1 (inclusive)
12
15
  }
13
16
 
14
17
  const props = withDefaults(defineProps<Props>(), {
15
- locale: "th",
18
+ locale: 'th',
19
+ showSuffix: true,
16
20
  zeroBase: true,
17
21
  });
18
22
 
19
- const normalizeLocale = (locale: Locale): "en" | "th" =>
20
- locale.startsWith("en") ? "en" : "th";
23
+ // Fallback map: map complex locale (e.g., en-US) to base (e.g., en)
24
+ const normalizeLocale = (locale: Locale): 'en' | 'th' => {
25
+ if (locale.startsWith('en')) return 'en';
26
+ if (locale.startsWith('th')) return 'th';
27
+ return 'th'; // default fallback
28
+ };
21
29
 
22
- const countDate = computed(() => {
23
- const base = props.endDate ?? DateTime.now();
30
+ const labelsEnPlural: Record<Unit, string> = {
31
+ years: 'years',
32
+ months: 'months',
33
+ days: 'days',
34
+ hours: 'hours',
35
+ minutes: 'minutes',
36
+ seconds: 'seconds',
37
+ };
38
+ const labelsEnSingular: Record<Unit, string> = {
39
+ years: 'year',
40
+ months: 'month',
41
+ days: 'day',
42
+ hours: 'hour',
43
+ minutes: 'minute',
44
+ seconds: 'second',
45
+ };
46
+
47
+ const localizedLabels: Record<'en' | 'th', Record<Unit, string>> = {
48
+ en: labelsEnPlural, // จะสลับเป็น singular ตามค่าจริงตอน render
49
+ th: {
50
+ years: 'ปี',
51
+ months: 'เดือน',
52
+ days: 'วัน',
53
+ hours: 'ชั่วโมง',
54
+ minutes: 'นาที',
55
+ seconds: 'วินาที',
56
+ },
57
+ };
58
+
59
+ const localizedSuffix: Record<'en' | 'th', string> = {
60
+ en: 'ago',
61
+ th: 'ที่ผ่านมา',
62
+ };
63
+
64
+ const outputText = computed(() => {
65
+ const base = props.endDate ?? DateTime.now(); // ใช้ endDate ถ้ามี ไม่งั้น now
24
66
  const baseLocale = normalizeLocale(props.locale);
25
67
 
26
- let days = Math.floor(base.diff(props.modelValue, "days").days ?? 0);
68
+ const units: Unit[] = props.units?.length
69
+ ? props.units
70
+ : ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
71
+
72
+ const diffObj = base.diff(props.modelValue, units).toObject();
73
+
74
+ // helper: คืน label ตาม singular/plural (เฉพาะ en)
75
+ const labelFor = (unit: Unit, value: number) => {
76
+ if (baseLocale === 'en') {
77
+ return value === 1 ? labelsEnSingular[unit] : labelsEnPlural[unit];
78
+ }
79
+ return localizedLabels[baseLocale][unit];
80
+ };
81
+
82
+ // ---------- โหมด single unit ----------
83
+ if (!props.units) {
84
+ const foundUnit = units.find((unit) => (diffObj[unit] ?? 0) >= 1);
85
+ if (foundUnit) {
86
+ const raw = Math.floor(diffObj[foundUnit] ?? 0); // >= 1
87
+ const value = props.zeroBase ? raw : raw + 1; // inclusive (+1)
88
+ const label = labelFor(foundUnit, value);
89
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
90
+ return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
91
+ }
92
+
93
+ // ถ้าไม่มีหน่วยใด >= 1 ให้ใช้หน่วยเล็กสุด
94
+ const lastUnit = units.at(-1)!;
95
+ const raw = Math.max(0, Math.floor(diffObj[lastUnit] ?? 0));
96
+ const value = props.zeroBase ? raw : 1; // inclusive: อย่างน้อย 1
97
+ const label = labelFor(lastUnit, value);
98
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
99
+ return `${value} ${label}${suffix ? ` ${suffix}` : ''}`;
100
+ }
101
+
102
+ // ---------- โหมด multi-unit ----------
103
+ // เก็บค่าแบบตัวเลขก่อน แล้วค่อยเรนเดอร์
104
+ const values = units.map((unit) => ({
105
+ unit,
106
+ raw: Math.max(0, Math.floor(diffObj[unit] ?? 0)),
107
+ }));
108
+
109
+ // เลือกหน่วยที่เล็กที่สุด
110
+ const smallest = values[values.length - 1];
111
+
112
+ // ถ้ามีค่าอย่างน้อยหนึ่งหน่วย > 0 และเป็น inclusive (zeroBase=false) => +1 ที่หน่วยเล็กสุด
113
+ const anyPositive = values.some((v) => v.raw > 0);
114
+ const adjusted = values.map((v, idx) => {
115
+ if (!props.zeroBase && anyPositive && idx === values.length - 1) {
116
+ return { ...v, raw: v.raw + 1 };
117
+ }
118
+ return v;
119
+ });
27
120
 
28
- if (!props.zeroBase) days += 1;
121
+ // สร้างข้อความจากหน่วยที่มีค่า > 0
122
+ const parts = adjusted
123
+ .filter((v) => v.raw > 0)
124
+ .map((v) => `${v.raw} ${labelFor(v.unit, v.raw)}`);
29
125
 
30
- if (baseLocale === "en") {
31
- return days === 1 ? "1 day" : `${days} days`;
126
+ // หากทั้งหมดเป็น 0:
127
+ if (parts.length === 0) {
128
+ const minValue = props.zeroBase ? 0 : 1;
129
+ const label = labelFor(smallest.unit, minValue);
130
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
131
+ return `${minValue} ${label}${suffix ? ` ${suffix}` : ''}`;
32
132
  }
33
133
 
34
- return `${days} วัน`;
134
+ const suffix = props.showSuffix ? localizedSuffix[baseLocale] : '';
135
+ return `${parts.join(' ')}${suffix ? ` ${suffix}` : ''}`;
35
136
  });
36
137
  </script>
37
138
 
38
139
  <template>
39
- <span>{{ countDate }}</span>
140
+ <span>
141
+ <i v-if="props.zeroBase" class="mdi mdi-clock-time-twelve-outline"></i>
142
+ <span v-if="props.zeroBase">&nbsp;</span>{{ outputText }}
143
+ </span>
40
144
  </template>
@@ -2,10 +2,11 @@
2
2
  import { VAutocomplete } from 'vuetify/components/VAutocomplete'
3
3
  import { defineModel, withDefaults } from 'vue'
4
4
  import { useLookupList, type LookupProps } from '../../composables/lookupList'
5
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
5
6
 
6
7
  interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$props']> {}
7
8
 
8
- const props = withDefaults(defineProps<Props & LookupProps>(), {
9
+ const props = withDefaults(defineProps<Props & LookupProps & PersistSlimProps>(), {
9
10
  fuzzy: false,
10
11
  showCode: false,
11
12
  cache: false,
@@ -22,6 +23,8 @@ const emit = defineEmits<{
22
23
 
23
24
  const selectedItems = defineModel<any>()
24
25
 
26
+ useLocalStorageModel(selectedItems,props)
27
+
25
28
  const {
26
29
  searchData,
27
30
  computedItems,
@@ -2,10 +2,11 @@
2
2
  import { VCombobox } from 'vuetify/components/VCombobox'
3
3
  import { defineModel, withDefaults } from 'vue'
4
4
  import { useLookupList, type LookupProps } from '../../composables/lookupList'
5
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
5
6
 
6
7
  interface Props extends /* @vue-ignore */ InstanceType<typeof VCombobox['$props']> {}
7
8
 
8
- const props = withDefaults(defineProps<Props & LookupProps>(), {
9
+ const props = withDefaults(defineProps<Props & LookupProps & PersistSlimProps>(), {
9
10
  fuzzy: false,
10
11
  showCode: false,
11
12
  cache: false,
@@ -22,6 +23,8 @@ const emit = defineEmits<{
22
23
 
23
24
  const selectedItems = defineModel<any>()
24
25
 
26
+ useLocalStorageModel(selectedItems,props)
27
+
25
28
  const {
26
29
  searchData,
27
30
  computedItems,
@@ -2,10 +2,11 @@
2
2
  import { VCombobox } from 'vuetify/components/VCombobox'
3
3
  import { defineModel, withDefaults } from 'vue'
4
4
  import { useLookupList, type StaticLookupProps } from '../../composables/lookupList'
5
+ import { useLocalStorageModel, type PersistSlimProps } from '../../composables/localStorageModel'
5
6
 
6
7
  interface Props extends /* @vue-ignore */ InstanceType<typeof VCombobox['$props']> {}
7
8
 
8
- const props = withDefaults(defineProps<Props & StaticLookupProps>(), {
9
+ const props = withDefaults(defineProps<Props & StaticLookupProps & PersistSlimProps>(), {
9
10
  fuzzy: false,
10
11
  showCode: false,
11
12
  cache: false,
@@ -17,6 +18,8 @@ const emit = defineEmits<{
17
18
 
18
19
  const selectedItems = defineModel<any>()
19
20
 
21
+ useLocalStorageModel(selectedItems,props)
22
+
20
23
  const {
21
24
  computedItems,
22
25
  computedFilterKeys,
@@ -15,12 +15,12 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof PDF['$props']> {
15
15
  }
16
16
 
17
17
  const props = withDefaults(defineProps<Props>(), {
18
- base64String: "",
19
- title: "",
20
- fileName: "",
18
+ base64String: '',
19
+ title: '',
20
+ fileName: '',
21
21
  disabled: false,
22
22
  isPrint: false,
23
- showBackToTopBtn: false
23
+ showBackToTopBtn: false,
24
24
  })
25
25
 
26
26
  const emit = defineEmits(['closeDialog'])
@@ -33,25 +33,34 @@ const generateUniqueId = (): string => {
33
33
  }
34
34
 
35
35
  const downloadPdf = (): void => {
36
- const byteString = atob(props.base64String || '')
37
- const byteArray = new Uint8Array(byteString.length)
36
+ try {
37
+ if (!props.base64String) alert?.addAlert({ message: 'No Base64 provided', alertType: 'error' })
38
38
 
39
- for (let i = 0; i < byteString.length; i++) {
40
- byteArray[i] = byteString.charCodeAt(i)
41
- }
39
+ const byteString = atob(props.base64String || '')
40
+ const byteArray = new Uint8Array(byteString.length)
42
41
 
43
- const blob = new Blob([byteArray], { type: 'application/pdf' })
44
- const link = URL.createObjectURL(blob)
45
- const anchorElement = document.createElement('a')
46
- anchorElement.style.display = 'none'
47
- anchorElement.href = link
48
- anchorElement.download = `${generateUniqueId()}.pdf`
49
-
50
- document.body.appendChild(anchorElement)
51
- anchorElement.click()
52
- URL.revokeObjectURL(link)
53
- document.body.removeChild(anchorElement)
54
- base64.value = ''
42
+ for (let i = 0; i < byteString.length; i++) {
43
+ byteArray[i] = byteString.charCodeAt(i)
44
+ }
45
+
46
+ const blob = new Blob([byteArray], { type: 'application/pdf' })
47
+ const link = URL.createObjectURL(blob)
48
+ const anchorElement = document.createElement('a')
49
+ anchorElement.style.display = 'none'
50
+ anchorElement.href = link
51
+ anchorElement.download = `${generateUniqueId()}.pdf`
52
+
53
+ document.body.appendChild(anchorElement)
54
+ anchorElement.click()
55
+ URL.revokeObjectURL(link)
56
+ document.body.removeChild(anchorElement)
57
+ base64.value = ''
58
+
59
+ alert?.addAlert({ message: 'Download success', alertType: 'success' })
60
+ }
61
+ catch (error) {
62
+ alert?.addAlert({ message: `Download unsuccess : ${error}`, alertType: 'error' })
63
+ }
55
64
  }
56
65
 
57
66
  const printPdf = () => {
@@ -60,7 +69,7 @@ const printPdf = () => {
60
69
  type: 'pdf',
61
70
  base64: true,
62
71
  onPrintDialogClose: endLoadPdf,
63
- onError: (error: any) => {
72
+ onError: (error) => {
64
73
  alert?.addAlert({ message: error, alertType: 'error' })
65
74
  },
66
75
  })
@@ -73,18 +82,19 @@ const endLoadPdf = () => {
73
82
  }
74
83
 
75
84
  const isMobile = () => {
76
- return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent);
85
+ return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent)
77
86
  }
78
87
 
79
88
  const checkMobileAndPrint = computed(() => {
80
- return !isMobile() && props.isPrint;
81
- });
89
+ return !isMobile() && props.isPrint
90
+ })
82
91
 
83
92
  const setWidthPdf = computed(() => {
84
93
  if (isMobile()) {
85
- return "100%"
86
- } else {
87
- return "100dvh"
94
+ return '100%'
95
+ }
96
+ else {
97
+ return '100dvh'
88
98
  }
89
99
  })
90
100
  </script>
@@ -114,10 +124,10 @@ const setWidthPdf = computed(() => {
114
124
  </v-toolbar>
115
125
  <v-card-text class="justify-center h-screen">
116
126
  <PDF
117
- v-bind="$attrs"
118
- :pdf-width="setWidthPdf"
119
- :src="base64"
120
- :show-back-to-top-btn="props.showBackToTopBtn"
127
+ v-bind="$attrs"
128
+ :pdf-width="setWidthPdf"
129
+ :src="base64"
130
+ :show-back-to-top-btn="props.showBackToTopBtn"
121
131
  />
122
132
  </v-card-text>
123
133
  </v-card>
@@ -2,9 +2,9 @@ import type { UseFetchOptions } from 'nuxt/app';
2
2
  import type { SearchParameters } from 'ofetch';
3
3
  export declare function useApi(): {
4
4
  urlBuilder: (url: string | string[]) => string;
5
- get: (url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<unknown>;
6
- getPromise: (url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<unknown>;
7
- post: (url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<unknown>;
8
- postPromise: (url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<unknown>;
5
+ get: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<T>;
6
+ getPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<T>;
7
+ post: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<T>;
8
+ postPromise: <T>(url: string | string[], body?: Record<string, any> | [], params?: SearchParameters, options?: UseFetchOptions<unknown>, cache?: boolean | number) => Promise<T>;
9
9
  hashKey: (data: any) => Promise<string>;
10
10
  };
@@ -1,29 +1,33 @@
1
1
  import { ofetch } from "ofetch";
2
2
  import { trimEnd, trimStart } from "lodash-es";
3
- import ls from "localstorage-ttl";
3
+ import ls from "localstorage-slim";
4
4
  import { stableStringify } from "../utils/object.js";
5
5
  import { sha256 } from "../utils/hash.js";
6
6
  import { useAuthentication, useRuntimeConfig } from "#imports";
7
7
  export function useApi() {
8
8
  const config = useRuntimeConfig();
9
+ const CACHE_PREFIX = "api-cache-";
9
10
  function urlBuilder(url) {
10
11
  let returnUrl;
11
12
  if (Array.isArray(url)) {
12
- if (url[0].toLowerCase() == "agent") url[0] = config?.public.WS_AGENT || config?.public.WS_DEVICE;
13
+ if (url[0].toLowerCase() === "agent") {
14
+ url[0] = config?.public.WS_AGENT || config?.public.WS_DEVICE;
15
+ }
13
16
  returnUrl = url.join("/");
14
17
  } else {
15
18
  returnUrl = url;
16
19
  }
17
20
  if (returnUrl.startsWith("http://") || returnUrl.startsWith("https://")) return returnUrl;
18
- else return trimEnd(config?.public.WS_API, "/") + "/" + trimStart(returnUrl, "/");
21
+ return trimEnd(config?.public.WS_API, "/") + "/" + trimStart(returnUrl, "/");
19
22
  }
20
23
  function optionBuilder(method, body, params, options = {}) {
21
24
  const headers = {
22
25
  "Content-Type": "application/json",
23
- "Accept": "application/json"
26
+ Accept: "application/json"
24
27
  };
25
28
  const { token } = useAuthentication().keycloak || {};
26
29
  if (token) {
30
+ ;
27
31
  headers["Authorization"] = `Bearer ${token}`;
28
32
  }
29
33
  const baseOptions = {
@@ -58,27 +62,35 @@ export function useApi() {
58
62
  const jsonString = stableStringify(data);
59
63
  return sha256(jsonString);
60
64
  }
61
- async function ofetchWithCache(url, method, body, params, options, cache) {
62
- const keyData = { url: urlBuilder(url), method, body, params, options };
63
- let ttl = 0;
64
- if (cache) {
65
- if (typeof cache === "boolean") ttl = 5 * 60 * 1e3;
66
- else {
67
- ttl = +cache * 60 * 1e3;
68
- }
65
+ function computeTtlSecond(cache) {
66
+ if (!cache) return 0;
67
+ if (typeof cache === "boolean") return 5 * 60;
68
+ let ttlNum = Math.max(0, Number(cache));
69
+ return ttlNum > 60 ? ttlNum : ttlNum * 60;
70
+ }
71
+ function safeGetCache(key) {
72
+ try {
73
+ const v = ls.get(key);
74
+ return v ?? null;
75
+ } catch {
76
+ return null;
69
77
  }
78
+ }
79
+ async function ofetchWithCache(url, method, body, params, options, cache) {
80
+ const builtUrl = urlBuilder(url);
81
+ const ttl = computeTtlSecond(cache);
70
82
  if (ttl === 0) {
71
- return ofetch(urlBuilder(url), optionBuilder(method, body, params, options));
83
+ return ofetch(builtUrl, optionBuilder(method, body, params, options));
72
84
  }
73
- const key = "api-cache-" + await hashKey(keyData);
74
- const cached = ls.get(key);
75
- if (cached) {
76
- return Promise.resolve(cached);
85
+ const keyData = { url: builtUrl, method, body, params, headers: options?.headers };
86
+ const key = CACHE_PREFIX + await hashKey(keyData);
87
+ const cached = safeGetCache(key);
88
+ if (cached !== null) {
89
+ return cached;
77
90
  }
78
- return ofetch(urlBuilder(url), optionBuilder(method, body, params, options)).then((result) => {
79
- ls.set(key, result, ttl);
80
- return result;
81
- });
91
+ const result = await ofetch(builtUrl, optionBuilder(method, body, params, options));
92
+ ls.set(key, result, { ttl });
93
+ return result;
82
94
  }
83
95
  return { urlBuilder, get, getPromise, post, postPromise, hashKey };
84
96
  }
@@ -0,0 +1,15 @@
1
+ /** JSON-serializable config object (backend: Map<String, Object>) */
2
+ export type ClientConfig = Record<string, any>;
3
+ export interface ClientConfigOptions {
4
+ storageKey?: string;
5
+ }
6
+ export interface ClientConfigService {
7
+ get(): ClientConfig | null;
8
+ set(cfg: ClientConfig): void;
9
+ clear(): void;
10
+ /** GraphQL-first, localStorage fallback */
11
+ load(): Promise<ClientConfig | null>;
12
+ /** BLANK method stub: implement GraphQL retrieval */
13
+ fetchFromGraphql(): Promise<ClientConfig>;
14
+ }
15
+ export declare function createClientConfigService(opts?: ClientConfigOptions): ClientConfigService;