Merge pull request 'Ajustando menú por rol de usuario #29' (#32) from adjust_menu_by_rol_#29 into main

Reviewed-on: #32
This commit is contained in:
2026-03-14 23:02:00 -05:00
13 changed files with 215 additions and 149 deletions

View File

@@ -19,6 +19,11 @@ src/
├── plugins/ # Configuración de Vuetify, etc. ├── plugins/ # Configuración de Vuetify, etc.
├── router/ # Configuración de rutas ├── router/ # Configuración de rutas
├── services/ # API services (auth.js, etc.) ├── services/ # API services (auth.js, etc.)
│ ├── api.js # Clase wrapper que делегат methods
│ ├── api-implementation.js # Factory que selecciona implementación
│ ├── auth.js # Manejo de auth (login, tokens JWT)
│ ├── django-api.js # Implementación de API para Django
│ └── http.js # Axios instance con interceptors
├── stores/ # Pinia stores ├── stores/ # Pinia stores
└── styles/ # SCSS settings └── styles/ # SCSS settings
``` ```
@@ -51,6 +56,7 @@ import MiComponente from '@/components/MiComponente.vue';
- Ubicación: `src/services/` - Ubicación: `src/services/`
- Usar Axios para HTTP requests - Usar Axios para HTTP requests
- JWT tokens en localStorage (`access_token`, `refresh_token`) - JWT tokens en localStorage (`access_token`, `refresh_token`)
- La API se inyecta globalmente via `app.provide('api', api)` y se usa con `inject('api')`
### Routing ### Routing
- Rutas automáticas basadas en archivos en `src/pages/` - Rutas automáticas basadas en archivos en `src/pages/`
@@ -73,9 +79,36 @@ npm run lint # ESLint fix
## Git Commits ## Git Commits
**Antes de hacer commit:** **Antes de hacer commit:**
1. Pedir permiso al usuario 1. **SIEMPRE pedir permiso al usuario antes de hacer commit**
2. Mostrar resumen de los cambios que se incluirán 2. Mostrar resumen de los cambios que se incluirán
**Formato de mensajes:** **Formato de mensajes:**
- Usar prefijo `#<numero>` para referenciar el issue (ej: `#28 feat: add login` donde #28 es el número del issue en GitHub/GitLab) - Usar prefijo `#<numero>` para referenciar el issue (ej: `#28 feat: add login` donde #28 es el número del issue en GitHub/GitLab)
- Prefijos válidos: `feat`, `fix`, `chore`, `docs`, `refactor`, `style` - Prefijos válidos: `feat`, `fix`, `chore`, `docs`, `refactor`, `style`
## Análisis del Proyecto
### Flujo de Autenticación
1. **Login:** `AuthService.login(credentials)` → obtiene JWT tokens → guarda en localStorage
2. **Token:** Se envía en headers via interceptor en `http.js` (`Authorization: Bearer <token>`)
3. **Refresh:** El interceptor renueva automáticamente el token si expira (401)
4. **Logout:** `AuthService.logout()` → limpia localStorage
### Estructura de API
- `api.js`: Interfaz genérica con métodos como `getCustomers()`, `getProducts()`, etc.
- `api-implementation.js`: Factory que selecciona implementación (actualmente solo Django)
- `django-api.js`: Implementación concreta con endpoints de Django
### Componentes Principales
- **NavBar.vue**: Barra de navegación con menú de usuario
- **LoginDialog.vue**: Diálogo de inicio de sesión
- **Purchase.vue / AdminPurchase.vue**: Componentes de compra
- **Cart.vue**: Carrito de compras
- **SummaryPurchase.vue**: Resumen de compra
### Endpoints Django Comunes
- `/api/token/` - Autenticación (login/refresh)
- `/users/me/` - Usuario actual
- `/don_confiao/api/customers/` - Clientes
- `/don_confiao/api/products/` - Productos
- `/don_confiao/api/sales/` - Ventas

View File

@@ -1,51 +0,0 @@
<template>
<v-dialog v-model="dialog" persistent>
<v-card>
<v-card-title>
Ingrese el código
</v-card-title>
<v-card-text>
<v-form id="code-form" @submit.prevent="verifyCode">
<v-text-field v-model="code" label="Código" type="password" autocomplete="off" />
</v-form>
</v-card-text>
<v-card-actions>
<v-btn type="submit" form="code-form" color="green">Aceptar</v-btn>
<v-btn :to="{ path: '/' }" color="red">Cancelar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { inject } from 'vue';
export default {
data() {
return {
api: inject('api'),
dialog: true,
code: '',
};
},
methods: {
verifyCode() {
this.api.isValidAdminCode(this.code)
.then(data => {
if (data['validCode']) {
this.$emit('code-verified', true);
this.dialog = false;
} else {
alert('Código incorrecto');
this.$emit('code-verified', false);
}
})
.catch(error => {
alert('Error al validar el código');
this.$emit('code-verified', false);
console.error(error);
});
}
},
}
</script>

View File

@@ -13,11 +13,37 @@
</v-btn> </v-btn>
<v-btn <v-btn
v-else v-else
prepend-icon="mdi-logout"
variant="text" variant="text"
@click="logout"
> >
Logout <v-menu activator="parent">
<v-list>
<v-list-item>
<v-list-item-title class="font-weight-bold">{{ user?.username }}</v-list-item-title>
<v-list-item-subtitle>{{ user?.email }}</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<v-list-item v-if="user?.first_name || user?.last_name">
<v-list-item-title>{{ user?.first_name }} {{ user?.last_name }}</v-list-item-title>
</v-list-item>
<v-list-item>
<v-chip
:color="user?.role === 'administrator' ? 'error' : 'primary'"
size="small"
>
{{ user?.role === 'administrator' ? 'Administrador' : 'Usuario' }}
</v-chip>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="logout">
<v-list-item-title>
<v-icon start>mdi-logout</v-icon>
Cerrar sesión
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-icon start>mdi-account</v-icon>
{{ user?.username }}
</v-btn> </v-btn>
</v-app-bar> </v-app-bar>
<v-navigation-drawer v-model="drawer" <v-navigation-drawer v-model="drawer"
@@ -34,9 +60,9 @@
:prepend-icon="item.icon" :prepend-icon="item.icon"
@click="navigate(item.route)" @click="navigate(item.route)"
></v-list-item> ></v-list-item>
<v-list-item prepend-icon="mdi-cog" title="Administracion" @click="toggleAdminMenu()"></v-list-item> <v-list-item prepend-icon="mdi-cog" title="Administracion" @click="toggleAdminMenu()" v-if="isAuthenticated && isAdmin"></v-list-item>
<v-list-item> <v-list-item v-if="isAuthenticated && isAdmin && showAdminMenu">
<v-list v-if="showAdminMenu"> <v-list>
<v-list-item <v-list-item
v-for="item in menuAdminItems" v-for="item in menuAdminItems"
:key="item.title" :key="item.title"
@@ -53,13 +79,21 @@
<script> <script>
import trytonIcon from '../assets/icons/tryton-icon.svg'; import trytonIcon from '../assets/icons/tryton-icon.svg';
import AuthService from '@/services/auth'; import AuthService from '@/services/auth';
import { useAuthStore } from '@/stores/auth';
import { inject } from 'vue';
export default { export default {
name: 'NavBar', name: 'NavBar',
setup() {
const authStore = useAuthStore();
return { authStore };
},
data: () => ({ data: () => ({
drawer: false, drawer: false,
group: null, group: null,
showAdminMenu: false, showAdminMenu: false,
isAuthenticated: false, isAuthenticated: false,
user: null,
api: inject('api'),
menuItems: [ menuItems: [
{ title: 'Inicio', route: '/', icon: 'mdi-home'}, { title: 'Inicio', route: '/', icon: 'mdi-home'},
{ title: 'Comprar', route:'/comprar', icon: 'mdi-cart'}, { title: 'Comprar', route:'/comprar', icon: 'mdi-cart'},
@@ -74,8 +108,16 @@
{ title: 'Actualizar Ventas Tryton', route: '/sincronizar_ventas_tryton', icon: 'trytonIcon'} { title: 'Actualizar Ventas Tryton', route: '/sincronizar_ventas_tryton', icon: 'trytonIcon'}
], ],
}), }),
computed: {
isAdmin() {
return this.user?.role === 'administrator';
}
},
mounted() { mounted() {
this.checkAuth(); this.checkAuth();
if (this.isAuthenticated) {
this.fetchUser();
}
}, },
watch: { watch: {
group () { group () {
@@ -83,12 +125,23 @@
}, },
$route() { $route() {
this.checkAuth(); this.checkAuth();
if (this.isAuthenticated && !this.user) {
this.fetchUser();
}
}, },
}, },
methods: { methods: {
checkAuth() { checkAuth() {
this.isAuthenticated = AuthService.isAuthenticated(); this.isAuthenticated = AuthService.isAuthenticated();
}, },
async fetchUser() {
try {
this.user = await this.api.getCurrentUser();
this.authStore.setUser(this.user);
} catch (error) {
console.error('Error fetching user:', error);
}
},
navigate(route) { navigate(route) {
this.$router.push(route); this.$router.push(route);
}, },
@@ -102,6 +155,8 @@
logout() { logout() {
AuthService.logout(); AuthService.logout();
this.isAuthenticated = false; this.isAuthenticated = false;
this.user = null;
this.authStore.clearUser();
this.$router.push('/'); this.$router.push('/');
}, },
} }

View File

@@ -1,20 +1,19 @@
<template> <template>
<div> <AdminPurchase v-if="authStore.isAdmin"/>
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<AdminPurchase v-if="showComponent"/>
</template> </template>
<script > <script >
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
export default { export default {
data() { setup() {
return { const authStore = useAuthStore();
showComponent: false, return { authStore };
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
components: { CodeDialog },
methods: {},
} }
</script> </script>

View File

@@ -1,20 +1,19 @@
<template> <template>
<div> <ReconciliationJar v-if="authStore.isAdmin" />
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<ReconciliationJar v-if="showComponent" />
</template> </template>
<script > <script >
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
export default { export default {
data() { setup() {
return { const authStore = useAuthStore();
showComponent: false, return { authStore };
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
components: { CodeDialog },
methods: {},
} }
</script> </script>

View File

@@ -1,20 +1,19 @@
<template> <template>
<div> <ReconciliationJarIndex v-if="authStore.isAdmin" />
<CodeDialog @code-verified="(verified) => showComponent = verified" />
</div>
<ReconciliationJarIndex v-if="showComponent" />
</template> </template>
<script> <script>
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
export default { export default {
data() { setup() {
return { const authStore = useAuthStore();
showComponent: false, return { authStore };
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
components: { CodeDialog },
methods: {},
} }
</script> </script>

View File

@@ -1,8 +1,5 @@
<template> <template>
<div> <v-container v-if="authStore.isAdmin" class="fill-height d-flex align-center justify-center">
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<v-container class="fill-height d-flex align-center justify-center">
<v-card class="pa-6" max-width="600" elevation="4"> <v-card class="pa-6" max-width="600" elevation="4">
<v-card-title class="text-h5 font-weight-bold text-center"> <v-card-title class="text-h5 font-weight-bold text-center">
🔄 Sincronización de Clientes 🔄 Sincronización de Clientes
@@ -44,11 +41,15 @@
</template> </template>
<script> <script>
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
import { inject } from 'vue'; import { inject } from 'vue';
export default { export default {
name: 'CustomersFromTryton', name: 'CustomersFromTryton',
setup() {
const authStore = useAuthStore();
return { authStore };
},
data() { data() {
return { return {
api: inject('api'), api: inject('api'),
@@ -59,21 +60,24 @@
updated_customers: [], updated_customers: [],
} }
}, },
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
}
},
methods: { methods: {
startSync() { startSync() {
this.api.getCustomersFromTryton() this.api.getCustomersFromTryton()
.then(response => { .then(response => {
// Manejar la respuesta exitosa
this.checked_tryton_parties = response.checked_tryton_parties.map(id => ({ id })); this.checked_tryton_parties = response.checked_tryton_parties.map(id => ({ id }));
this.created_customers = response.created_customers.map(id => ({ id })); this.created_customers = response.created_customers.map(id => ({ id }));
this.failed_parties = response.failed_parties.map(id => ({ id })); this.failed_parties = response.failed_parties.map(id => ({ id }));
this.untouched_customers = response.untouched_customers.map(id => ({ id })); this.untouched_customers = response.untouched_customers.map(id => ({ id }));
}) })
.catch(error => { .catch(error => {
// Manejar el error
console.error("Error al sincronizar clientes:", error); console.error("Error al sincronizar clientes:", error);
}); });
} }
} }
} }
</script> </script>

View File

@@ -1,8 +1,5 @@
<template> <template>
<div> <v-container v-if="authStore.isAdmin" class="fill-height d-flex align-center justify-center">
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<v-container class="fill-height d-flex align-center justify-center">
<v-card class="pa-6" max-width="600" elevation="4"> <v-card class="pa-6" max-width="600" elevation="4">
<v-card-title class="text-h5 font-weight-bold text-center"> <v-card-title class="text-h5 font-weight-bold text-center">
🔄 Sincronización de Productos 🔄 Sincronización de Productos
@@ -39,16 +36,24 @@
</template> </template>
<script> <script>
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
import { inject } from 'vue'; import { inject } from 'vue';
export default { export default {
name: 'ProductsFromTryton', name: 'ProductsFromTryton',
setup() {
const authStore = useAuthStore();
return { authStore };
},
data() { data() {
return { return {
api: inject('api'), api: inject('api'),
productos_tryton: [{}], productos_tryton: [{}],
showComponent: false, }
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
methods: { methods: {
@@ -57,4 +62,4 @@
} }
} }
} }
</script> </script>

View File

@@ -1,8 +1,5 @@
<template> <template>
<div> <v-container v-if="authStore.isAdmin" class="fill-height d-flex align-center justify-center">
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<v-container class="fill-height d-flex align-center justify-center">
<v-card class="pa-6" max-width="600" elevation="4"> <v-card class="pa-6" max-width="600" elevation="4">
<v-card-title class="text-h5 font-weight-bold text-center"> <v-card-title class="text-h5 font-weight-bold text-center">
🔄 Sincronización de Ventas 🔄 Sincronización de Ventas
@@ -35,16 +32,24 @@
</v-container> </v-container>
</template> </template>
<script> <script>
import CodeDialog from '../components/CodeDialog.vue'; import { useAuthStore } from '@/stores/auth';
import { inject } from 'vue'; import { inject } from 'vue';
export default { export default {
name: 'SalesToTryton', name: 'SalesToTryton',
setup() {
const authStore = useAuthStore();
return { authStore };
},
data() { data() {
return { return {
api: inject('api'), api: inject('api'),
ventas_tryton: [], ventas_tryton: [],
showComponent: false, }
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
methods: { methods: {

View File

@@ -1,19 +1,18 @@
<template> <template>
<div> <ExportPurchasesForTryton v-if="authStore.isAdmin" />
<CodeDialog @code-verified="(verified) => showComponent = verified"/>
</div>
<ExportPurchasesForTryton v-if="showComponent" />
</template> </template>
<script> <script>
import CodeDialog from '../components/CodeDialog.vue' import { useAuthStore } from '@/stores/auth';
export default { export default {
data() { setup() {
return { const authStore = useAuthStore();
showComponent: false, return { authStore };
},
mounted() {
if (!this.authStore.isAdmin) {
this.$router.push('/');
} }
}, },
components: { CodeDialog },
methods: {},
} }
</script> </script>

View File

@@ -31,10 +31,6 @@ class Api {
return this.apiImplementation.getReconciliation(reconciliationId); return this.apiImplementation.getReconciliation(reconciliationId);
} }
isValidAdminCode(code) {
return this.apiImplementation.isValidAdminCode(code);
}
createPurchase(purchase) { createPurchase(purchase) {
return this.apiImplementation.createPurchase(purchase); return this.apiImplementation.createPurchase(purchase);
} }
@@ -62,6 +58,10 @@ class Api {
sendSalesToTryton(){ sendSalesToTryton(){
return this.apiImplementation.sendSalesToTryton(); return this.apiImplementation.sendSalesToTryton();
} }
getCurrentUser() {
return this.apiImplementation.getCurrentUser();
}
} }
export default Api; export default Api;

View File

@@ -49,11 +49,6 @@ class DjangoApi {
return this.getRequest(url); return this.getRequest(url);
} }
isValidAdminCode(code) {
const url = this.base + `/don_confiao/api/admin_code/validate/${code}`
return this.getRequest(url)
}
createPurchase(purchase) { createPurchase(purchase) {
const url = this.base + '/don_confiao/api/sales/'; const url = this.base + '/don_confiao/api/sales/';
return this.postRequest(url, purchase); return this.postRequest(url, purchase);
@@ -88,6 +83,11 @@ class DjangoApi {
const url = this.base + '/don_confiao/api/enviar_ventas_a_tryton'; const url = this.base + '/don_confiao/api/enviar_ventas_a_tryton';
return this.postRequest(url, {}); return this.postRequest(url, {});
} }
getCurrentUser() {
const url = this.base + '/api/users/me/';
return this.getRequest(url);
}
} }
export default DjangoApi; export default DjangoApi;

19
src/stores/auth.js Normal file
View File

@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null
}),
getters: {
isAdmin: (state) => state.user?.role === 'administrator',
isAuthenticated: (state) => !!state.user
},
actions: {
setUser(user) {
this.user = user
},
clearUser() {
this.user = null
}
}
})