Compare commits
5 Commits
d6d91aeaf9
...
29e4497ff1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29e4497ff1 | ||
| 6d3adcc8e5 | |||
|
|
47cc7cdb8e | ||
|
|
1189b7b02e | ||
|
|
93a2527e60 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -30,3 +30,4 @@ dist-ssr
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
.agents
|
||||||
4
components.d.ts
vendored
4
components.d.ts
vendored
@ -25,19 +25,19 @@ declare module 'vue' {
|
|||||||
IconField: typeof import('primevue/iconfield')['default']
|
IconField: typeof import('primevue/iconfield')['default']
|
||||||
InputIcon: typeof import('primevue/inputicon')['default']
|
InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
InputNumber: typeof import('primevue/inputnumber')['default']
|
InputNumber: typeof import('primevue/inputnumber')['default']
|
||||||
|
InputSwitch: typeof import('primevue/inputswitch')['default']
|
||||||
InputText: typeof import('primevue/inputtext')['default']
|
InputText: typeof import('primevue/inputtext')['default']
|
||||||
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
|
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
|
||||||
Menu: typeof import('primevue/menu')['default']
|
Menu: typeof import('primevue/menu')['default']
|
||||||
Message: typeof import('primevue/message')['default']
|
Message: typeof import('primevue/message')['default']
|
||||||
Paginator: typeof import('primevue/paginator')['default']
|
Paginator: typeof import('primevue/paginator')['default']
|
||||||
Panel: typeof import('primevue/panel')['default']
|
Password: typeof import('primevue/password')['default']
|
||||||
ProgressSpinner: typeof import('primevue/progressspinner')['default']
|
ProgressSpinner: typeof import('primevue/progressspinner')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
|
||||||
Tag: typeof import('primevue/tag')['default']
|
Tag: typeof import('primevue/tag')['default']
|
||||||
Toast: typeof import('primevue/toast')['default']
|
Toast: typeof import('primevue/toast')['default']
|
||||||
Toolbar: typeof import('primevue/toolbar')['default']
|
|
||||||
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
|
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
|
|||||||
56
package-lock.json
generated
56
package-lock.json
generated
@ -27,7 +27,7 @@
|
|||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vue-tsc": "^3.1.0"
|
"vue-tsc": "^3.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
@ -1216,30 +1216,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.28",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
"integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
|
"integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/source-map": "2.4.23"
|
"@volar/source-map": "2.4.28"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/source-map": {
|
"node_modules/@volar/source-map": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.28",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
|
||||||
"integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
|
"integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@volar/typescript": {
|
"node_modules/@volar/typescript": {
|
||||||
"version": "2.4.23",
|
"version": "2.4.28",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
|
||||||
"integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
|
"integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.4.23",
|
"@volar/language-core": "2.4.28",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"vscode-uri": "^3.0.8"
|
"vscode-uri": "^3.0.8"
|
||||||
}
|
}
|
||||||
@ -1325,27 +1325,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
|
||||||
"integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==",
|
"integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.4.23",
|
"@volar/language-core": "2.4.28",
|
||||||
"@vue/compiler-dom": "^3.5.0",
|
"@vue/compiler-dom": "^3.5.0",
|
||||||
"@vue/shared": "^3.5.0",
|
"@vue/shared": "^3.5.0",
|
||||||
"alien-signals": "^3.0.0",
|
"alien-signals": "^3.0.0",
|
||||||
"muggle-string": "^0.4.1",
|
"muggle-string": "^0.4.1",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"picomatch": "^4.0.2"
|
"picomatch": "^4.0.2"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
@ -1468,9 +1460,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "3.0.6",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
|
||||||
"integrity": "sha512-gCs0YqC1mkYGC6IRXsSrA62ShOSv1FlVN5tRp/Cs2vRWLK/BAeluWIdfsl253pFQPznKEvRmHhfep7crWfyfWQ==",
|
"integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@ -2802,14 +2794,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
|
||||||
"integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==",
|
"integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/typescript": "2.4.23",
|
"@volar/typescript": "2.4.28",
|
||||||
"@vue/language-core": "3.1.3"
|
"@vue/language-core": "3.2.6"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-tsc": "bin/vue-tsc.js"
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
|||||||
@ -28,6 +28,6 @@
|
|||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7",
|
||||||
"vue-tsc": "^3.1.0"
|
"vue-tsc": "^3.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useAuth } from '../../modules/auth/composables/useAuth';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
items?: MenuItem[];
|
items?: MenuItem[];
|
||||||
|
permission?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems = ref<MenuItem[]>([
|
const menuItems = ref<MenuItem[]>([
|
||||||
@ -22,11 +25,33 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
label: 'Catálogo',
|
label: 'Catálogo',
|
||||||
icon: 'pi pi-book',
|
icon: 'pi pi-book',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Unidades de Medida', icon: 'pi pi-calculator', to: '/catalog/units-of-measure' },
|
{
|
||||||
|
label: 'Unidades de Medida',
|
||||||
|
icon: 'pi pi-calculator',
|
||||||
|
to: '/catalog/units-of-measure',
|
||||||
|
permission: [
|
||||||
|
'units-of-measure.index',
|
||||||
|
'units-of-measure.show',
|
||||||
|
'units-of-measure.store',
|
||||||
|
'units-of-measure.update',
|
||||||
|
'units-of-measure.destroy',
|
||||||
|
],
|
||||||
|
},
|
||||||
{ label: 'Clasificaciones Comerciales', icon: 'pi pi-tags', to: '/catalog/classifications-comercial' },
|
{ label: 'Clasificaciones Comerciales', icon: 'pi pi-tags', to: '/catalog/classifications-comercial' },
|
||||||
{ label: 'Proveedores', icon: 'pi pi-briefcase', to: '/catalog/suppliers' },
|
{ label: 'Proveedores', icon: 'pi pi-briefcase', to: '/catalog/suppliers' },
|
||||||
{ label: 'Documentos del Modelo', icon: 'pi pi-file', to: '/catalog/model-documents' },
|
{ label: 'Documentos del Modelo', icon: 'pi pi-file', to: '/catalog/model-documents' },
|
||||||
{ label: 'Empresas', icon: 'pi pi-building', to: '/catalog/companies' }
|
{
|
||||||
|
label: 'Empresas',
|
||||||
|
icon: 'pi pi-building',
|
||||||
|
to: '/catalog/companies',
|
||||||
|
permission: [
|
||||||
|
'companies.index',
|
||||||
|
'companies.show',
|
||||||
|
'companies.store',
|
||||||
|
'companies.update',
|
||||||
|
'companies.destroy',
|
||||||
|
],
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -99,6 +124,36 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
const sidebarVisible = ref(true);
|
const sidebarVisible = ref(true);
|
||||||
const openItems = ref<string[]>([]);
|
const openItems = ref<string[]>([]);
|
||||||
|
|
||||||
|
const canAccessItem = (item: MenuItem): boolean => {
|
||||||
|
if (!item.permission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasPermission(item.permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterMenuItems = (items: MenuItem[]): MenuItem[] => {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if (item.items && item.items.length > 0) {
|
||||||
|
const children = filterMenuItems(item.items);
|
||||||
|
if (children.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
items: children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return canAccessItem(item) ? item : null;
|
||||||
|
})
|
||||||
|
.filter((item): item is MenuItem => item !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleMenuItems = computed(() => filterMenuItems(menuItems.value));
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
sidebarVisible.value = !sidebarVisible.value;
|
sidebarVisible.value = !sidebarVisible.value;
|
||||||
};
|
};
|
||||||
@ -134,7 +189,7 @@ const isRouteActive = (to: string | undefined) => {
|
|||||||
// SOLO marcar activo si la ruta hija NO está explícitamente en el menú
|
// SOLO marcar activo si la ruta hija NO está explícitamente en el menú
|
||||||
if (route.path.startsWith(to + '/')) {
|
if (route.path.startsWith(to + '/')) {
|
||||||
// Verificar si la ruta actual está definida como un item del menú
|
// Verificar si la ruta actual está definida como un item del menú
|
||||||
const isExplicitRoute = menuItems.value.some(item => {
|
const isExplicitRoute = visibleMenuItems.value.some(item => {
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
return item.items.some(subItem => subItem.to === route.path);
|
return item.items.some(subItem => subItem.to === route.path);
|
||||||
}
|
}
|
||||||
@ -197,8 +252,15 @@ defineExpose({ toggleSidebar });
|
|||||||
|
|
||||||
<!-- Navigation Menu -->
|
<!-- Navigation Menu -->
|
||||||
<nav class="flex-1 overflow-y-auto p-3">
|
<nav class="flex-1 overflow-y-auto p-3">
|
||||||
|
<div v-if="visibleMenuItems.length === 0" class="px-3 py-6 text-center">
|
||||||
|
<i class="pi pi-lock text-2xl text-surface-400 dark:text-surface-500"></i>
|
||||||
|
<p v-if="sidebarVisible" class="mt-3 text-sm text-surface-500 dark:text-surface-400">
|
||||||
|
No tienes permisos para ver módulos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li v-for="item in menuItems" :key="item.label">
|
<li v-for="item in visibleMenuItems" :key="item.label">
|
||||||
<!-- Item sin subitems -->
|
<!-- Item sin subitems -->
|
||||||
<a v-if="!item.items" :href="item.to" @click.prevent="item.to && navigateTo(item.to)" :class="[
|
<a v-if="!item.items" :href="item.to" @click.prevent="item.to && navigateTo(item.to)" :class="[
|
||||||
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
||||||
|
|||||||
12
src/main.ts
12
src/main.ts
@ -34,10 +34,6 @@ const MyPreset = definePreset(Aura, {
|
|||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
|
||||||
// Inicializar autenticación desde localStorage
|
|
||||||
const { initAuth } = useAuth();
|
|
||||||
initAuth();
|
|
||||||
|
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(ConfirmationService);
|
app.use(ConfirmationService);
|
||||||
@ -53,4 +49,10 @@ app.use(PrimeVue, {
|
|||||||
|
|
||||||
app.directive("styleclass", StyleClass);
|
app.directive("styleclass", StyleClass);
|
||||||
|
|
||||||
app.mount("#app");
|
const bootstrap = async () => {
|
||||||
|
const { initAuth } = useAuth();
|
||||||
|
await initAuth();
|
||||||
|
app.mount('#app');
|
||||||
|
};
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useAuth } from '../composables/useAuth';
|
import { useAuth } from '../composables/useAuth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const { login, isLoading } = useAuth();
|
const { login, isLoading } = useAuth();
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
@ -24,7 +25,9 @@ const handleLogin = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
router.push('/');
|
const requestedRedirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
|
||||||
|
const redirect = requestedRedirect.startsWith('/') ? requestedRedirect : '/';
|
||||||
|
router.push(redirect);
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = result.error || 'Error al iniciar sesión';
|
errorMessage.value = result.error || 'Error al iniciar sesión';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import authService from '../services/authService';
|
import authService from '../services/authService';
|
||||||
|
import {
|
||||||
|
clearStoredAuthSession,
|
||||||
|
persistAuthSession,
|
||||||
|
readStoredPermissions,
|
||||||
|
readStoredRoles,
|
||||||
|
readStoredToken,
|
||||||
|
readStoredUser,
|
||||||
|
} from '../utils/authStorage';
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
id: number;
|
id: number;
|
||||||
@ -51,23 +59,60 @@ export interface LoginCredentials {
|
|||||||
// Estado global de autenticación
|
// Estado global de autenticación
|
||||||
const user = ref<User | null>(null);
|
const user = ref<User | null>(null);
|
||||||
const token = ref<string | null>(null);
|
const token = ref<string | null>(null);
|
||||||
|
const roles = ref<Role[]>([]);
|
||||||
|
const permissions = ref<string[]>([]);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
const isInitialized = ref(false);
|
||||||
|
let unauthorizedListenerRegistered = false;
|
||||||
|
|
||||||
|
const normalizeTarget = (target: string | string[]): string[] =>
|
||||||
|
Array.isArray(target) ? target : [target];
|
||||||
|
|
||||||
|
const clearSession = () => {
|
||||||
|
user.value = null;
|
||||||
|
token.value = null;
|
||||||
|
roles.value = [];
|
||||||
|
permissions.value = [];
|
||||||
|
clearStoredAuthSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasRole = (target: string | string[]): boolean => {
|
||||||
|
const roleNames = roles.value.map((role) => role.name);
|
||||||
|
return normalizeTarget(target).some((roleName) => roleNames.includes(roleName));
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPermission = (target: string | string[]): boolean => {
|
||||||
|
return normalizeTarget(target).some((permissionName) => permissions.value.includes(permissionName));
|
||||||
|
};
|
||||||
|
|
||||||
// Inicializar desde localStorage
|
// Inicializar desde localStorage
|
||||||
const initAuth = () => {
|
const initAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const storedToken = localStorage.getItem('auth_token');
|
const storedToken = readStoredToken();
|
||||||
const storedUser = localStorage.getItem('auth_user');
|
const storedUser = readStoredUser();
|
||||||
|
const storedRoles = readStoredRoles();
|
||||||
|
const storedPermissions = readStoredPermissions();
|
||||||
|
|
||||||
if (storedToken && storedUser && storedUser !== 'undefined') {
|
if (!storedToken || !storedUser) {
|
||||||
token.value = storedToken;
|
clearSession();
|
||||||
user.value = JSON.parse(storedUser);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token.value = storedToken;
|
||||||
|
user.value = storedUser;
|
||||||
|
roles.value = storedRoles;
|
||||||
|
permissions.value = storedPermissions;
|
||||||
|
|
||||||
|
const sessionData = await authService.refreshSession();
|
||||||
|
user.value = sessionData.user;
|
||||||
|
roles.value = sessionData.roles;
|
||||||
|
permissions.value = sessionData.permissions;
|
||||||
|
persistAuthSession(storedToken, sessionData.user, sessionData.roles, sessionData.permissions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al inicializar autenticación:', error);
|
console.error('Error al inicializar autenticación:', error);
|
||||||
// Limpiar localStorage si hay datos corruptos
|
clearSession();
|
||||||
localStorage.removeItem('auth_token');
|
} finally {
|
||||||
localStorage.removeItem('auth_user');
|
isInitialized.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,10 +130,18 @@ const login = async (credentials: LoginCredentials): Promise<{ success: boolean;
|
|||||||
// Guardar en estado
|
// Guardar en estado
|
||||||
user.value = response.user;
|
user.value = response.user;
|
||||||
token.value = response.token;
|
token.value = response.token;
|
||||||
|
roles.value = [];
|
||||||
|
permissions.value = [];
|
||||||
|
|
||||||
|
persistAuthSession(response.token, response.user, [], []);
|
||||||
|
|
||||||
|
const sessionData = await authService.refreshSession();
|
||||||
|
user.value = sessionData.user;
|
||||||
|
roles.value = sessionData.roles;
|
||||||
|
permissions.value = sessionData.permissions;
|
||||||
|
|
||||||
// Persistir en localStorage
|
// Persistir en localStorage
|
||||||
localStorage.setItem('auth_token', response.token);
|
persistAuthSession(response.token, sessionData.user, sessionData.roles, sessionData.permissions);
|
||||||
localStorage.setItem('auth_user', JSON.stringify(response.user));
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -105,10 +158,7 @@ const login = async (credentials: LoginCredentials): Promise<{ success: boolean;
|
|||||||
// Función de logout
|
// Función de logout
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
// Limpiar estado local inmediatamente para reactividad
|
// Limpiar estado local inmediatamente para reactividad
|
||||||
user.value = null;
|
clearSession();
|
||||||
token.value = null;
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
localStorage.removeItem('auth_user');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Notificar al backend (en segundo plano)
|
// Notificar al backend (en segundo plano)
|
||||||
@ -119,15 +169,28 @@ const logout = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && !unauthorizedListenerRegistered) {
|
||||||
|
unauthorizedListenerRegistered = true;
|
||||||
|
window.addEventListener('auth:unauthorized', () => {
|
||||||
|
clearSession();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Composable para usar en componentes
|
// Composable para usar en componentes
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isInitialized,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
hasRole,
|
||||||
|
hasPermission,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
initAuth
|
initAuth,
|
||||||
|
clearSession,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,87 @@ export interface RegisterData {
|
|||||||
password_confirmation: string;
|
password_confirmation: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionRefreshData {
|
||||||
|
user: User;
|
||||||
|
roles: Role[];
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
|
private extractArrayFromResponse(payload: unknown): unknown[] {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = payload as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (Array.isArray(root.data)) {
|
||||||
|
return root.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.data && typeof root.data === 'object') {
|
||||||
|
const nested = root.data as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (Array.isArray(nested.permissions)) {
|
||||||
|
return nested.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(nested.roles)) {
|
||||||
|
return nested.roles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePermissions(payload: unknown): string[] {
|
||||||
|
const items = this.extractArrayFromResponse(payload);
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item && typeof item === 'object' && typeof (item as Record<string, unknown>).name === 'string') {
|
||||||
|
return (item as Record<string, string>).name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((permission): permission is string => Boolean(permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeRoles(payload: unknown): Role[] {
|
||||||
|
const items = this.extractArrayFromResponse(payload);
|
||||||
|
|
||||||
|
return items.filter((item): item is Role => {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = item as Partial<Role>;
|
||||||
|
return typeof role.name === 'string';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveAuthError(error: any, fallback = 'Error en autenticación'): never {
|
||||||
|
if (error.response?.status === 422 && error.response?.data?.errors) {
|
||||||
|
const firstError = Object.values(error.response.data.errors)?.[0] as string[] | undefined;
|
||||||
|
throw new Error(firstError?.[0] || error.response.data.message || fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
throw new Error('Demasiados intentos de inicio de sesión. Intenta nuevamente en un minuto.');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(error.response?.data?.message || fallback);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iniciar sesión
|
* Iniciar sesión
|
||||||
*/
|
*/
|
||||||
@ -34,17 +114,7 @@ class AuthService {
|
|||||||
token: response.data.data.token
|
token: response.data.data.token
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error en login:', error);
|
this.resolveAuthError(error, 'Error al iniciar sesión');
|
||||||
|
|
||||||
// Manejar errores de validación (422)
|
|
||||||
if (error.response?.status === 422 && error.response?.data?.errors) {
|
|
||||||
const errors = error.response.data.errors;
|
|
||||||
const firstError = Object.values(errors)[0] as string[];
|
|
||||||
throw new Error(firstError[0] || error.response.data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manejar otros errores
|
|
||||||
throw new Error(error.response?.data?.message || 'Error al iniciar sesión');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +126,7 @@ class AuthService {
|
|||||||
const response = await api.post<LoginResponse>('/api/auth/register', data);
|
const response = await api.post<LoginResponse>('/api/auth/register', data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al registrar usuario');
|
this.resolveAuthError(error, 'Error al registrar usuario');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,18 +135,7 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await api.post<{
|
await api.post('/api/auth/logout');
|
||||||
status: string;
|
|
||||||
data: {
|
|
||||||
is_revoked: boolean;
|
|
||||||
};
|
|
||||||
}>('/api/auth/logout');
|
|
||||||
|
|
||||||
console.log('Respuesta logout:', response.data);
|
|
||||||
|
|
||||||
if (response.data.status === 'success' && response.data.data.is_revoked) {
|
|
||||||
console.log('Token revocado exitosamente');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error al cerrar sesión:', error);
|
console.error('Error al cerrar sesión:', error);
|
||||||
// No lanzar error para que el logout local continúe aunque falle el backend
|
// No lanzar error para que el logout local continúe aunque falle el backend
|
||||||
@ -88,10 +147,46 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async getCurrentUser(): Promise<User> {
|
async getCurrentUser(): Promise<User> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<User>('/api/auth/me');
|
const response = await api.get<User>('/api/user');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al obtener usuario');
|
this.resolveAuthError(error, 'Error al obtener usuario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserRoles(): Promise<Role[]> {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api/user/roles');
|
||||||
|
return this.normalizeRoles(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.resolveAuthError(error, 'Error al obtener roles del usuario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserPermissions(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api/user/permissions');
|
||||||
|
return this.normalizePermissions(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.resolveAuthError(error, 'Error al obtener permisos del usuario');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshSession(): Promise<SessionRefreshData> {
|
||||||
|
try {
|
||||||
|
const [currentUser, roles, permissions] = await Promise.all([
|
||||||
|
this.getCurrentUser(),
|
||||||
|
this.getUserRoles(),
|
||||||
|
this.getUserPermissions(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: currentUser,
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
this.resolveAuthError(error, 'Error al sincronizar sesión');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +198,7 @@ class AuthService {
|
|||||||
const response = await api.put<User>('/api/auth/profile', data);
|
const response = await api.put<User>('/api/auth/profile', data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al actualizar perfil');
|
this.resolveAuthError(error, 'Error al actualizar perfil');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +207,12 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await api.post('/auth/change-password', {
|
await api.post('/api/auth/change-password', {
|
||||||
current_password: currentPassword,
|
current_password: currentPassword,
|
||||||
new_password: newPassword
|
new_password: newPassword
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al cambiar contraseña');
|
this.resolveAuthError(error, 'Error al cambiar contraseña');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +223,7 @@ class AuthService {
|
|||||||
try {
|
try {
|
||||||
await api.post('/api/auth/forgot-password', { email });
|
await api.post('/api/auth/forgot-password', { email });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al solicitar recuperación');
|
this.resolveAuthError(error, 'Error al solicitar recuperación');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +234,7 @@ class AuthService {
|
|||||||
try {
|
try {
|
||||||
await api.post('/api/auth/reset-password', { token, password });
|
await api.post('/api/auth/reset-password', { token, password });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.response?.data?.message || 'Error al resetear contraseña');
|
this.resolveAuthError(error, 'Error al resetear contraseña');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/modules/auth/utils/authStorage.ts
Normal file
66
src/modules/auth/utils/authStorage.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import type { Role, User } from '../composables/useAuth';
|
||||||
|
|
||||||
|
export const AUTH_TOKEN_KEY = 'auth_token';
|
||||||
|
export const AUTH_USER_KEY = 'auth_user';
|
||||||
|
export const AUTH_ROLES_KEY = 'auth_roles';
|
||||||
|
export const AUTH_PERMISSIONS_KEY = 'auth_permissions';
|
||||||
|
|
||||||
|
export const readStoredToken = (): string | null => localStorage.getItem(AUTH_TOKEN_KEY);
|
||||||
|
|
||||||
|
export const readStoredUser = (): User | null => {
|
||||||
|
const rawUser = localStorage.getItem(AUTH_USER_KEY);
|
||||||
|
|
||||||
|
if (!rawUser || rawUser === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawUser) as User;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readStoredRoles = (): Role[] => {
|
||||||
|
const rawRoles = localStorage.getItem(AUTH_ROLES_KEY);
|
||||||
|
|
||||||
|
if (!rawRoles || rawRoles === 'undefined') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawRoles) as Role[];
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readStoredPermissions = (): string[] => {
|
||||||
|
const rawPermissions = localStorage.getItem(AUTH_PERMISSIONS_KEY);
|
||||||
|
|
||||||
|
if (!rawPermissions || rawPermissions === 'undefined') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawPermissions) as string[];
|
||||||
|
return Array.isArray(parsed) ? parsed.filter((permission) => typeof permission === 'string') : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistAuthSession = (token: string, user: User, roles: Role[] = [], permissions: string[] = []) => {
|
||||||
|
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||||
|
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
|
||||||
|
localStorage.setItem(AUTH_ROLES_KEY, JSON.stringify(roles));
|
||||||
|
localStorage.setItem(AUTH_PERMISSIONS_KEY, JSON.stringify(permissions));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearStoredAuthSession = () => {
|
||||||
|
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(AUTH_USER_KEY);
|
||||||
|
localStorage.removeItem(AUTH_ROLES_KEY);
|
||||||
|
localStorage.removeItem(AUTH_PERMISSIONS_KEY);
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@
|
|||||||
<p class="text-slate-500 dark:text-slate-400">Administra y monitorea todas las entidades legales registradas en el sistema.</p>
|
<p class="text-slate-500 dark:text-slate-400">Administra y monitorea todas las entidades legales registradas en el sistema.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canCreateCompany"
|
||||||
label="Registrar Nueva Empresa"
|
label="Registrar Nueva Empresa"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
@ -17,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table Section -->
|
<!-- Table Section -->
|
||||||
<Card>
|
<Card v-if="canViewCompanies">
|
||||||
<template #content>
|
<template #content>
|
||||||
<DataTable
|
<DataTable
|
||||||
:value="companies"
|
:value="companies"
|
||||||
@ -60,13 +61,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column field="address" header="Ubicación" style="min-width: 200px">
|
<Column field="domains_count" header="Dominios" style="min-width: 120px">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div v-if="data.address">
|
<span class="font-mono text-sm">{{ data.domains_count || 0 }}</span>
|
||||||
<p class="text-sm">{{ data.address.city }}</p>
|
|
||||||
<p class="text-xs text-slate-400">{{ data.address.state }}</p>
|
|
||||||
</div>
|
|
||||||
<span v-else class="text-slate-400">Sin dirección</span>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
@ -74,6 +71,7 @@
|
|||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canUpdateCompany"
|
||||||
icon="pi pi-pencil"
|
icon="pi pi-pencil"
|
||||||
class="p-button-rounded p-button-text p-button-sm"
|
class="p-button-rounded p-button-text p-button-sm"
|
||||||
@click="editCompany(data)"
|
@click="editCompany(data)"
|
||||||
@ -86,6 +84,7 @@
|
|||||||
v-tooltip.top="'Ver detalles'"
|
v-tooltip.top="'Ver detalles'"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canDeleteCompany"
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
class="p-button-rounded p-button-text p-button-sm p-button-danger"
|
class="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||||
@click="confirmDelete(data)"
|
@click="confirmDelete(data)"
|
||||||
@ -105,6 +104,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card v-else>
|
||||||
|
<template #content>
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<i class="pi pi-lock text-4xl text-slate-300 mb-3"></i>
|
||||||
|
<p class="text-slate-500">No tienes permisos para ver el listado de empresas.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- Form Dialog -->
|
<!-- Form Dialog -->
|
||||||
<CompaniesForm
|
<CompaniesForm
|
||||||
ref="companiesFormRef"
|
ref="companiesFormRef"
|
||||||
@ -129,14 +137,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import CompaniesForm from './CompaniesForm.vue';
|
import CompaniesForm from './CompaniesForm.vue';
|
||||||
import { companyService } from './companies.service';
|
import { companiesService } from './companies.service';
|
||||||
import type { Company, PaginatedResponse } from './companies.types';
|
import type { Company, TenantFormData } from './companies.types';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
|
||||||
// Toast
|
// Toast
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const companiesFormRef = ref<InstanceType<typeof CompaniesForm> | null>(null);
|
const companiesFormRef = ref<InstanceType<typeof CompaniesForm> | null>(null);
|
||||||
@ -146,46 +156,43 @@ const companies = ref<Company[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const deleteDialogVisible = ref(false);
|
const deleteDialogVisible = ref(false);
|
||||||
const selectedCompany = ref<Company | null>(null);
|
const selectedCompany = ref<Company | undefined>(undefined);
|
||||||
const companyToDelete = ref<Company | null>(null);
|
const companyToDelete = ref<Company | null>(null);
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const totalRecords = ref(0);
|
const totalRecords = ref(0);
|
||||||
const perPage = ref(10);
|
const perPage = ref(10);
|
||||||
|
const canViewCompanies = computed(() =>
|
||||||
|
hasPermission([
|
||||||
|
'companies.index',
|
||||||
|
'companies.show',
|
||||||
|
'companies.store',
|
||||||
|
'companies.update',
|
||||||
|
'companies.destroy',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const canCreateCompany = computed(() => hasPermission('companies.store'));
|
||||||
|
const canUpdateCompany = computed(() => hasPermission('companies.update'));
|
||||||
|
const canDeleteCompany = computed(() => hasPermission('companies.destroy'));
|
||||||
|
|
||||||
// Search filters
|
// Search filters
|
||||||
const searchFilters = ref({
|
|
||||||
rfc: '',
|
|
||||||
curp: '',
|
|
||||||
company_name: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const loadCompanies = async () => {
|
const loadCompanies = async (page = currentPage.value) => {
|
||||||
loading.value = true;
|
if (!canViewCompanies.value) {
|
||||||
try {
|
companies.value = [];
|
||||||
const params = {
|
return;
|
||||||
paginate: true,
|
|
||||||
page: currentPage.value,
|
|
||||||
...searchFilters.value
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await companyService.getAll(params);
|
|
||||||
|
|
||||||
if ('current_page' in response) {
|
|
||||||
// Respuesta paginada
|
|
||||||
const paginatedData = response as PaginatedResponse<Company>;
|
|
||||||
companies.value = paginatedData.data;
|
|
||||||
currentPage.value = paginatedData.current_page;
|
|
||||||
totalRecords.value = paginatedData.total;
|
|
||||||
perPage.value = paginatedData.per_page;
|
|
||||||
} else {
|
|
||||||
// Respuesta sin paginación
|
|
||||||
companies.value = response as Company[];
|
|
||||||
totalRecords.value = companies.value.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await companiesService.getAll(page);
|
||||||
|
companies.value = response.data;
|
||||||
|
currentPage.value = response.current_page;
|
||||||
|
totalRecords.value = response.total;
|
||||||
|
perPage.value = response.per_page;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading companies:', error);
|
console.error('Error loading companies:', error);
|
||||||
companies.value = [];
|
companies.value = [];
|
||||||
@ -196,25 +203,37 @@ const loadCompanies = async () => {
|
|||||||
|
|
||||||
const onPageChange = (event: any) => {
|
const onPageChange = (event: any) => {
|
||||||
currentPage.value = event.page + 1; // PrimeVue usa índice base 0
|
currentPage.value = event.page + 1; // PrimeVue usa índice base 0
|
||||||
loadCompanies();
|
loadCompanies(currentPage.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
selectedCompany.value = null;
|
if (!canCreateCompany.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCompany.value = undefined;
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const editCompany = (company: any) => {
|
const editCompany = (company: Company) => {
|
||||||
|
if (!canUpdateCompany.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedCompany.value = { ...company };
|
selectedCompany.value = { ...company };
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
};
|
}; // No change needed — assigns a valid Company
|
||||||
|
|
||||||
const viewCompany = (company: any) => {
|
const viewCompany = (company: Company) => {
|
||||||
// TODO: Implementar vista de detalles
|
// Aquí podrías abrir un modal de detalles o navegar a una vista
|
||||||
console.log('View company:', company);
|
console.log('View company:', company);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = (company: any) => {
|
const confirmDelete = (company: Company) => {
|
||||||
|
if (!canDeleteCompany.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
companyToDelete.value = company;
|
companyToDelete.value = company;
|
||||||
deleteDialogVisible.value = true;
|
deleteDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
@ -223,7 +242,7 @@ const deleteCompany = async () => {
|
|||||||
if (!companyToDelete.value?.id) return;
|
if (!companyToDelete.value?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await companyService.delete(companyToDelete.value.id);
|
await companiesService.delete(companyToDelete.value.id);
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
@ -248,76 +267,76 @@ const deleteCompany = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async (companyData: any) => {
|
const handleSave = async (companyData: TenantFormData) => {
|
||||||
try {
|
try {
|
||||||
if (selectedCompany.value?.id) {
|
const isEditMode = Boolean(selectedCompany.value?.id);
|
||||||
// Actualizar empresa existente
|
|
||||||
await companyService.update(selectedCompany.value.id, companyData);
|
|
||||||
|
|
||||||
|
if (isEditMode && !canUpdateCompany.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permiso para actualizar empresas.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
companiesFormRef.value?.resetSubmitting();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEditMode && !canCreateCompany.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permiso para crear empresas.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
companiesFormRef.value?.resetSubmitting();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditMode && selectedCompany.value?.id) {
|
||||||
|
await companiesService.update(selectedCompany.value.id, companyData);
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Empresa actualizada',
|
summary: 'Empresa actualizada',
|
||||||
detail: `La empresa "${companyData.company_name}" ha sido actualizada exitosamente`,
|
detail: `La empresa "${companyData.company_name}" se actualizó correctamente`,
|
||||||
life: 3000
|
life: 3000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Crear nueva empresa
|
await companiesService.create(companyData);
|
||||||
await companyService.create(companyData);
|
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Empresa creada',
|
summary: 'Empresa creada',
|
||||||
detail: `La empresa "${companyData.company_name}" ha sido registrada exitosamente`,
|
detail: `La empresa "${companyData.company_name}" se creó correctamente`,
|
||||||
life: 3000
|
life: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recargar la lista después de guardar
|
|
||||||
await loadCompanies();
|
await loadCompanies();
|
||||||
|
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
selectedCompany.value = null;
|
selectedCompany.value = undefined;
|
||||||
|
companiesFormRef.value?.resetSubmitting();
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error saving company:', error);
|
console.error('Error saving company:', error);
|
||||||
|
|
||||||
// Verificar si es un error de validación (422)
|
if (error.response?.status === 422 && error.response?.data?.errors && companiesFormRef.value) {
|
||||||
if (error.response?.status === 422 && error.response?.data?.errors) {
|
companiesFormRef.value.setValidationErrors(error.response.data.errors);
|
||||||
const backendErrors = error.response.data.errors;
|
|
||||||
|
|
||||||
// Setear los errores en el formulario
|
|
||||||
if (companiesFormRef.value) {
|
|
||||||
companiesFormRef.value.setValidationErrors(backendErrors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar toast con mensaje general
|
|
||||||
toast.add({
|
|
||||||
severity: 'warn',
|
|
||||||
summary: 'Errores de validación',
|
|
||||||
detail: error.response.data.message || 'Por favor, corrija los errores en el formulario',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otros errores
|
|
||||||
const errorMessage = error.response?.data?.message ||
|
|
||||||
error.response?.data?.error ||
|
|
||||||
error.message ||
|
|
||||||
'No se pudo guardar la empresa';
|
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: selectedCompany.value?.id ? 'Error al actualizar' : 'Error al crear',
|
summary: 'Error de validación',
|
||||||
detail: errorMessage,
|
detail: 'Revisa los campos marcados en rojo',
|
||||||
life: 5000
|
life: 4000,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
// Resetear loading en el formulario para otros tipos de error
|
|
||||||
if (companiesFormRef.value) {
|
|
||||||
companiesFormRef.value.setValidationErrors({});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companiesFormRef.value?.resetSubmitting();
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error al guardar',
|
||||||
|
detail: error.response?.data?.message || 'No se pudo guardar la empresa',
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -333,7 +352,9 @@ const getInitials = (name: string): string => {
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (canViewCompanies.value) {
|
||||||
loadCompanies();
|
loadCompanies();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -3,23 +3,35 @@
|
|||||||
v-model:visible="isVisible"
|
v-model:visible="isVisible"
|
||||||
:modal="true"
|
:modal="true"
|
||||||
:closable="true"
|
:closable="true"
|
||||||
:style="{ width: '900px' }"
|
:style="{ width: '980px' }"
|
||||||
:header="formTitle"
|
:header="formTitle"
|
||||||
@hide="handleClose"
|
@hide="handleClose"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
<form class="space-y-6" @submit.prevent="handleSubmit">
|
||||||
<!-- Información Fiscal -->
|
|
||||||
<div class="border-b pb-4">
|
<div class="border-b pb-4">
|
||||||
<h3 class="text-lg font-semibold mb-4">Información Fiscal</h3>
|
<h3 class="text-lg font-semibold mb-4">Información principal</h3>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2 md:col-span-2">
|
||||||
|
<label for="company_name" class="font-medium text-sm">Razón social *</label>
|
||||||
|
<InputText
|
||||||
|
id="company_name"
|
||||||
|
v-model="form.company_name"
|
||||||
|
placeholder="Empresa Demo SA de CV"
|
||||||
|
:class="{ 'p-invalid': errors.company_name }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.company_name" class="p-error text-red-600">{{ errors.company_name }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="rfc" class="font-medium text-sm">RFC *</label>
|
<label for="rfc" class="font-medium text-sm">RFC *</label>
|
||||||
<InputText
|
<InputText
|
||||||
id="rfc"
|
id="rfc"
|
||||||
v-model="form.rfc"
|
v-model="form.rfc"
|
||||||
placeholder="ABC123456789"
|
placeholder="DEMO010101ABC"
|
||||||
:class="{ 'p-invalid': errors.rfc }"
|
|
||||||
maxlength="13"
|
maxlength="13"
|
||||||
|
class="font-mono"
|
||||||
|
:class="{ 'p-invalid': errors.rfc }"
|
||||||
|
@input="form.rfc = form.rfc.toUpperCase()"
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.rfc" class="p-error text-red-600">{{ errors.rfc }}</small>
|
<small v-if="errors.rfc" class="p-error text-red-600">{{ errors.rfc }}</small>
|
||||||
</div>
|
</div>
|
||||||
@ -29,79 +41,118 @@
|
|||||||
<InputText
|
<InputText
|
||||||
id="curp"
|
id="curp"
|
||||||
v-model="form.curp"
|
v-model="form.curp"
|
||||||
placeholder="XYZ987654321"
|
placeholder="XEXX010101HNEXXXA1"
|
||||||
:class="{ 'p-invalid': errors.curp }"
|
|
||||||
maxlength="18"
|
maxlength="18"
|
||||||
|
class="font-mono"
|
||||||
|
:class="{ 'p-invalid': errors.curp }"
|
||||||
|
@input="form.curp = form.curp.toUpperCase()"
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.curp" class="p-error text-red-600">{{ errors.curp }}</small>
|
<small v-if="errors.curp" class="p-error text-red-600">{{ errors.curp }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 md:col-span-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="company_name" class="font-medium text-sm">Razón Social *</label>
|
<label for="email" class="font-medium text-sm">Email *</label>
|
||||||
<InputText
|
<InputText
|
||||||
id="company_name"
|
id="email"
|
||||||
v-model="form.company_name"
|
v-model="form.email"
|
||||||
placeholder="Corporativo Ejemplo S.A. de C.V."
|
placeholder="admin@empresa-demo.com"
|
||||||
:class="{ 'p-invalid': errors.company_name }"
|
:class="{ 'p-invalid': errors.email }"
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.company_name" class="p-error text-red-600">{{ errors.company_name }}</small>
|
<small v-if="errors.email" class="p-error text-red-600">{{ errors.email }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="primary_domain" class="font-medium text-sm">
|
||||||
|
Dominio primario {{ isEditMode ? '(opcional en edición)' : '*' }}
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
id="primary_domain"
|
||||||
|
v-model="form.primary_domain"
|
||||||
|
placeholder="empresa-demo.local"
|
||||||
|
:disabled="isEditMode"
|
||||||
|
:class="{ 'p-invalid': errors.primary_domain }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.primary_domain" class="p-error text-red-600">{{ errors.primary_domain }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="phone" class="font-medium text-sm">Teléfono</label>
|
||||||
|
<InputText id="phone" v-model="form.phone" placeholder="5551234567" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="tax_regime" class="font-medium text-sm">Régimen fiscal</label>
|
||||||
|
<InputText id="tax_regime" v-model="form.tax_regime" placeholder="601" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 md:col-span-2">
|
||||||
|
<label for="legal_representative" class="font-medium text-sm">Representante legal</label>
|
||||||
|
<InputText
|
||||||
|
id="legal_representative"
|
||||||
|
v-model="form.legal_representative"
|
||||||
|
placeholder="Juan Pérez"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 md:col-span-2">
|
||||||
|
<InputSwitch v-model="form.is_active" inputId="is_active" />
|
||||||
|
<label for="is_active" class="font-medium text-sm">
|
||||||
|
Tenant activo
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tasas e Impuestos -->
|
|
||||||
<div class="border-b pb-4">
|
<div class="border-b pb-4">
|
||||||
<h3 class="text-lg font-semibold mb-4">Tasas e Impuestos (%)</h3>
|
<h3 class="text-lg font-semibold mb-4">Tasas e impuestos (%)</h3>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="vat_rate" class="font-medium text-sm">Tasa de IVA *</label>
|
<label for="vat_rate" class="font-medium text-sm">Tasa de IVA (opcional)</label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="vat_rate"
|
id="vat_rate"
|
||||||
v-model="form.vat_rate"
|
v-model="form.vat_rate"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
:minFractionDigits="2"
|
:minFractionDigits="1"
|
||||||
:maxFractionDigits="2"
|
:maxFractionDigits="2"
|
||||||
suffix="%"
|
suffix="%"
|
||||||
:class="{ 'p-invalid': errors.vat_rate }"
|
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.vat_rate" class="p-error text-red-600">{{ errors.vat_rate }}</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="isr_withholding" class="font-medium text-sm">Retención ISR</label>
|
<label for="isr_withholding" class="font-medium text-sm">Retención ISR (opcional)</label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="isr_withholding"
|
id="isr_withholding"
|
||||||
v-model="form.isr_withholding"
|
v-model="form.isr_withholding"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
:minFractionDigits="2"
|
:minFractionDigits="1"
|
||||||
:maxFractionDigits="2"
|
:maxFractionDigits="2"
|
||||||
suffix="%"
|
suffix="%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="vat_withholding" class="font-medium text-sm">Retención IVA</label>
|
<label for="vat_withholding" class="font-medium text-sm">Retención IVA (opcional)</label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="vat_withholding"
|
id="vat_withholding"
|
||||||
v-model="form.vat_withholding"
|
v-model="form.vat_withholding"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
:minFractionDigits="2"
|
:minFractionDigits="1"
|
||||||
:maxFractionDigits="2"
|
:maxFractionDigits="2"
|
||||||
suffix="%"
|
suffix="%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="additional_tax" class="font-medium text-sm">Impuesto Adicional</label>
|
<label for="additional_tax" class="font-medium text-sm">Impuesto adicional (opcional)</label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
id="additional_tax"
|
id="additional_tax"
|
||||||
v-model="form.additional_tax"
|
v-model="form.additional_tax"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="100"
|
:max="100"
|
||||||
:minFractionDigits="2"
|
:minFractionDigits="1"
|
||||||
:maxFractionDigits="2"
|
:maxFractionDigits="2"
|
||||||
suffix="%"
|
suffix="%"
|
||||||
/>
|
/>
|
||||||
@ -109,315 +160,204 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dirección -->
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold mb-4">Dirección Fiscal</h3>
|
<h3 class="text-lg font-semibold mb-4">Certificados SAT</h3>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="country" class="font-medium text-sm">País *</label>
|
<label for="certificate_password" class="font-medium text-sm">Password del certificado</label>
|
||||||
<InputText
|
<Password
|
||||||
id="country"
|
id="certificate_password"
|
||||||
v-model="form.address.country"
|
v-model="form.certificate_password"
|
||||||
placeholder="México"
|
:feedback="false"
|
||||||
:class="{ 'p-invalid': errors.country }"
|
toggleMask
|
||||||
|
placeholder="Password del certificado"
|
||||||
|
:class="{ 'p-invalid': errors.certificate_password }"
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.country" class="p-error text-red-600">{{ errors.country }}</small>
|
<small v-if="errors.certificate_password" class="p-error text-red-600">{{ errors.certificate_password }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="postal_code" class="font-medium text-sm">Código Postal *</label>
|
<label for="certificate_cer_file" class="font-medium text-sm">Archivo .cer</label>
|
||||||
<InputText
|
<input
|
||||||
id="postal_code"
|
id="certificate_cer_file"
|
||||||
v-model="form.address.postal_code"
|
type="file"
|
||||||
placeholder="12345"
|
accept=".cer"
|
||||||
:class="{ 'p-invalid': errors.postal_code }"
|
class="p-2 border rounded-md border-surface-300 dark:border-surface-700"
|
||||||
maxlength="10"
|
@change="onFileChange($event, 'certificate_cer_file')"
|
||||||
/>
|
/>
|
||||||
<small v-if="errors.postal_code" class="p-error text-red-600">{{ errors.postal_code }}</small>
|
<small v-if="errors.certificate_cer_file" class="p-error text-red-600">{{ errors.certificate_cer_file }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="state" class="font-medium text-sm">Estado *</label>
|
<label for="certificate_key_file" class="font-medium text-sm">Archivo .key</label>
|
||||||
<InputText
|
<input
|
||||||
id="state"
|
id="certificate_key_file"
|
||||||
v-model="form.address.state"
|
type="file"
|
||||||
placeholder="CDMX"
|
accept=".key"
|
||||||
:class="{ 'p-invalid': errors.state }"
|
class="p-2 border rounded-md border-surface-300 dark:border-surface-700"
|
||||||
/>
|
@change="onFileChange($event, 'certificate_key_file')"
|
||||||
<small v-if="errors.state" class="p-error text-red-600">{{ errors.state }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label for="municipality" class="font-medium text-sm">Municipio / Alcaldía *</label>
|
|
||||||
<InputText
|
|
||||||
id="municipality"
|
|
||||||
v-model="form.address.municipality"
|
|
||||||
placeholder="Benito Juárez"
|
|
||||||
:class="{ 'p-invalid': errors.municipality }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.municipality" class="p-error text-red-600">{{ errors.municipality }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label for="city" class="font-medium text-sm">Ciudad *</label>
|
|
||||||
<InputText
|
|
||||||
id="city"
|
|
||||||
v-model="form.address.city"
|
|
||||||
placeholder="Ciudad de México"
|
|
||||||
:class="{ 'p-invalid': errors.city }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.city" class="p-error text-red-600">{{ errors.city }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label for="street" class="font-medium text-sm">Calle *</label>
|
|
||||||
<InputText
|
|
||||||
id="street"
|
|
||||||
v-model="form.address.street"
|
|
||||||
placeholder="Av. Ejemplo"
|
|
||||||
:class="{ 'p-invalid': errors.street }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.street" class="p-error text-red-600">{{ errors.street }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label for="num_ext" class="font-medium text-sm">Número Exterior *</label>
|
|
||||||
<InputText
|
|
||||||
id="num_ext"
|
|
||||||
v-model="form.address.num_ext"
|
|
||||||
placeholder="100"
|
|
||||||
:class="{ 'p-invalid': errors.num_ext }"
|
|
||||||
/>
|
|
||||||
<small v-if="errors.num_ext" class="p-error text-red-600">{{ errors.num_ext }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label for="num_int" class="font-medium text-sm">Número Interior</label>
|
|
||||||
<InputText
|
|
||||||
id="num_int"
|
|
||||||
v-model="form.address.num_int"
|
|
||||||
placeholder="10"
|
|
||||||
/>
|
/>
|
||||||
|
<small v-if="errors.certificate_key_file" class="p-error text-red-600">{{ errors.certificate_key_file }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-slate-500">
|
<div class="text-xs text-slate-500">* Campos obligatorios</div>
|
||||||
* Campos obligatorios
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<Button label="Cancelar" icon="pi pi-times" @click="handleClose" class="p-button-text" />
|
||||||
label="Cancelar"
|
<Button label="Guardar" icon="pi pi-check" @click="handleSubmit" :loading="loading" />
|
||||||
icon="pi pi-times"
|
|
||||||
@click="handleClose"
|
|
||||||
class="p-button-text"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Guardar"
|
|
||||||
icon="pi pi-check"
|
|
||||||
@click="handleSubmit"
|
|
||||||
:loading="loading"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import type { Company, TenantFormData } from './companies.types';
|
||||||
|
import { toTenantFormData } from './companies.mapper';
|
||||||
|
|
||||||
// Props
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
company?: any;
|
company?: Company;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: [];
|
close: [];
|
||||||
save: [data: any];
|
save: [data: TenantFormData];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Métodos
|
|
||||||
// Método para setear errores de validación del backend
|
|
||||||
const setValidationErrors = (backendErrors: Record<string, string[]>) => {
|
|
||||||
loading.value = false;
|
|
||||||
errors.value = {};
|
|
||||||
|
|
||||||
// Mapear los errores del backend al formato del formulario
|
|
||||||
Object.keys(backendErrors).forEach(key => {
|
|
||||||
if (backendErrors[key] && backendErrors[key].length > 0) {
|
|
||||||
// Tomar el primer mensaje de error de cada campo
|
|
||||||
const firstError = backendErrors[key][0];
|
|
||||||
if (firstError) {
|
|
||||||
errors.value[key] = firstError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expose method to set errors from parent
|
|
||||||
defineExpose({
|
|
||||||
setValidationErrors
|
|
||||||
});
|
|
||||||
|
|
||||||
// State
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const errors = ref<Record<string, string>>({});
|
||||||
|
const form = ref<TenantFormData>(toTenantFormData());
|
||||||
|
|
||||||
const isVisible = computed({
|
const isVisible = computed({
|
||||||
get: () => props.visible,
|
get: () => props.visible,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
if (!value) emit('close');
|
if (!value) emit('close');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formTitle = computed(() => {
|
const isEditMode = computed(() => Boolean(props.company?.id));
|
||||||
return props.company ? 'Editar Empresa' : 'Registrar Nueva Empresa';
|
const formTitle = computed(() => (isEditMode.value ? 'Editar Empresa' : 'Registrar Nueva Empresa'));
|
||||||
});
|
|
||||||
|
|
||||||
// Form data
|
const setValidationErrors = (backendErrors: Record<string, string[]>) => {
|
||||||
const defaultForm = () => ({
|
loading.value = false;
|
||||||
rfc: '',
|
|
||||||
curp: '',
|
|
||||||
company_name: '',
|
|
||||||
vat_rate: 16.00,
|
|
||||||
isr_withholding: 0.00,
|
|
||||||
vat_withholding: 0.00,
|
|
||||||
additional_tax: 0.00,
|
|
||||||
address: {
|
|
||||||
country: 'México',
|
|
||||||
postal_code: '',
|
|
||||||
state: '',
|
|
||||||
municipality: '',
|
|
||||||
city: '',
|
|
||||||
street: '',
|
|
||||||
num_ext: '',
|
|
||||||
num_int: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = ref(defaultForm());
|
|
||||||
const errors = ref<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Watch for company changes
|
|
||||||
watch(() => props.company, (newCompany) => {
|
|
||||||
if (newCompany) {
|
|
||||||
form.value = {
|
|
||||||
...defaultForm(),
|
|
||||||
...newCompany,
|
|
||||||
address: {
|
|
||||||
...defaultForm().address,
|
|
||||||
...(newCompany.address || {})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
form.value = defaultForm();
|
|
||||||
}
|
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
loading.value = false; // Reset loading state
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
// Watch for dialog visibility to reset loading
|
Object.keys(backendErrors).forEach((key) => {
|
||||||
watch(() => props.visible, (isVisible) => {
|
const firstError = backendErrors[key]?.[0];
|
||||||
if (!isVisible) {
|
if (firstError) {
|
||||||
|
errors.value[key] = firstError;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSubmitting = () => {
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setValidationErrors,
|
||||||
|
resetSubmitting,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.company,
|
||||||
|
(newCompany) => {
|
||||||
|
const primaryDomain = newCompany?.primary_domain ?? newCompany?.domains?.[0]?.domain ?? '';
|
||||||
|
form.value = toTenantFormData({
|
||||||
|
id: newCompany?.id,
|
||||||
|
company_name: newCompany?.company_name,
|
||||||
|
rfc: newCompany?.rfc,
|
||||||
|
email: newCompany?.email,
|
||||||
|
primary_domain: primaryDomain,
|
||||||
|
curp: newCompany?.curp ?? '',
|
||||||
|
phone: newCompany?.phone ?? '',
|
||||||
|
legal_representative: newCompany?.legal_representative ?? '',
|
||||||
|
tax_regime: newCompany?.tax_regime ?? '',
|
||||||
|
is_active: newCompany?.is_active ?? true,
|
||||||
|
vat_rate: newCompany?.vat_rate ?? 0.0,
|
||||||
|
isr_withholding: newCompany?.isr_withholding ?? 0.0,
|
||||||
|
vat_withholding: newCompany?.vat_withholding ?? 0.0,
|
||||||
|
additional_tax: newCompany?.additional_tax ?? 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
errors.value = {};
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (!visible) {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValidEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
|
||||||
// Methods
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
let isValid = true;
|
let valid = true;
|
||||||
|
|
||||||
// RFC validation
|
if (!form.value.company_name.trim()) {
|
||||||
if (!form.value.rfc || form.value.rfc.trim() === '') {
|
|
||||||
errors.value.rfc = 'El RFC es obligatorio';
|
|
||||||
isValid = false;
|
|
||||||
} else if (form.value.rfc.length < 12 || form.value.rfc.length > 13) {
|
|
||||||
errors.value.rfc = 'El RFC debe tener 12 o 13 caracteres';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Company name validation
|
|
||||||
if (!form.value.company_name || form.value.company_name.trim() === '') {
|
|
||||||
errors.value.company_name = 'La razón social es obligatoria';
|
errors.value.company_name = 'La razón social es obligatoria';
|
||||||
isValid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VAT rate validation
|
if (!form.value.rfc.trim()) {
|
||||||
if (form.value.vat_rate === null || form.value.vat_rate === undefined) {
|
errors.value.rfc = 'El RFC es obligatorio';
|
||||||
errors.value.vat_rate = 'La tasa de IVA es obligatoria';
|
valid = false;
|
||||||
isValid = false;
|
} else if (form.value.rfc.trim().length > 13) {
|
||||||
|
errors.value.rfc = 'El RFC no puede exceder 13 caracteres';
|
||||||
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address validations
|
if (!form.value.email.trim()) {
|
||||||
if (!form.value.address.country || form.value.address.country.trim() === '') {
|
errors.value.email = 'El email es obligatorio';
|
||||||
errors.value.country = 'El país es obligatorio';
|
valid = false;
|
||||||
isValid = false;
|
} else if (!isValidEmail(form.value.email.trim())) {
|
||||||
|
errors.value.email = 'Ingresa un email válido';
|
||||||
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.value.address.postal_code || form.value.address.postal_code.trim() === '') {
|
if (!isEditMode.value && !form.value.primary_domain.trim()) {
|
||||||
errors.value.postal_code = 'El código postal es obligatorio';
|
errors.value.primary_domain = 'El dominio primario es obligatorio';
|
||||||
isValid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.value.address.state || form.value.address.state.trim() === '') {
|
return valid;
|
||||||
errors.value.state = 'El estado es obligatorio';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!form.value.address.municipality || form.value.address.municipality.trim() === '') {
|
|
||||||
errors.value.municipality = 'El municipio es obligatorio';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!form.value.address.city || form.value.address.city.trim() === '') {
|
|
||||||
errors.value.city = 'La ciudad es obligatoria';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!form.value.address.street || form.value.address.street.trim() === '') {
|
|
||||||
errors.value.street = 'La calle es obligatoria';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!form.value.address.num_ext || form.value.address.num_ext.trim() === '') {
|
|
||||||
errors.value.num_ext = 'El número exterior es obligatorio';
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const onFileChange = (event: Event, field: 'certificate_cer_file' | 'certificate_key_file') => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0] ?? null;
|
||||||
|
form.value[field] = file;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
emit('save', {
|
||||||
// Convert string values to uppercase for RFC and CURP
|
|
||||||
const formData = {
|
|
||||||
...form.value,
|
...form.value,
|
||||||
rfc: form.value.rfc.toUpperCase(),
|
rfc: form.value.rfc.trim().toUpperCase(),
|
||||||
curp: form.value.curp ? form.value.curp.toUpperCase() : null,
|
curp: form.value.curp.trim().toUpperCase(),
|
||||||
};
|
});
|
||||||
|
|
||||||
emit('save', formData);
|
|
||||||
|
|
||||||
// El loading se mantendrá hasta que el padre cierre el diálogo
|
|
||||||
// o maneje el error
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting form:', error);
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
form.value = defaultForm();
|
form.value = toTenantFormData();
|
||||||
errors.value = {};
|
errors.value = {};
|
||||||
|
loading.value = false;
|
||||||
emit('close');
|
emit('close');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
114
src/modules/catalog/components/companies/companies.mapper.ts
Normal file
114
src/modules/catalog/components/companies/companies.mapper.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import type { CreateTenantPayload, TenantFormData, UpdateTenantPayload } from './companies.types';
|
||||||
|
|
||||||
|
const appendIfNotEmpty = (formData: FormData, key: string, value: string | number | null | undefined) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = String(value).trim();
|
||||||
|
if (normalized.length > 0) {
|
||||||
|
formData.append(key, normalized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendNumericIfDefined = (formData: FormData, key: string, value: number | null | undefined) => {
|
||||||
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append(key, String(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendFileIfPresent = (formData: FormData, key: string, value: File | null | undefined) => {
|
||||||
|
if (value instanceof File) {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildCreateTenantPayload = (data: TenantFormData): CreateTenantPayload => ({
|
||||||
|
company_name: data.company_name.trim(),
|
||||||
|
rfc: data.rfc.trim().toUpperCase(),
|
||||||
|
email: data.email.trim(),
|
||||||
|
primary_domain: data.primary_domain.trim(),
|
||||||
|
curp: data.curp.trim().toUpperCase(),
|
||||||
|
phone: data.phone.trim(),
|
||||||
|
legal_representative: data.legal_representative.trim(),
|
||||||
|
tax_regime: data.tax_regime.trim(),
|
||||||
|
is_active: data.is_active,
|
||||||
|
vat_rate: data.vat_rate,
|
||||||
|
isr_withholding: data.isr_withholding,
|
||||||
|
vat_withholding: data.vat_withholding,
|
||||||
|
additional_tax: data.additional_tax,
|
||||||
|
certificate_password: data.certificate_password,
|
||||||
|
certificate_cer_file: data.certificate_cer_file,
|
||||||
|
certificate_key_file: data.certificate_key_file,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buildUpdateTenantPayload = (data: TenantFormData): UpdateTenantPayload => ({
|
||||||
|
company_name: data.company_name.trim(),
|
||||||
|
rfc: data.rfc.trim().toUpperCase(),
|
||||||
|
email: data.email.trim(),
|
||||||
|
primary_domain: data.primary_domain.trim() || undefined,
|
||||||
|
curp: data.curp.trim().toUpperCase(),
|
||||||
|
phone: data.phone.trim(),
|
||||||
|
legal_representative: data.legal_representative.trim(),
|
||||||
|
tax_regime: data.tax_regime.trim(),
|
||||||
|
is_active: data.is_active,
|
||||||
|
vat_rate: data.vat_rate,
|
||||||
|
isr_withholding: data.isr_withholding,
|
||||||
|
vat_withholding: data.vat_withholding,
|
||||||
|
additional_tax: data.additional_tax,
|
||||||
|
certificate_password: data.certificate_password,
|
||||||
|
certificate_cer_file: data.certificate_cer_file,
|
||||||
|
certificate_key_file: data.certificate_key_file,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toTenantFormData = (data?: Partial<TenantFormData>): TenantFormData => ({
|
||||||
|
id: data?.id,
|
||||||
|
company_name: data?.company_name ?? '',
|
||||||
|
rfc: data?.rfc ?? '',
|
||||||
|
email: data?.email ?? '',
|
||||||
|
primary_domain: data?.primary_domain ?? '',
|
||||||
|
curp: data?.curp ?? '',
|
||||||
|
phone: data?.phone ?? '',
|
||||||
|
legal_representative: data?.legal_representative ?? '',
|
||||||
|
tax_regime: data?.tax_regime ?? '',
|
||||||
|
is_active: data?.is_active ?? true,
|
||||||
|
vat_rate: data?.vat_rate ?? 0.0,
|
||||||
|
isr_withholding: data?.isr_withholding ?? 0.0,
|
||||||
|
vat_withholding: data?.vat_withholding ?? 0.0,
|
||||||
|
additional_tax: data?.additional_tax ?? 0.0,
|
||||||
|
certificate_password: data?.certificate_password ?? '',
|
||||||
|
certificate_cer_file: null,
|
||||||
|
certificate_key_file: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tenantPayloadToFormData = (
|
||||||
|
data: CreateTenantPayload | UpdateTenantPayload,
|
||||||
|
options?: { forcePut?: boolean }
|
||||||
|
): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (options?.forcePut) {
|
||||||
|
formData.append('_method', 'PUT');
|
||||||
|
}
|
||||||
|
|
||||||
|
appendIfNotEmpty(formData, 'company_name', data.company_name);
|
||||||
|
appendIfNotEmpty(formData, 'rfc', data.rfc);
|
||||||
|
appendIfNotEmpty(formData, 'email', data.email);
|
||||||
|
appendIfNotEmpty(formData, 'primary_domain', data.primary_domain);
|
||||||
|
appendIfNotEmpty(formData, 'curp', data.curp);
|
||||||
|
appendIfNotEmpty(formData, 'phone', data.phone);
|
||||||
|
appendIfNotEmpty(formData, 'legal_representative', data.legal_representative);
|
||||||
|
appendIfNotEmpty(formData, 'tax_regime', data.tax_regime);
|
||||||
|
appendNumericIfDefined(formData, 'vat_rate', data.vat_rate);
|
||||||
|
appendNumericIfDefined(formData, 'isr_withholding', data.isr_withholding);
|
||||||
|
appendNumericIfDefined(formData, 'vat_withholding', data.vat_withholding);
|
||||||
|
appendNumericIfDefined(formData, 'additional_tax', data.additional_tax);
|
||||||
|
appendIfNotEmpty(formData, 'certificate_password', data.certificate_password);
|
||||||
|
formData.append('is_active', data.is_active ? '1' : '0');
|
||||||
|
appendFileIfPresent(formData, 'certificate_cer_file', data.certificate_cer_file);
|
||||||
|
appendFileIfPresent(formData, 'certificate_key_file', data.certificate_key_file);
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
@ -1,123 +1,79 @@
|
|||||||
import api from '@/services/api';
|
import api from '@/services/api';
|
||||||
import type {
|
import {
|
||||||
Company,
|
buildCreateTenantPayload,
|
||||||
CompanyCreateResponse,
|
buildUpdateTenantPayload,
|
||||||
CompanyFormData,
|
tenantPayloadToFormData,
|
||||||
CompanyQueryParams,
|
} from './companies.mapper';
|
||||||
PaginatedResponse
|
import type { CompaniesPagination, TenantFormData } from './companies.types';
|
||||||
} from './companies.types';
|
|
||||||
|
|
||||||
|
|
||||||
export class CompanyService {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener todas las empresas con paginación y filtros opcionales
|
* Servicio para obtener todas las empresas (tenants) con paginación.
|
||||||
*/
|
*/
|
||||||
async getAll(params?: CompanyQueryParams): Promise<PaginatedResponse<Company> | Company[]> {
|
export class CompaniesService {
|
||||||
|
/**
|
||||||
|
* Obtiene todas las empresas del sistema.
|
||||||
|
* @returns Promesa con la respuesta paginada de empresas
|
||||||
|
*/
|
||||||
|
public async getAll(page = 1): Promise<CompaniesPagination> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('api/catalogs/companies', { params });
|
const response = await api.get('/api/admin/admin/tenants', {
|
||||||
|
params: { page },
|
||||||
// Si la respuesta tiene paginación, devolver el objeto completo
|
});
|
||||||
if (response.data.current_page !== undefined) {
|
return response.data;
|
||||||
return response.data as PaginatedResponse<Company>;
|
} catch (error) {
|
||||||
}
|
|
||||||
|
|
||||||
// Si no tiene paginación, devolver solo el array de data
|
|
||||||
return response.data.data as Company[];
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error al obtener empresas:', error);
|
console.error('Error al obtener empresas:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener una empresa por ID
|
* Crea una empresa (tenant) usando multipart/form-data.
|
||||||
*/
|
*/
|
||||||
async getById(id: number): Promise<Company> {
|
public async create(data: TenantFormData): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`api/catalogs/companies/${id}`);
|
const payload = buildCreateTenantPayload(data);
|
||||||
return response.data;
|
const formData = tenantPayloadToFormData(payload);
|
||||||
} catch (error: any) {
|
await api.post('/api/admin/admin/tenants', formData, {
|
||||||
console.error(`Error al obtener empresa con ID ${id}:`, error);
|
headers: {
|
||||||
throw error;
|
'Content-Type': 'multipart/form-data',
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crear una nueva empresa
|
|
||||||
*/
|
|
||||||
async create(data: CompanyFormData): Promise<CompanyCreateResponse> {
|
|
||||||
try {
|
|
||||||
const response = await api.post('api/catalogs/companies', data);
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error al crear empresa:', error);
|
|
||||||
// Mejorar el mensaje de error si viene del backend
|
|
||||||
if (error.response?.data) {
|
|
||||||
throw {
|
|
||||||
...error,
|
|
||||||
message: error.response.data.message || error.response.data.error || 'Error al crear la empresa'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actualizar una empresa existente
|
|
||||||
*/
|
|
||||||
async update(id: number, data: Partial<CompanyFormData>): Promise<Company> {
|
|
||||||
try {
|
|
||||||
const response = await api.put(`api/catalogs/companies/${id}`, data);
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error al actualizar empresa con ID ${id}:`, error);
|
|
||||||
// Mejorar el mensaje de error si viene del backend
|
|
||||||
if (error.response?.data) {
|
|
||||||
throw {
|
|
||||||
...error,
|
|
||||||
message: error.response.data.message || error.response.data.error || 'Error al actualizar la empresa'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Eliminar una empresa
|
|
||||||
*/
|
|
||||||
async delete(id: number): Promise<void> {
|
|
||||||
try {
|
|
||||||
await api.delete(`api/catalogs/companies/${id}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error al eliminar empresa con ID ${id}:`, error);
|
|
||||||
// Mejorar el mensaje de error si viene del backend
|
|
||||||
if (error.response?.data) {
|
|
||||||
throw {
|
|
||||||
...error,
|
|
||||||
message: error.response.data.message || error.response.data.error || 'Error al eliminar la empresa'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar empresas por múltiples criterios
|
|
||||||
*/
|
|
||||||
async search(params: CompanyQueryParams): Promise<Company[]> {
|
|
||||||
try {
|
|
||||||
const response = await api.get('api/catalogs/companies', {
|
|
||||||
params: { ...params, paginate: false }
|
|
||||||
});
|
});
|
||||||
return response.data.data;
|
} catch (error) {
|
||||||
} catch (error: any) {
|
console.error('Error al crear empresa:', error);
|
||||||
console.error('Error al buscar empresas:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
/**
|
||||||
|
* Actualiza una empresa (tenant) usando POST + _method=PUT para multipart.
|
||||||
|
*/
|
||||||
|
public async update(id: number, data: TenantFormData): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payload = buildUpdateTenantPayload(data);
|
||||||
|
const formData = tenantPayloadToFormData(payload, { forcePut: true });
|
||||||
|
await api.post(`/api/admin/admin/tenants/${id}`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al actualizar empresa:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exportar una instancia única del servicio
|
/**
|
||||||
export const companyService = new CompanyService();
|
* Elimina una empresa por su ID.
|
||||||
|
* @param id ID de la empresa
|
||||||
|
*/
|
||||||
|
public async delete(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/admin/admin/tenants/${id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar empresa:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const companiesService = new CompaniesService();
|
||||||
|
|||||||
@ -1,72 +1,71 @@
|
|||||||
export interface CompanyAddress {
|
export interface Company {
|
||||||
country: string;
|
id: number;
|
||||||
postal_code: string;
|
rfc: string;
|
||||||
state: string;
|
curp?: string | null;
|
||||||
municipality: string;
|
vat_rate?: number | null;
|
||||||
city: string;
|
isr_withholding?: number | null;
|
||||||
street: string;
|
vat_withholding?: number | null;
|
||||||
num_ext: string;
|
additional_tax?: number | null;
|
||||||
num_int?: string;
|
company_name: string;
|
||||||
|
email?: string;
|
||||||
|
primary_domain?: string;
|
||||||
|
phone?: string | null;
|
||||||
|
legal_representative?: string | null;
|
||||||
|
tax_regime?: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at?: string | null;
|
||||||
|
data?: Record<string, unknown> | null;
|
||||||
|
is_active: boolean;
|
||||||
|
trial_ends_at?: string | null;
|
||||||
|
subscribed_until?: string | null;
|
||||||
|
domains_count: number;
|
||||||
|
domains: CompanyDomain[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Company {
|
export interface CompanyDomain {
|
||||||
|
id?: number;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantFormData {
|
||||||
id?: number;
|
id?: number;
|
||||||
rfc: string;
|
|
||||||
curp?: string;
|
|
||||||
company_name: string;
|
company_name: string;
|
||||||
|
rfc: string;
|
||||||
|
email: string;
|
||||||
|
primary_domain: string;
|
||||||
|
curp: string;
|
||||||
|
phone: string;
|
||||||
|
legal_representative: string;
|
||||||
|
tax_regime: string;
|
||||||
|
is_active: boolean;
|
||||||
vat_rate: number;
|
vat_rate: number;
|
||||||
isr_withholding: number;
|
isr_withholding: number;
|
||||||
vat_withholding: number;
|
vat_withholding: number;
|
||||||
additional_tax: number;
|
additional_tax: number;
|
||||||
address: CompanyAddress;
|
certificate_password: string;
|
||||||
created_at?: string;
|
certificate_cer_file: File | null;
|
||||||
updated_at?: string;
|
certificate_key_file: File | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyCreateResponse{
|
export type CreateTenantPayload = Omit<TenantFormData, 'id'>;
|
||||||
data: Company;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompanyFormData extends Omit<Company, 'id' | 'created_at' | 'updated_at'> {}
|
export type UpdateTenantPayload = Omit<TenantFormData, 'id' | 'primary_domain'> & {
|
||||||
|
primary_domain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface CompanyStats {
|
export interface CompaniesPagination {
|
||||||
total: number;
|
|
||||||
active: number;
|
|
||||||
pending: number;
|
|
||||||
monthlyIncrease: number;
|
|
||||||
activePercentage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationLink {
|
|
||||||
url: string | null;
|
|
||||||
label: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
|
||||||
current_page: number;
|
current_page: number;
|
||||||
data: T[];
|
data: Company[];
|
||||||
first_page_url: string;
|
first_page_url: string;
|
||||||
from: number;
|
from: number;
|
||||||
last_page: number;
|
last_page: number;
|
||||||
last_page_url: string;
|
last_page_url: string;
|
||||||
links: PaginationLink[];
|
links: Array<{ url: string | null; label: string; active: boolean }>;
|
||||||
next_page_url: string | null;
|
next_page_url?: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
per_page: number;
|
per_page: number;
|
||||||
prev_page_url: string | null;
|
prev_page_url?: string | null;
|
||||||
to: number;
|
to: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyListResponse {
|
|
||||||
data: Company[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompanyQueryParams {
|
|
||||||
paginate?: boolean;
|
|
||||||
rfc?: string;
|
|
||||||
curp?: string;
|
|
||||||
company_name?: string;
|
|
||||||
page?: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
@ -12,14 +12,18 @@ import Tag from 'primevue/tag';
|
|||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import ConfirmDialog from 'primevue/confirmdialog';
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Select from 'primevue/select';
|
||||||
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
|
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
|
||||||
import UnitsForm from './UnitsForm.vue';
|
import UnitsForm from './UnitsForm.vue';
|
||||||
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
|
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const unitStore = useUnitOfMeasureStore();
|
const unitStore = useUnitOfMeasureStore();
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
|
||||||
// Breadcrumb
|
// Breadcrumb
|
||||||
const breadcrumbItems = ref([
|
const breadcrumbItems = ref([
|
||||||
@ -36,62 +40,176 @@ const home = ref({
|
|||||||
const showDialog = ref(false);
|
const showDialog = ref(false);
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
const selectedUnit = ref<UnitOfMeasure | null>(null);
|
const selectedUnit = ref<UnitOfMeasure | null>(null);
|
||||||
|
const unitsFormRef = ref<InstanceType<typeof UnitsForm> | null>(null);
|
||||||
|
const search = ref('');
|
||||||
|
const statusFilter = ref<'all' | 'active' | 'inactive'>('all');
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const units = computed(() => unitStore.units);
|
const units = computed(() => unitStore.units);
|
||||||
const loading = computed(() => unitStore.loading);
|
const loading = computed(() => unitStore.loading);
|
||||||
|
const statusFilterOptions = [
|
||||||
|
{ label: 'Todas', value: 'all' },
|
||||||
|
{ label: 'Activas', value: 'active' },
|
||||||
|
{ label: 'Inactivas', value: 'inactive' },
|
||||||
|
];
|
||||||
|
|
||||||
const getStatusConfig = (isActive: number) => {
|
const getStatusConfig = (isActive: boolean) => {
|
||||||
return isActive === 1
|
return isActive
|
||||||
? { label: 'Activa', severity: 'success' }
|
? { label: 'Activa', severity: 'success' }
|
||||||
: { label: 'Inactiva', severity: 'secondary' };
|
: { label: 'Inactiva', severity: 'secondary' };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canViewUnits = computed(() =>
|
||||||
|
hasPermission([
|
||||||
|
'units-of-measure.index',
|
||||||
|
'units-of-measure.show',
|
||||||
|
'units-of-measure.store',
|
||||||
|
'units-of-measure.update',
|
||||||
|
'units-of-measure.destroy',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const canCreateUnit = computed(() => hasPermission('units-of-measure.store'));
|
||||||
|
const canUpdateUnit = computed(() => hasPermission('units-of-measure.update'));
|
||||||
|
const canDeleteUnit = computed(() => hasPermission('units-of-measure.destroy'));
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
const buildFilters = () => ({
|
||||||
|
paginate: true,
|
||||||
|
per_page: 25,
|
||||||
|
search: search.value.trim(),
|
||||||
|
is_active: statusFilter.value === 'all' ? undefined : statusFilter.value === 'active',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadUnits = async (force = false) => {
|
||||||
|
if (!canViewUnits.value) {
|
||||||
|
unitStore.clearUnits();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await unitStore.fetchUnits({
|
||||||
|
force,
|
||||||
|
...buildFilters(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
|
if (!canCreateUnit.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permisos para crear unidades de medida.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
selectedUnit.value = null;
|
selectedUnit.value = null;
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditDialog = (unit: UnitOfMeasure) => {
|
const openEditDialog = (unit: UnitOfMeasure) => {
|
||||||
|
if (!canUpdateUnit.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permisos para actualizar unidades de medida.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
isEditing.value = true;
|
isEditing.value = true;
|
||||||
selectedUnit.value = unit;
|
selectedUnit.value = unit;
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveUnit = async (data: CreateUnitOfMeasureData) => {
|
const handleCreateUnit = async (data: CreateUnitOfMeasureData) => {
|
||||||
try {
|
|
||||||
if (isEditing.value && selectedUnit.value) {
|
|
||||||
await unitStore.updateUnit(selectedUnit.value.id, data);
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Actualización Exitosa',
|
|
||||||
detail: 'La unidad de medida ha sido actualizada correctamente.',
|
|
||||||
life: 3000
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await unitStore.createUnit(data);
|
await unitStore.createUnit(data);
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Creación Exitosa',
|
summary: 'Creación Exitosa',
|
||||||
detail: 'La unidad de medida ha sido creada correctamente.',
|
detail: 'La unidad de medida ha sido creada correctamente.',
|
||||||
life: 3000
|
life: 3000,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateUnit = async (id: number, data: CreateUnitOfMeasureData) => {
|
||||||
|
await unitStore.updateUnit(id, data);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Actualización Exitosa',
|
||||||
|
detail: 'La unidad de medida ha sido actualizada correctamente.',
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveUnit = async (data: CreateUnitOfMeasureData) => {
|
||||||
|
try {
|
||||||
|
if (isEditing.value && selectedUnit.value) {
|
||||||
|
if (!canUpdateUnit.value) {
|
||||||
|
throw new Error('without-update-permission');
|
||||||
}
|
}
|
||||||
|
await handleUpdateUnit(selectedUnit.value.id, data);
|
||||||
|
} else {
|
||||||
|
if (!canCreateUnit.value) {
|
||||||
|
throw new Error('without-create-permission');
|
||||||
|
}
|
||||||
|
await handleCreateUnit(data);
|
||||||
|
}
|
||||||
|
|
||||||
showDialog.value = false;
|
showDialog.value = false;
|
||||||
|
selectedUnit.value = null;
|
||||||
|
unitsFormRef.value?.resetSubmitting();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving unit:', error);
|
console.error('Error saving unit:', error);
|
||||||
|
|
||||||
|
const requestError = error as {
|
||||||
|
message?: string;
|
||||||
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
data?: {
|
||||||
|
errors?: Record<string, string[]>;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requestError.response?.status === 422 && requestError.response.data?.errors) {
|
||||||
|
unitsFormRef.value?.setValidationErrors(requestError.response.data.errors);
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Errores de validación',
|
||||||
|
detail: 'Corrige los campos marcados para continuar.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unitsFormRef.value?.resetSubmitting();
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: 'Error',
|
summary: 'Error',
|
||||||
detail: 'No se pudo guardar la unidad de medida. Por favor, intenta nuevamente.',
|
detail:
|
||||||
life: 3000
|
requestError.response?.data?.message ||
|
||||||
|
(requestError.message === 'without-update-permission'
|
||||||
|
? 'No tienes permisos para actualizar unidades de medida.'
|
||||||
|
: requestError.message === 'without-create-permission'
|
||||||
|
? 'No tienes permisos para crear unidades de medida.'
|
||||||
|
: 'No se pudo guardar la unidad de medida. Por favor, intenta nuevamente.'),
|
||||||
|
life: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = (unit: UnitOfMeasure) => {
|
const confirmDelete = (unit: UnitOfMeasure) => {
|
||||||
|
if (!canDeleteUnit.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permisos para eliminar unidades de medida.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
confirm.require({
|
confirm.require({
|
||||||
message: `¿Estás seguro de eliminar la unidad de medida "${unit.name}"?`,
|
message: `¿Estás seguro de eliminar la unidad de medida "${unit.name}"?`,
|
||||||
header: 'Confirmar Eliminación',
|
header: 'Confirmar Eliminación',
|
||||||
@ -104,13 +222,23 @@ const confirmDelete = (unit: UnitOfMeasure) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteUnit = async (id: number) => {
|
const deleteUnit = async (id: number) => {
|
||||||
|
if (!canDeleteUnit.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Sin permisos',
|
||||||
|
detail: 'No tienes permisos para eliminar unidades de medida.',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await unitStore.deleteUnit(id);
|
await unitStore.deleteUnit(id);
|
||||||
|
await loadUnits(true);
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Eliminación Exitosa',
|
summary: 'Eliminación Exitosa',
|
||||||
detail: 'La unidad de medida ha sido eliminada correctamente.',
|
detail: 'La unidad de medida ha sido eliminada correctamente.',
|
||||||
life: 3000
|
life: 3000,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting unit:', error);
|
console.error('Error deleting unit:', error);
|
||||||
@ -118,15 +246,32 @@ const deleteUnit = async (id: number) => {
|
|||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: 'Error',
|
summary: 'Error',
|
||||||
detail: 'No se pudo eliminar la unidad de medida. Puede estar en uso.',
|
detail: 'No se pudo eliminar la unidad de medida. Puede estar en uso.',
|
||||||
life: 3000
|
life: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lifecycle
|
const applyFilters = async () => {
|
||||||
onMounted(async () => {
|
await loadUnits(true);
|
||||||
await unitStore.fetchUnits();
|
};
|
||||||
});
|
|
||||||
|
const clearFilters = async () => {
|
||||||
|
search.value = '';
|
||||||
|
statusFilter.value = 'all';
|
||||||
|
await loadUnits(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => canViewUnits.value,
|
||||||
|
async (allowed) => {
|
||||||
|
if (allowed) {
|
||||||
|
await loadUnits();
|
||||||
|
} else {
|
||||||
|
unitStore.clearUnits();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -159,6 +304,7 @@ onMounted(async () => {
|
|||||||
Unidades de Medida
|
Unidades de Medida
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canCreateUnit"
|
||||||
label="Nueva Unidad"
|
label="Nueva Unidad"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
@ -167,8 +313,44 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Card -->
|
<!-- Main Card -->
|
||||||
<Card>
|
<Card v-if="canViewUnits">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
<div class="flex flex-col md:flex-row md:items-end gap-3 mb-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="unit-search" class="block text-sm font-medium mb-2">Buscar</label>
|
||||||
|
<InputText
|
||||||
|
id="unit-search"
|
||||||
|
v-model="search"
|
||||||
|
class="w-full"
|
||||||
|
placeholder="Buscar por nombre o abreviatura"
|
||||||
|
@keyup.enter="applyFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full md:w-64">
|
||||||
|
<label for="status-filter" class="block text-sm font-medium mb-2">Estado</label>
|
||||||
|
<Select
|
||||||
|
id="status-filter"
|
||||||
|
v-model="statusFilter"
|
||||||
|
class="w-full"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
:options="statusFilterOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button label="Filtrar" icon="pi pi-search" @click="applyFilters" />
|
||||||
|
<Button
|
||||||
|
label="Limpiar"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
@click="clearFilters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="loading && units.length === 0" class="flex justify-center items-center py-12">
|
<div v-if="loading && units.length === 0" class="flex justify-center items-center py-12">
|
||||||
<ProgressSpinner
|
<ProgressSpinner
|
||||||
@ -236,6 +418,7 @@ onMounted(async () => {
|
|||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canUpdateUnit"
|
||||||
icon="pi pi-pencil"
|
icon="pi pi-pencil"
|
||||||
text
|
text
|
||||||
rounded
|
rounded
|
||||||
@ -244,6 +427,7 @@ onMounted(async () => {
|
|||||||
v-tooltip.top="'Editar'"
|
v-tooltip.top="'Editar'"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="canDeleteUnit"
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
text
|
text
|
||||||
rounded
|
rounded
|
||||||
@ -264,8 +448,18 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card v-else>
|
||||||
|
<template #content>
|
||||||
|
<div class="text-center py-10 text-surface-500 dark:text-surface-400">
|
||||||
|
<i class="pi pi-lock text-4xl mb-3"></i>
|
||||||
|
<p>No tienes permisos para visualizar este módulo.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- Create/Edit Form Dialog -->
|
<!-- Create/Edit Form Dialog -->
|
||||||
<UnitsForm
|
<UnitsForm
|
||||||
|
ref="unitsFormRef"
|
||||||
v-model:visible="showDialog"
|
v-model:visible="showDialog"
|
||||||
:unit="selectedUnit"
|
:unit="selectedUnit"
|
||||||
:is-editing="isEditing"
|
:is-editing="isEditing"
|
||||||
|
|||||||
@ -31,12 +31,14 @@ const emit = defineEmits<Emits>();
|
|||||||
const formData = ref<CreateUnitOfMeasureData>({
|
const formData = ref<CreateUnitOfMeasureData>({
|
||||||
name: '',
|
name: '',
|
||||||
abbreviation: '',
|
abbreviation: '',
|
||||||
code_sat: 1,
|
code_sat: null,
|
||||||
is_active: 1
|
is_active: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Estado interno del switch (boolean para el UI)
|
// Estado interno del switch (boolean para el UI)
|
||||||
const isActiveSwitch = ref(true);
|
const isActiveSwitch = ref(true);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errors = ref<Record<string, string>>({});
|
||||||
|
|
||||||
// SAT Units
|
// SAT Units
|
||||||
const satUnits = ref<SatUnit[]>([]);
|
const satUnits = ref<SatUnit[]>([]);
|
||||||
@ -58,7 +60,7 @@ const dialogTitle = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return !!(formData.value.name && formData.value.abbreviation);
|
return !!formData.value.name?.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyMessage = computed(() => {
|
const emptyMessage = computed(() => {
|
||||||
@ -99,7 +101,7 @@ const loadSatUnits = async (search: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search
|
||||||
const handleSearchChange = (event: any) => {
|
const handleSearchChange = (event: { value?: string }) => {
|
||||||
const query = event.value || '';
|
const query = event.value || '';
|
||||||
searchQuery.value = query;
|
searchQuery.value = query;
|
||||||
|
|
||||||
@ -120,9 +122,11 @@ const resetForm = () => {
|
|||||||
name: '',
|
name: '',
|
||||||
abbreviation: '',
|
abbreviation: '',
|
||||||
code_sat: null,
|
code_sat: null,
|
||||||
is_active: 1
|
is_active: true
|
||||||
};
|
};
|
||||||
isActiveSwitch.value = true;
|
isActiveSwitch.value = true;
|
||||||
|
errors.value = {};
|
||||||
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch para actualizar el formulario cuando cambie la unidad
|
// Watch para actualizar el formulario cuando cambie la unidad
|
||||||
@ -134,7 +138,7 @@ watch(() => props.unit, (newUnit) => {
|
|||||||
code_sat: newUnit.code_sat,
|
code_sat: newUnit.code_sat,
|
||||||
is_active: newUnit.is_active
|
is_active: newUnit.is_active
|
||||||
};
|
};
|
||||||
isActiveSwitch.value = newUnit.is_active === 1;
|
isActiveSwitch.value = newUnit.is_active;
|
||||||
|
|
||||||
// Precargar la unidad SAT actual en el dropdown
|
// Precargar la unidad SAT actual en el dropdown
|
||||||
if (newUnit.sat_unit) {
|
if (newUnit.sat_unit) {
|
||||||
@ -152,13 +156,57 @@ const handleClose = () => {
|
|||||||
resetForm();
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const validateForm = (): boolean => {
|
||||||
// Convertir el switch boolean a number para el backend
|
errors.value = {};
|
||||||
formData.value.is_active = isActiveSwitch.value ? 1 : 0;
|
|
||||||
|
|
||||||
emit('save', { ...formData.value });
|
if (!formData.value.name?.trim()) {
|
||||||
handleClose();
|
errors.value.name = 'El nombre es obligatorio';
|
||||||
|
} else if (formData.value.name.trim().length > 50) {
|
||||||
|
errors.value.name = 'El nombre no puede exceder 50 caracteres';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.value.abbreviation && formData.value.abbreviation.trim().length > 10) {
|
||||||
|
errors.value.abbreviation = 'La abreviatura no puede exceder 10 caracteres';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(errors.value).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
formData.value.is_active = isActiveSwitch.value;
|
||||||
|
|
||||||
|
emit('save', {
|
||||||
|
...formData.value,
|
||||||
|
name: formData.value.name.trim(),
|
||||||
|
abbreviation: formData.value.abbreviation?.trim() || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValidationErrors = (backendErrors: Record<string, string[]>) => {
|
||||||
|
loading.value = false;
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
Object.keys(backendErrors).forEach((key) => {
|
||||||
|
const firstError = backendErrors[key]?.[0];
|
||||||
|
if (firstError) {
|
||||||
|
errors.value[key] = firstError;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetSubmitting = () => {
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setValidationErrors,
|
||||||
|
resetSubmitting,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -183,7 +231,9 @@ const handleSave = () => {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Ej: PIEZA, KILOGRAMO, METRO"
|
placeholder="Ej: PIEZA, KILOGRAMO, METRO"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
:class="{ 'p-invalid': errors.name }"
|
||||||
/>
|
/>
|
||||||
|
<small v-if="errors.name" class="p-error">{{ errors.name }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Abbreviation -->
|
<!-- Abbreviation -->
|
||||||
@ -196,8 +246,9 @@ const handleSave = () => {
|
|||||||
v-model="formData.abbreviation"
|
v-model="formData.abbreviation"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Ej: PZA, kg, m"
|
placeholder="Ej: PZA, kg, m"
|
||||||
:required="true"
|
:class="{ 'p-invalid': errors.abbreviation }"
|
||||||
/>
|
/>
|
||||||
|
<small v-if="errors.abbreviation" class="p-error">{{ errors.abbreviation }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Code SAT -->
|
<!-- Code SAT -->
|
||||||
@ -216,10 +267,12 @@ const handleSave = () => {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
:filter="true"
|
:filter="true"
|
||||||
filterPlaceholder="Buscar unidad SAT"
|
filterPlaceholder="Buscar unidad SAT"
|
||||||
:showClear="false"
|
:showClear="true"
|
||||||
@filter="handleSearchChange"
|
@filter="handleSearchChange"
|
||||||
:emptyFilterMessage="emptyMessage"
|
:emptyFilterMessage="emptyMessage"
|
||||||
|
:class="{ 'p-invalid': errors.code_sat }"
|
||||||
/>
|
/>
|
||||||
|
<small v-if="errors.code_sat" class="p-error">{{ errors.code_sat }}</small>
|
||||||
<small class="text-surface-500 dark:text-surface-400">
|
<small class="text-surface-500 dark:text-surface-400">
|
||||||
Unidad del catálogo SAT
|
Unidad del catálogo SAT
|
||||||
</small>
|
</small>
|
||||||
@ -253,6 +306,7 @@ const handleSave = () => {
|
|||||||
<Button
|
<Button
|
||||||
:label="isEditing ? 'Actualizar' : 'Crear'"
|
:label="isEditing ? 'Actualizar' : 'Crear'"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
|
:loading="loading"
|
||||||
@click="handleSave"
|
@click="handleSave"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,7 +11,14 @@ export const satUnitsService = {
|
|||||||
* Get all SAT units with search filter
|
* Get all SAT units with search filter
|
||||||
*/
|
*/
|
||||||
async getSatUnits(search: string = ''): Promise<SatUnitsResponse> {
|
async getSatUnits(search: string = ''): Promise<SatUnitsResponse> {
|
||||||
const response = await api.get(`/api/sat/units?search=${search}`);
|
try {
|
||||||
|
const response = await api.get('/api/sat/units', {
|
||||||
|
params: { search },
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al obtener unidades SAT:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
40
src/modules/catalog/services/unit-measure.mapper.ts
Normal file
40
src/modules/catalog/services/unit-measure.mapper.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type {
|
||||||
|
CreateUnitOfMeasureData,
|
||||||
|
UnitOfMeasure,
|
||||||
|
UnitOfMeasureApi,
|
||||||
|
UpdateUnitOfMeasureData,
|
||||||
|
} from '../types/unit-measure.interfaces';
|
||||||
|
|
||||||
|
const toBoolean = (value: boolean | number | undefined): boolean => {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapUnitFromApi = (apiUnit: UnitOfMeasureApi): UnitOfMeasure => ({
|
||||||
|
id: apiUnit.id,
|
||||||
|
name: apiUnit.name,
|
||||||
|
abbreviation: apiUnit.abbreviation,
|
||||||
|
is_active: toBoolean(apiUnit.is_active),
|
||||||
|
created_at: apiUnit.created_at,
|
||||||
|
updated_at: apiUnit.updated_at,
|
||||||
|
deleted_at: apiUnit.deleted_at,
|
||||||
|
code_sat: apiUnit.code_sat,
|
||||||
|
sat_unit: apiUnit.sat_unit,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapCreatePayload = (data: CreateUnitOfMeasureData): CreateUnitOfMeasureData => ({
|
||||||
|
name: data.name.trim(),
|
||||||
|
abbreviation: data.abbreviation?.trim() || undefined,
|
||||||
|
code_sat: data.code_sat ?? null,
|
||||||
|
is_active: data.is_active ?? true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mapUpdatePayload = (data: UpdateUnitOfMeasureData): UpdateUnitOfMeasureData => ({
|
||||||
|
name: data.name?.trim(),
|
||||||
|
abbreviation: data.abbreviation?.trim() || undefined,
|
||||||
|
code_sat: data.code_sat ?? null,
|
||||||
|
is_active: data.is_active,
|
||||||
|
});
|
||||||
@ -1,52 +1,124 @@
|
|||||||
import api from '../../../services/api';
|
import api from '../../../services/api';
|
||||||
import type {
|
import type {
|
||||||
|
UnitOfMeasureApi,
|
||||||
|
UnitOfMeasurePaginatedApiResponse,
|
||||||
|
UnitOfMeasureUnpaginatedApiResponse,
|
||||||
UnitOfMeasurePaginatedResponse,
|
UnitOfMeasurePaginatedResponse,
|
||||||
UnitOfMeasureUnpaginatedResponse,
|
UnitOfMeasureUnpaginatedResponse,
|
||||||
CreateUnitOfMeasureData,
|
CreateUnitOfMeasureData,
|
||||||
UpdateUnitOfMeasureData,
|
UpdateUnitOfMeasureData,
|
||||||
SingleUnitOfMeasureResponse,
|
SingleUnitOfMeasureResponse,
|
||||||
UnitOfMeasureResponseById
|
UnitOfMeasureResponseById,
|
||||||
|
UnitOfMeasureQueryParams
|
||||||
} from '../types/unit-measure.interfaces';
|
} from '../types/unit-measure.interfaces';
|
||||||
|
import { mapCreatePayload, mapUnitFromApi, mapUpdatePayload } from './unit-measure.mapper';
|
||||||
|
|
||||||
|
const mapPaginatedResponse = (
|
||||||
|
response: UnitOfMeasurePaginatedApiResponse
|
||||||
|
): UnitOfMeasurePaginatedResponse => ({
|
||||||
|
...response,
|
||||||
|
data: response.data.map(mapUnitFromApi),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapUnpaginatedResponse = (
|
||||||
|
response: UnitOfMeasureUnpaginatedApiResponse
|
||||||
|
): UnitOfMeasureUnpaginatedResponse => ({
|
||||||
|
data: response.data.map(mapUnitFromApi),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapSingleResponse = (response: { message: string; data: UnitOfMeasureApi }): SingleUnitOfMeasureResponse => ({
|
||||||
|
message: response.message,
|
||||||
|
data: mapUnitFromApi(response.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export const unitOfMeasureService = {
|
export const unitOfMeasureService = {
|
||||||
/**
|
/**
|
||||||
* Get all units of measure with optional pagination and search
|
* Get all units of measure with optional pagination and search
|
||||||
*/
|
*/
|
||||||
async getUnits(paginate: boolean = true, search: string = ''): Promise<UnitOfMeasurePaginatedResponse | UnitOfMeasureUnpaginatedResponse> {
|
async getUnits(params: UnitOfMeasureQueryParams = {}): Promise<UnitOfMeasurePaginatedResponse | UnitOfMeasureUnpaginatedResponse> {
|
||||||
const response = await api.get(`/api/catalogs/units-of-measure?paginate=${paginate}&search=${search}`);
|
try {
|
||||||
return response.data;
|
const {
|
||||||
|
search = '',
|
||||||
|
is_active,
|
||||||
|
paginate = true,
|
||||||
|
per_page,
|
||||||
|
page,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const response = await api.get('/api/catalogs/units-of-measure', {
|
||||||
|
params: {
|
||||||
|
search,
|
||||||
|
is_active,
|
||||||
|
paginate,
|
||||||
|
per_page,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('current_page' in response.data) {
|
||||||
|
return mapPaginatedResponse(response.data as UnitOfMeasurePaginatedApiResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapUnpaginatedResponse(response.data as UnitOfMeasureUnpaginatedApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al obtener unidades de medida:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single unit of measure by ID
|
* Get a single unit of measure by ID
|
||||||
*/
|
*/
|
||||||
async getUnitById(id: number): Promise<UnitOfMeasureResponseById> {
|
async getUnitById(id: number): Promise<UnitOfMeasureResponseById> {
|
||||||
|
try {
|
||||||
const response = await api.get(`/api/catalogs/units-of-measure/${id}`);
|
const response = await api.get(`/api/catalogs/units-of-measure/${id}`);
|
||||||
return response.data;
|
return {
|
||||||
|
data: mapUnitFromApi((response.data as { data: UnitOfMeasureApi }).data),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error al obtener unidad de medida con id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new unit of measure
|
* Create a new unit of measure
|
||||||
*/
|
*/
|
||||||
async createUnit(data: CreateUnitOfMeasureData): Promise<SingleUnitOfMeasureResponse> {
|
async createUnit(data: CreateUnitOfMeasureData): Promise<SingleUnitOfMeasureResponse> {
|
||||||
const response = await api.post(`/api/catalogs/units-of-measure`, data);
|
try {
|
||||||
console.log('Create Unit response:', response);
|
const payload = mapCreatePayload(data);
|
||||||
return response.data;
|
const response = await api.post('/api/catalogs/units-of-measure', payload);
|
||||||
|
return mapSingleResponse(response.data as { message: string; data: UnitOfMeasureApi });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear unidad de medida:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing unit of measure
|
* Update an existing unit of measure
|
||||||
*/
|
*/
|
||||||
async updateUnit(id: number, data: UpdateUnitOfMeasureData): Promise<SingleUnitOfMeasureResponse> {
|
async updateUnit(id: number, data: UpdateUnitOfMeasureData): Promise<SingleUnitOfMeasureResponse> {
|
||||||
const response = await api.put(`/api/catalogs/units-of-measure/${id}`, data);
|
try {
|
||||||
return response.data;
|
const payload = mapUpdatePayload(data);
|
||||||
|
const response = await api.put(`/api/catalogs/units-of-measure/${id}`, payload);
|
||||||
|
return mapSingleResponse(response.data as { message: string; data: UnitOfMeasureApi });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error al actualizar unidad de medida con id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a unit of measure
|
* Delete a unit of measure
|
||||||
*/
|
*/
|
||||||
async deleteUnit(id: number): Promise<void> {
|
async deleteUnit(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
await api.delete(`/api/catalogs/units-of-measure/${id}`);
|
await api.delete(`/api/catalogs/units-of-measure/${id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error al eliminar unidad de medida con id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,23 +4,40 @@ import { unitOfMeasureService } from '../services/unit-measure.services';
|
|||||||
import type {
|
import type {
|
||||||
UnitOfMeasure,
|
UnitOfMeasure,
|
||||||
CreateUnitOfMeasureData,
|
CreateUnitOfMeasureData,
|
||||||
UpdateUnitOfMeasureData
|
UpdateUnitOfMeasureData,
|
||||||
|
UnitOfMeasureQueryParams,
|
||||||
|
UnitOfMeasurePaginatedResponse,
|
||||||
|
UnitOfMeasureUnpaginatedResponse,
|
||||||
} from '../types/unit-measure.interfaces';
|
} from '../types/unit-measure.interfaces';
|
||||||
|
|
||||||
|
interface FetchUnitsOptions extends UnitOfMeasureQueryParams {
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPaginatedResponse = (
|
||||||
|
response: UnitOfMeasurePaginatedResponse | UnitOfMeasureUnpaginatedResponse
|
||||||
|
): response is UnitOfMeasurePaginatedResponse => 'current_page' in response;
|
||||||
|
|
||||||
export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
||||||
// State
|
// State
|
||||||
const units = ref<UnitOfMeasure[]>([]);
|
const units = ref<UnitOfMeasure[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loaded = ref(false);
|
const loaded = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
const lastFetchOptions = ref<UnitOfMeasureQueryParams>({
|
||||||
|
paginate: true,
|
||||||
|
search: '',
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
});
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const activeUnits = computed(() =>
|
const activeUnits = computed(() =>
|
||||||
units.value.filter(unit => unit.is_active === 1)
|
units.value.filter((unit) => unit.is_active)
|
||||||
);
|
);
|
||||||
|
|
||||||
const inactiveUnits = computed(() =>
|
const inactiveUnits = computed(() =>
|
||||||
units.value.filter(unit => unit.is_active === 0)
|
units.value.filter((unit) => !unit.is_active)
|
||||||
);
|
);
|
||||||
|
|
||||||
const unitCount = computed(() => units.value.length);
|
const unitCount = computed(() => units.value.length);
|
||||||
@ -35,31 +52,35 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const fetchUnits = async (force = false, paginate = true, search = '') => {
|
const fetchUnits = async (options: FetchUnitsOptions = {}) => {
|
||||||
// Si ya están cargados y no se fuerza la recarga, no hacer nada
|
const {
|
||||||
if (loaded.value && !force) {
|
force = false,
|
||||||
console.log('Units of measure already loaded from store');
|
...queryOptions
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (loaded.value && !force && units.value.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedOptions: UnitOfMeasureQueryParams = {
|
||||||
|
...lastFetchOptions.value,
|
||||||
|
...queryOptions,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
const response = await unitOfMeasureService.getUnits(paginate, search);
|
const response = await unitOfMeasureService.getUnits(normalizedOptions);
|
||||||
|
|
||||||
// Manejar respuesta paginada o no paginada
|
if (isPaginatedResponse(response)) {
|
||||||
if ('current_page' in response) {
|
|
||||||
// Respuesta paginada
|
|
||||||
units.value = response.data;
|
units.value = response.data;
|
||||||
} else {
|
} else {
|
||||||
// Respuesta no paginada
|
|
||||||
units.value = response.data;
|
units.value = response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
|
lastFetchOptions.value = normalizedOptions;
|
||||||
console.log('Units of measure loaded into store:', units.value.length);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Error loading units of measure';
|
error.value = err instanceof Error ? err.message : 'Error loading units of measure';
|
||||||
console.error('Error in unit of measure store:', err);
|
console.error('Error in unit of measure store:', err);
|
||||||
@ -69,8 +90,12 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshUnits = () => {
|
const refreshUnits = (overrides: UnitOfMeasureQueryParams = {}) => {
|
||||||
return fetchUnits(true);
|
return fetchUnits({
|
||||||
|
force: true,
|
||||||
|
...lastFetchOptions.value,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createUnit = async (data: CreateUnitOfMeasureData) => {
|
const createUnit = async (data: CreateUnitOfMeasureData) => {
|
||||||
@ -82,8 +107,6 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
|
|
||||||
// Refrescar la lista después de crear
|
// Refrescar la lista después de crear
|
||||||
await refreshUnits();
|
await refreshUnits();
|
||||||
|
|
||||||
console.log('Unit of measure created successfully');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Error creating unit of measure';
|
error.value = err instanceof Error ? err.message : 'Error creating unit of measure';
|
||||||
console.error('Error creating unit of measure:', err);
|
console.error('Error creating unit of measure:', err);
|
||||||
@ -102,8 +125,6 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
|
|
||||||
// Refrescar la lista después de actualizar
|
// Refrescar la lista después de actualizar
|
||||||
await refreshUnits();
|
await refreshUnits();
|
||||||
|
|
||||||
console.log('Unit of measure updated successfully');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Error updating unit of measure';
|
error.value = err instanceof Error ? err.message : 'Error updating unit of measure';
|
||||||
console.error('Error updating unit of measure:', err);
|
console.error('Error updating unit of measure:', err);
|
||||||
@ -122,8 +143,6 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
|
|
||||||
// Refrescar la lista después de eliminar
|
// Refrescar la lista después de eliminar
|
||||||
await refreshUnits();
|
await refreshUnits();
|
||||||
|
|
||||||
console.log('Unit of measure deleted successfully');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Error deleting unit of measure';
|
error.value = err instanceof Error ? err.message : 'Error deleting unit of measure';
|
||||||
console.error('Error deleting unit of measure:', err);
|
console.error('Error deleting unit of measure:', err);
|
||||||
@ -145,6 +164,7 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
|
|||||||
loading,
|
loading,
|
||||||
loaded,
|
loaded,
|
||||||
error,
|
error,
|
||||||
|
lastFetchOptions,
|
||||||
// Getters
|
// Getters
|
||||||
activeUnits,
|
activeUnits,
|
||||||
inactiveUnits,
|
inactiveUnits,
|
||||||
|
|||||||
@ -8,32 +8,50 @@ export interface SatUnit {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface para la Unidad de Medida
|
export interface UnitOfMeasureApi {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
abbreviation: string;
|
||||||
|
is_active: boolean | number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
code_sat: number | null;
|
||||||
|
sat_unit: SatUnit | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UnitOfMeasure {
|
export interface UnitOfMeasure {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
abbreviation: string;
|
abbreviation: string;
|
||||||
is_active: number;
|
is_active: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
deleted_at: string | null;
|
deleted_at: string | null;
|
||||||
code_sat: number;
|
code_sat: number | null;
|
||||||
sat_unit: SatUnit;
|
sat_unit: SatUnit | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interfaces para crear y actualizar unidades de medida
|
|
||||||
export interface CreateUnitOfMeasureData {
|
export interface CreateUnitOfMeasureData {
|
||||||
name: string;
|
name: string;
|
||||||
abbreviation: string;
|
abbreviation?: string;
|
||||||
code_sat: number | null;
|
code_sat?: number | null;
|
||||||
is_active: number;
|
is_active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateUnitOfMeasureData {
|
export interface UpdateUnitOfMeasureData {
|
||||||
name?: string;
|
name?: string;
|
||||||
abbreviation?: string;
|
abbreviation?: string;
|
||||||
code_sat?: number | null;
|
code_sat?: number | null;
|
||||||
is_active?: number;
|
is_active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitOfMeasureQueryParams {
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
paginate?: boolean;
|
||||||
|
per_page?: number;
|
||||||
|
page?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface para los links de paginación
|
// Interface para los links de paginación
|
||||||
@ -65,10 +83,12 @@ export interface UnpaginatedResponse<T> {
|
|||||||
data: T[];
|
data: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tipo específico para la respuesta paginada de Unidades de Medida
|
export type UnitOfMeasurePaginatedApiResponse = PaginatedResponse<UnitOfMeasureApi>;
|
||||||
|
|
||||||
|
export type UnitOfMeasureUnpaginatedApiResponse = UnpaginatedResponse<UnitOfMeasureApi>;
|
||||||
|
|
||||||
export type UnitOfMeasurePaginatedResponse = PaginatedResponse<UnitOfMeasure>;
|
export type UnitOfMeasurePaginatedResponse = PaginatedResponse<UnitOfMeasure>;
|
||||||
|
|
||||||
// Tipo específico para la respuesta no paginada de Unidades de Medida
|
|
||||||
export type UnitOfMeasureUnpaginatedResponse = UnpaginatedResponse<UnitOfMeasure>;
|
export type UnitOfMeasureUnpaginatedResponse = UnpaginatedResponse<UnitOfMeasure>;
|
||||||
|
|
||||||
export type UnitOfMeasureResponseById = {
|
export type UnitOfMeasureResponseById = {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { InternalAxiosRequestConfig } from 'axios';
|
import type { InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { AUTH_TOKEN_KEY, clearStoredAuthSession } from '../modules/auth/utils/authStorage';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
|
||||||
@ -12,7 +13,7 @@ const api = axios.create({
|
|||||||
// Interceptor para agregar el Bearer Token a cada petición
|
// Interceptor para agregar el Bearer Token a cada petición
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const token = localStorage.getItem('auth_token'); // Ajusta según dónde guardes el token
|
const token = localStorage.getItem(AUTH_TOKEN_KEY);
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
@ -28,14 +29,12 @@ api.interceptors.response.use(
|
|||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// El servidor respondió con un código de estado fuera del rango 2xx
|
|
||||||
console.error(error.response.data);
|
|
||||||
|
|
||||||
// Manejar error 401 (no autorizado) - redirigir al login
|
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
localStorage.removeItem('auth_token');
|
clearStoredAuthSession();
|
||||||
localStorage.removeItem('auth_user');
|
|
||||||
window.location.href = '/login';
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:unauthorized'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user