@softdin/shared 0.1.3
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.
- package/README.md +48 -0
- package/package.json +50 -0
- package/src/a11y/announcer.ts +43 -0
- package/src/a11y/apply.ts +83 -0
- package/src/a11y/index.ts +31 -0
- package/src/a11y/state.ts +40 -0
- package/src/a11y/storage.ts +106 -0
- package/src/a11y/types.ts +187 -0
- package/src/api.ts +89 -0
- package/src/assets/softdin-logo.png +0 -0
- package/src/auth.ts +134 -0
- package/src/components/a11y/A11yLiveRegion.vue +11 -0
- package/src/components/a11y/AccessibilityMenuControls.vue +426 -0
- package/src/components/a11y/AccessibilityPreferencesPanel.vue +331 -0
- package/src/components/a11y/ColorSchemeQuickToggle.vue +105 -0
- package/src/components/a11y/SkipLink.vue +41 -0
- package/src/components/auth/AuthBrandHeader.vue +129 -0
- package/src/components/auth/AuthShell.vue +52 -0
- package/src/components/auth/DataProcessingConsentCheckbox.vue +303 -0
- package/src/components/auth/DataProcessingConsentForm.vue +111 -0
- package/src/components/forms/ErrorAlert.vue +170 -0
- package/src/components/forms/FormCancelButton.vue +87 -0
- package/src/components/forms/FormCloseButton.vue +114 -0
- package/src/components/forms/FormHelpButton.vue +109 -0
- package/src/components/forms/FormHelpModal.vue +193 -0
- package/src/components/forms/FormMaximizeButton.vue +143 -0
- package/src/components/forms/FormRefreshButton.vue +152 -0
- package/src/components/forms/FormSaveButton.vue +113 -0
- package/src/components/forms/PasswordInput.vue +154 -0
- package/src/components/navigation/CatalogListPagination.vue +113 -0
- package/src/config/apiBase.ts +37 -0
- package/src/lib/catalog-pagination.ts +27 -0
- package/src/lib/pagination.ts +86 -0
- package/src/lib/softdin-libreria.ts +50 -0
- package/src/style.css +278 -0
- package/src/types/auth.ts +146 -0
- package/src/types/softdin-libreria.d.ts +41 -0
- package/src/utils/apiError.ts +55 -0
- package/src/utils/connectivity.ts +33 -0
- package/src/utils/tenantLogo.ts +6 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
loading?: boolean
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
label?: string
|
|
7
|
+
loadingLabel?: string
|
|
8
|
+
}>(),
|
|
9
|
+
{
|
|
10
|
+
loading: false,
|
|
11
|
+
disabled: false,
|
|
12
|
+
label: 'Guardar',
|
|
13
|
+
loadingLabel: 'Guardando…',
|
|
14
|
+
},
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button
|
|
20
|
+
type="submit"
|
|
21
|
+
class="softdin-form-save"
|
|
22
|
+
:aria-label="loading ? loadingLabel : label"
|
|
23
|
+
:title="loading ? loadingLabel : label"
|
|
24
|
+
:disabled="disabled || loading"
|
|
25
|
+
>
|
|
26
|
+
<svg
|
|
27
|
+
v-if="loading"
|
|
28
|
+
class="softdin-form-save__icon softdin-form-save__icon--loading"
|
|
29
|
+
viewBox="0 0 20 20"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
>
|
|
32
|
+
<circle cx="10" cy="10" r="7" />
|
|
33
|
+
<path d="M10 6v4l2.5 1.5" />
|
|
34
|
+
</svg>
|
|
35
|
+
<svg v-else class="softdin-form-save__icon" viewBox="0 0 20 20" aria-hidden="true">
|
|
36
|
+
<path d="M4 3h10l2 2v12H4z" />
|
|
37
|
+
<path d="M7 3v5h6V3M7 16v-5h6v5" />
|
|
38
|
+
</svg>
|
|
39
|
+
<span>{{ loading ? loadingLabel : label }}</span>
|
|
40
|
+
</button>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.softdin-form-save {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
gap: 0.5rem;
|
|
49
|
+
min-height: 2.25rem;
|
|
50
|
+
border: 0;
|
|
51
|
+
border-radius: 9999px;
|
|
52
|
+
background: var(--color-brand-600);
|
|
53
|
+
padding: 0.5rem 1rem;
|
|
54
|
+
font-size: 0.875rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
color: #fff;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.softdin-form-save:hover:not(:disabled),
|
|
62
|
+
.softdin-form-save:focus-visible {
|
|
63
|
+
background: var(--color-brand-700);
|
|
64
|
+
box-shadow: 0 6px 14px color-mix(in srgb, var(--color-brand-700) 28%, transparent);
|
|
65
|
+
transform: translateY(-2px);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.softdin-form-save:focus-visible {
|
|
69
|
+
outline: 2px solid var(--color-brand-600);
|
|
70
|
+
outline-offset: 2px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.softdin-form-save:disabled {
|
|
74
|
+
opacity: 0.55;
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.softdin-form-save__icon {
|
|
79
|
+
width: 1rem;
|
|
80
|
+
height: 1rem;
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
fill: none;
|
|
83
|
+
stroke: currentColor;
|
|
84
|
+
stroke-width: 1.8;
|
|
85
|
+
stroke-linecap: round;
|
|
86
|
+
stroke-linejoin: round;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.softdin-form-save__icon--loading {
|
|
90
|
+
animation: softdin-form-save-spin 1s linear infinite;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@keyframes softdin-form-save-spin {
|
|
94
|
+
to {
|
|
95
|
+
transform: rotate(360deg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (prefers-reduced-motion: reduce) {
|
|
100
|
+
.softdin-form-save {
|
|
101
|
+
transition: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.softdin-form-save:hover:not(:disabled),
|
|
105
|
+
.softdin-form-save:focus-visible {
|
|
106
|
+
transform: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.softdin-form-save__icon--loading {
|
|
110
|
+
animation: none;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
id?: string
|
|
7
|
+
modelValue: string
|
|
8
|
+
required?: boolean
|
|
9
|
+
autocomplete?: string
|
|
10
|
+
minlength?: number | string
|
|
11
|
+
readonly?: boolean
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
required: false,
|
|
16
|
+
readonly: false,
|
|
17
|
+
disabled: false,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
'update:modelValue': [value: string]
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const visible = ref(false)
|
|
26
|
+
|
|
27
|
+
const inputType = computed(() => (visible.value ? 'text' : 'password'))
|
|
28
|
+
|
|
29
|
+
const toggleLabel = computed(() =>
|
|
30
|
+
visible.value ? 'Ocultar contraseña' : 'Mostrar contraseña',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
function onInput(event: Event): void {
|
|
34
|
+
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="password-input">
|
|
40
|
+
<input
|
|
41
|
+
:id="id"
|
|
42
|
+
:type="inputType"
|
|
43
|
+
:value="modelValue"
|
|
44
|
+
:required="required"
|
|
45
|
+
:autocomplete="autocomplete"
|
|
46
|
+
:minlength="minlength"
|
|
47
|
+
:readonly="readonly"
|
|
48
|
+
:disabled="disabled"
|
|
49
|
+
class="password-input__field"
|
|
50
|
+
@input="onInput"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="password-input__toggle"
|
|
56
|
+
:aria-label="toggleLabel"
|
|
57
|
+
:title="toggleLabel"
|
|
58
|
+
:aria-pressed="visible"
|
|
59
|
+
:disabled="disabled"
|
|
60
|
+
@click="visible = !visible"
|
|
61
|
+
>
|
|
62
|
+
<svg
|
|
63
|
+
v-if="visible"
|
|
64
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
fill="none"
|
|
67
|
+
stroke="currentColor"
|
|
68
|
+
stroke-width="1.75"
|
|
69
|
+
stroke-linecap="round"
|
|
70
|
+
stroke-linejoin="round"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
>
|
|
73
|
+
<path d="M3 3l18 18" />
|
|
74
|
+
<path d="M10.58 10.58a2 2 0 0 0 2.84 2.84" />
|
|
75
|
+
<path d="M9.88 4.24A10.94 10.94 0 0 1 12 4c5 0 9.27 3.11 10.5 7.5a11.6 11.6 0 0 1-2.08 3.58" />
|
|
76
|
+
<path d="M6.09 6.09A10.94 10.94 0 0 0 2.5 12c1.23 4.39 5.5 7.5 10.5 7.5a10.9 10.9 0 0 0 4.24-.88" />
|
|
77
|
+
</svg>
|
|
78
|
+
|
|
79
|
+
<svg
|
|
80
|
+
v-else
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
fill="none"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
stroke-width="1.75"
|
|
86
|
+
stroke-linecap="round"
|
|
87
|
+
stroke-linejoin="round"
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
>
|
|
90
|
+
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
|
91
|
+
<circle cx="12" cy="12" r="3" />
|
|
92
|
+
</svg>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
.password-input {
|
|
99
|
+
position: relative;
|
|
100
|
+
width: 100%;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.password-input__field {
|
|
104
|
+
width: 100%;
|
|
105
|
+
border-radius: 0.5rem;
|
|
106
|
+
border: 1px solid #cbd5e1;
|
|
107
|
+
padding: 0.5rem 2.5rem 0.5rem 0.75rem;
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
outline: none;
|
|
110
|
+
background: #fff;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.password-input__field:focus {
|
|
114
|
+
border-color: var(--color-brand-500);
|
|
115
|
+
box-shadow: 0 0 0 3px var(--color-brand-100);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.password-input__field:disabled {
|
|
119
|
+
background: #f1f5f9;
|
|
120
|
+
color: #64748b;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.password-input__toggle {
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 50%;
|
|
126
|
+
right: 0.5rem;
|
|
127
|
+
display: flex;
|
|
128
|
+
height: 1.75rem;
|
|
129
|
+
width: 1.75rem;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
transform: translateY(-50%);
|
|
133
|
+
border: none;
|
|
134
|
+
border-radius: 0.375rem;
|
|
135
|
+
background: transparent;
|
|
136
|
+
color: #64748b;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.password-input__toggle:hover:not(:disabled) {
|
|
141
|
+
color: #334155;
|
|
142
|
+
background: #f1f5f9;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.password-input__toggle:disabled {
|
|
146
|
+
cursor: not-allowed;
|
|
147
|
+
opacity: 0.5;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.password-input__toggle svg {
|
|
151
|
+
height: 1.125rem;
|
|
152
|
+
width: 1.125rem;
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
currentPage: number
|
|
4
|
+
lastPage: number
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
}>(), {
|
|
7
|
+
disabled: false,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
go: [page: number]
|
|
12
|
+
}>()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<footer v-if="lastPage > 1" class="pagination">
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class="page-btn"
|
|
20
|
+
:disabled="disabled || currentPage <= 1"
|
|
21
|
+
title="Inicio"
|
|
22
|
+
aria-label="Inicio"
|
|
23
|
+
@click="emit('go', 1)"
|
|
24
|
+
>
|
|
25
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" class="page-icon">
|
|
26
|
+
<path fill="currentColor" d="M18.41 16.59 13.82 12l4.59-4.59L17 6l-6 6 6 6zM6 6h2v12H6z" />
|
|
27
|
+
</svg>
|
|
28
|
+
</button>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
class="page-btn"
|
|
32
|
+
:disabled="disabled || currentPage <= 1"
|
|
33
|
+
title="Anterior"
|
|
34
|
+
aria-label="Anterior"
|
|
35
|
+
@click="emit('go', currentPage - 1)"
|
|
36
|
+
>
|
|
37
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" class="page-icon">
|
|
38
|
+
<path fill="currentColor" d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
|
39
|
+
</svg>
|
|
40
|
+
</button>
|
|
41
|
+
<span class="pagination-label">Página {{ currentPage }} de {{ lastPage }}</span>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="page-btn"
|
|
45
|
+
:disabled="disabled || currentPage >= lastPage"
|
|
46
|
+
title="Siguiente"
|
|
47
|
+
aria-label="Siguiente"
|
|
48
|
+
@click="emit('go', currentPage + 1)"
|
|
49
|
+
>
|
|
50
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" class="page-icon">
|
|
51
|
+
<path fill="currentColor" d="M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="page-btn"
|
|
57
|
+
:disabled="disabled || currentPage >= lastPage"
|
|
58
|
+
title="Final"
|
|
59
|
+
aria-label="Final"
|
|
60
|
+
@click="emit('go', lastPage)"
|
|
61
|
+
>
|
|
62
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" class="page-icon">
|
|
63
|
+
<path fill="currentColor" d="m5.59 7.41 1.41-1.41 6 6-6 6-1.41-1.41L10.17 12zM16 6h2v12h-2z" />
|
|
64
|
+
</svg>
|
|
65
|
+
</button>
|
|
66
|
+
</footer>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<style scoped>
|
|
70
|
+
.pagination {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
gap: 0.375rem;
|
|
75
|
+
border-top: 1px solid #e2e8f0;
|
|
76
|
+
padding: 0.875rem 1.25rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.pagination-label {
|
|
80
|
+
min-width: 7.5rem;
|
|
81
|
+
text-align: center;
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
color: #64748b;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.page-btn {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
min-width: 2rem;
|
|
91
|
+
height: 2rem;
|
|
92
|
+
border: 1px solid #cbd5e1;
|
|
93
|
+
border-radius: 0.375rem;
|
|
94
|
+
background: #fff;
|
|
95
|
+
color: #334155;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.page-btn:hover:not(:disabled) {
|
|
100
|
+
background: #f8fafc;
|
|
101
|
+
border-color: #94a3b8;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.page-btn:disabled {
|
|
105
|
+
opacity: 0.45;
|
|
106
|
+
cursor: not-allowed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.page-icon {
|
|
110
|
+
width: 1.125rem;
|
|
111
|
+
height: 1.125rem;
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL base de la API. Debe coincidir con APP_URL del servidor + /api/v1
|
|
3
|
+
* (variable VITE_API_URL en softdin/.env o .env.production).
|
|
4
|
+
*/
|
|
5
|
+
const API_VERSION_SUFFIX = '/api/v1'
|
|
6
|
+
|
|
7
|
+
function hasApiVersionPath(url: string): boolean {
|
|
8
|
+
return /\/api\/v\d+\/?$/i.test(url)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveApiBaseUrl(): string {
|
|
12
|
+
const configured = import.meta.env.VITE_API_URL?.trim()
|
|
13
|
+
|
|
14
|
+
if (configured) {
|
|
15
|
+
const normalized = configured.replace(/\/+$/, '')
|
|
16
|
+
if (hasApiVersionPath(normalized)) {
|
|
17
|
+
return normalized
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return `${normalized}${API_VERSION_SUFFIX}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `http://localhost:8000${API_VERSION_SUFFIX}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** true si VITE_API_URL no incluía /api/v1 y se normalizó automáticamente */
|
|
27
|
+
export function wasApiBaseUrlNormalized(): boolean {
|
|
28
|
+
const configured = import.meta.env.VITE_API_URL?.trim()
|
|
29
|
+
if (!configured) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return !hasApiVersionPath(configured.replace(/\/+$/, ''))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const API_BASE_URL = resolveApiBaseUrl()
|
|
37
|
+
export const VITE_API_URL_RAW = import.meta.env.VITE_API_URL?.trim() ?? ''
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const CATALOG_PAGE_SIZE = 50
|
|
2
|
+
|
|
3
|
+
export type CatalogPageResult<T> = {
|
|
4
|
+
pageItems: T[]
|
|
5
|
+
currentPage: number
|
|
6
|
+
lastPage: number
|
|
7
|
+
total: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parte un arreglo en páginas (paginación en memoria). */
|
|
11
|
+
export function paginateCatalogItems<T>(
|
|
12
|
+
items: readonly T[],
|
|
13
|
+
page: number,
|
|
14
|
+
pageSize: number = CATALOG_PAGE_SIZE,
|
|
15
|
+
): CatalogPageResult<T> {
|
|
16
|
+
const total = items.length
|
|
17
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize) || 1)
|
|
18
|
+
const currentPage = Math.min(Math.max(1, page), lastPage)
|
|
19
|
+
const start = (currentPage - 1) * pageSize
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
pageItems: items.slice(start, start + pageSize) as T[],
|
|
23
|
+
currentPage,
|
|
24
|
+
lastPage,
|
|
25
|
+
total,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export interface PaginationMeta {
|
|
2
|
+
current_page: number
|
|
3
|
+
last_page: number
|
|
4
|
+
per_page: number
|
|
5
|
+
total: number
|
|
6
|
+
from: number | null
|
|
7
|
+
to: number | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Paginated<T> {
|
|
11
|
+
data: T[]
|
|
12
|
+
meta: PaginationMeta
|
|
13
|
+
links?: Record<string, string | null>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EMPTY_META: PaginationMeta = {
|
|
17
|
+
current_page: 1,
|
|
18
|
+
last_page: 1,
|
|
19
|
+
per_page: 50,
|
|
20
|
+
total: 0,
|
|
21
|
+
from: null,
|
|
22
|
+
to: null,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeMeta(source: unknown): PaginationMeta {
|
|
26
|
+
if (!source || typeof source !== 'object') {
|
|
27
|
+
return { ...EMPTY_META }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const meta = source as Record<string, unknown>
|
|
31
|
+
if (typeof meta.current_page !== 'number') {
|
|
32
|
+
return { ...EMPTY_META }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
current_page: meta.current_page,
|
|
37
|
+
last_page: typeof meta.last_page === 'number' ? meta.last_page : 1,
|
|
38
|
+
per_page: typeof meta.per_page === 'number' ? meta.per_page : 50,
|
|
39
|
+
total: typeof meta.total === 'number' ? meta.total : 0,
|
|
40
|
+
from: typeof meta.from === 'number' ? meta.from : null,
|
|
41
|
+
to: typeof meta.to === 'number' ? meta.to : null,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizePaginated<T>(payload: unknown): Paginated<T> {
|
|
46
|
+
if (!payload || typeof payload !== 'object') {
|
|
47
|
+
return { data: [], meta: { ...EMPTY_META } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body = payload as Record<string, unknown>
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(body.data)) {
|
|
53
|
+
return {
|
|
54
|
+
data: body.data as T[],
|
|
55
|
+
meta: normalizeMeta(body.meta ?? body),
|
|
56
|
+
links: body.links as Paginated<T>['links'],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const nested = body.data
|
|
61
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
62
|
+
const inner = nested as Record<string, unknown>
|
|
63
|
+
if (Array.isArray(inner.data)) {
|
|
64
|
+
return {
|
|
65
|
+
data: inner.data as T[],
|
|
66
|
+
meta: normalizeMeta(inner.meta ?? inner),
|
|
67
|
+
links: inner.links as Paginated<T>['links'],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { data: [], meta: { ...EMPTY_META } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeResource<T>(payload: unknown): T {
|
|
76
|
+
if (!payload || typeof payload !== 'object') {
|
|
77
|
+
throw new Error('Respuesta inválida del servidor.')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const body = payload as Record<string, unknown>
|
|
81
|
+
if ('data' in body && body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
82
|
+
return body.data as T
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return body as T
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Punto único de acceso a softdinlibreriajs (npm).
|
|
3
|
+
* El resto de la app no debe importar la librería directamente.
|
|
4
|
+
*/
|
|
5
|
+
import EnumContinente from 'softdinlibreriajs/src/EnumContinente.js'
|
|
6
|
+
import EnumTipoContrato from 'softdinlibreriajs/src/EnumTipoContrato.js'
|
|
7
|
+
|
|
8
|
+
export type ContinenteEnumItem = {
|
|
9
|
+
id: number
|
|
10
|
+
code: string
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TipoContratoEnumItem = {
|
|
15
|
+
id: number
|
|
16
|
+
code: string
|
|
17
|
+
description: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getContinentesEnum(): ContinenteEnumItem[] {
|
|
21
|
+
return EnumContinente.getAll()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getContinenteDescriptions(): string[] {
|
|
25
|
+
return EnumContinente.getAll().map((item) => item.description)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getContinenteByDescription(description: string): ContinenteEnumItem | null {
|
|
29
|
+
return EnumContinente.getByDescription(description)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getContinenteById(id: number): ContinenteEnumItem | null {
|
|
33
|
+
return EnumContinente.getById(id)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getTiposContratoEnum(): TipoContratoEnumItem[] {
|
|
37
|
+
return EnumTipoContrato.getAll()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getTipoContratoById(id: number): TipoContratoEnumItem | null {
|
|
41
|
+
return EnumTipoContrato.getById(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { EnumContinente, EnumTipoContrato }
|
|
45
|
+
|
|
46
|
+
/** ID por defecto: América del Sur (Colombia). */
|
|
47
|
+
export const DEFAULT_CONTINENTE_ID = EnumContinente.AMERICA_SUR
|
|
48
|
+
|
|
49
|
+
/** Tipo de contrato por defecto: Fijo. */
|
|
50
|
+
export const DEFAULT_TIPO_CONTRATO_ID = EnumTipoContrato.FIJO
|