diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 89cef95..2733699 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -94,6 +94,14 @@ onMounted(() => { to="admin.units-measure.index" /> +
+ +
+
+import { onMounted, ref } from 'vue'; +import { useRouter, useRoute, RouterLink } from 'vue-router'; +import { api, useForm } from '@Services/Api'; +import { apiTo, transl, viewTo } from './Module'; + +import IconButton from '@Holos/Button/Icon.vue' +import PageHeader from '@Holos/PageHeader.vue'; +import GoogleIcon from '@Shared/GoogleIcon.vue'; +import Form from './Form.vue' + +/** Definidores */ +const router = useRouter(); +const route = useRoute(); + +/** Propiedades */ +const form = useForm({ + code: '', + name: '', + description: '', + parent_id: null +}); + +const parentInfo = ref(null); + +/** Métodos */ +function submit() { + form.transform(data => ({ + ...data, + parent_id: data.parent_id // Usar el parent_id del formulario + })).post(apiTo('store'), { + onSuccess: () => { + Notify.success(Lang('register.create.onSuccess')) + router.push(viewTo({ name: 'index' })); + } + }) +} + +/** Ciclos */ +onMounted(() => { + // Verificar si se están pasando parámetros para crear subcategoría + if (route.query.parent_id) { + parentInfo.value = { + id: parseInt(route.query.parent_id), + name: route.query.parent_name, + code: route.query.parent_code + }; + // Pre-llenar el parent_id en el formulario + form.parent_id = parseInt(route.query.parent_id); + } +}) + + + \ No newline at end of file diff --git a/src/pages/ComercialClassifications/Edit.vue b/src/pages/ComercialClassifications/Edit.vue new file mode 100644 index 0000000..4527d2c --- /dev/null +++ b/src/pages/ComercialClassifications/Edit.vue @@ -0,0 +1,139 @@ + + + \ No newline at end of file diff --git a/src/pages/ComercialClassifications/Form.vue b/src/pages/ComercialClassifications/Form.vue new file mode 100644 index 0000000..bf02116 --- /dev/null +++ b/src/pages/ComercialClassifications/Form.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/src/pages/ComercialClassifications/Index.vue b/src/pages/ComercialClassifications/Index.vue new file mode 100644 index 0000000..459b9ba --- /dev/null +++ b/src/pages/ComercialClassifications/Index.vue @@ -0,0 +1,204 @@ + + + \ No newline at end of file diff --git a/src/pages/ComercialClassifications/Modals/Show.vue b/src/pages/ComercialClassifications/Modals/Show.vue new file mode 100644 index 0000000..1d84775 --- /dev/null +++ b/src/pages/ComercialClassifications/Modals/Show.vue @@ -0,0 +1,362 @@ + + + \ No newline at end of file diff --git a/src/pages/ComercialClassifications/Module.js b/src/pages/ComercialClassifications/Module.js new file mode 100644 index 0000000..24b9ccf --- /dev/null +++ b/src/pages/ComercialClassifications/Module.js @@ -0,0 +1,23 @@ +import { lang } from '@Lang/i18n'; +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`comercial-classifications.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ + name: `admin.comercial-classifications.${name}`, params, query +}) + +// Obtener traducción del componente +const transl = (str) => lang(`admin.comercial_classifications.${str}`) + +// Control de permisos +const can = (permission) => hasPermission(`admin.comercial-classifications.${permission}`) + +export { + can, + viewTo, + apiTo, + transl +} \ No newline at end of file diff --git a/src/pages/ComercialClassifications/interfaces/comercial-classifications.interfaces.js b/src/pages/ComercialClassifications/interfaces/comercial-classifications.interfaces.js new file mode 100644 index 0000000..8a68475 --- /dev/null +++ b/src/pages/ComercialClassifications/interfaces/comercial-classifications.interfaces.js @@ -0,0 +1,62 @@ +/** + * Interfaces para Comercial Classifications + * + * @author Sistema + * @version 1.0.0 + */ + +/** + * @typedef {Object} ComercialClassification + * @property {number} id - ID de la clasificación + * @property {string} code - Código de la clasificación + * @property {string} name - Nombre de la clasificación + * @property {string|null} description - Descripción de la clasificación + * @property {boolean} is_active - Estado activo/inactivo + * @property {number|null} parent_id - ID del padre + * @property {string} created_at - Fecha de creación + * @property {string} updated_at - Fecha de actualización + * @property {string|null} deleted_at - Fecha de eliminación + * @property {ComercialClassification|null} parent - Clasificación padre + * @property {ComercialClassification[]} children - Clasificaciones hijas + */ + +/** + * @typedef {Object} ComercialClassificationResponse + * @property {string} status - Estado de la respuesta + * @property {Object} data - Datos de la respuesta + * @property {string} data.message - Mensaje de la respuesta + * @property {ComercialClassification} data.comercial_classification - Clasificación comercial + */ + +/** + * @typedef {Object} ComercialClassificationsListResponse + * @property {string} status - Estado de la respuesta + * @property {Object} data - Datos de la respuesta + * @property {ComercialClassification[]} data.comercial_classifications - Lista de clasificaciones + */ + +/** + * @typedef {Object} CreateComercialClassificationData + * @property {string} code - Código de la clasificación + * @property {string} name - Nombre de la clasificación + * @property {string|null} description - Descripción de la clasificación + * @property {boolean} is_active - Estado activo/inactivo + * @property {number|null} parent_id - ID del padre + */ + +/** + * @typedef {Object} UpdateComercialClassificationData + * @property {string} [code] - Código de la clasificación + * @property {string} [name] - Nombre de la clasificación + * @property {string|null} [description] - Descripción de la clasificación + * @property {boolean} [is_active] - Estado activo/inactivo + * @property {number|null} [parent_id] - ID del padre + */ + +export { + ComercialClassification, + ComercialClassificationResponse, + ComercialClassificationsListResponse, + CreateComercialClassificationData, + UpdateComercialClassificationData +}; diff --git a/src/pages/ComercialClassifications/services/ComercialClassificationsService.js b/src/pages/ComercialClassifications/services/ComercialClassificationsService.js new file mode 100644 index 0000000..129895b --- /dev/null +++ b/src/pages/ComercialClassifications/services/ComercialClassificationsService.js @@ -0,0 +1,180 @@ +/** + * Servicio para Comercial Classifications + * + * @author Sistema + * @version 2.0.0 + */ + +import { api, apiURL } from '@Services/Api'; + +export default class ComercialClassificationsService { + + /** + * Obtener todas las clasificaciones comerciales + * @param {Object} params - Parámetros de la consulta + * @returns {Promise} Promesa con la respuesta + */ + async getAll(params = {}) { + return new Promise((resolve, reject) => { + api.get(apiURL('comercial-classifications'), { + params, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Obtener una clasificación comercial por ID + * @param {number} id - ID de la clasificación + * @returns {Promise} Promesa con la respuesta + */ + async getById(id) { + return new Promise((resolve, reject) => { + api.get(apiURL(`comercial-classifications/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Crear una nueva clasificación comercial + * @param {Object} data - Datos de la clasificación + * @param {string} data.code - Código de la clasificación + * @param {string} data.name - Nombre de la clasificación + * @param {string} data.description - Descripción de la clasificación + * @param {number|null} data.parent_id - ID de la clasificación padre (null para raíz) + * @param {boolean} data.is_active - Estado activo/inactivo + * @returns {Promise} Promesa con la respuesta + */ + async create(data) { + return new Promise((resolve, reject) => { + api.post(apiURL('comercial-classifications'), { + data, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Actualizar una clasificación comercial existente + * @param {number} id - ID de la clasificación + * @param {Object} data - Datos actualizados + * @returns {Promise} Promesa con la respuesta + */ + async update(id, data) { + return new Promise((resolve, reject) => { + api.put(apiURL(`comercial-classifications/${id}`), { + data, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Eliminar una clasificación comercial + * @param {number} id - ID de la clasificación + * @returns {Promise} Promesa con la respuesta + */ + async delete(id) { + return new Promise((resolve, reject) => { + api.delete(apiURL(`comercial-classifications/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Actualizar solo el estado de una clasificación comercial + * @param {number} id - ID de la clasificación + * @param {boolean} is_active - Nuevo estado + * @returns {Promise} Promesa con la respuesta + */ + async updateStatus(id, is_active) { + return new Promise((resolve, reject) => { + api.put(apiURL(`comercial-classifications/${id}`), { + data: { is_active }, + onSuccess: (response) => { + resolve(response); + }, + onError: (error) => { + // Mejorar el manejo de errores + const enhancedError = { + ...error, + timestamp: new Date().toISOString(), + action: 'updateStatus', + id: id, + is_active: is_active + }; + reject(enhancedError); + } + }); + }); + } + + /** + * Obtener clasificaciones padre disponibles (para selects) + * @param {number|null} excludeId - ID a excluir de la lista (para evitar loops) + * @returns {Promise} Promesa con la respuesta + */ + async getParentOptions(excludeId = null) { + return new Promise((resolve, reject) => { + api.get(apiURL('comercial-classifications'), { + params: { exclude_children_of: excludeId }, + onSuccess: (response) => { + // Aplanar la estructura jerárquica para el selector + const flattenOptions = (items, level = 0) => { + let options = []; + items.forEach(item => { + if (excludeId && (item.id === excludeId || this.hasDescendant(item, excludeId))) { + return; // Excluir item actual y sus descendientes + } + + options.push({ + ...item, + name: ' '.repeat(level) + item.name, // Indentación visual + level + }); + + if (item.children && item.children.length > 0) { + options = options.concat(flattenOptions(item.children, level + 1)); + } + }); + return options; + }; + + const flatOptions = flattenOptions(response.comercial_classifications?.data || response.data || []); + resolve(flatOptions); + }, + onError: (error) => reject(error) + }); + }); + } + + /** + * Función auxiliar para verificar si un item tiene como descendiente un ID específico + * @param {Object} item - Item a verificar + * @param {number} targetId - ID objetivo + * @returns {boolean} True si es descendiente + */ + hasDescendant(item, targetId) { + if (!item.children) return false; + return item.children.some(child => + child.id === targetId || this.hasDescendant(child, targetId) + ); + } + + /** + * Alternar el estado de una clasificación + * @param {Object} item - Objeto con la clasificación + * @returns {Promise} Promesa con la respuesta + */ + async toggleStatus(item) { + const newStatus = !item.is_active; + return this.updateStatus(item.id, newStatus); + } +} \ No newline at end of file diff --git a/src/router/Index.js b/src/router/Index.js index 57db59a..5802ee6 100644 --- a/src/router/Index.js +++ b/src/router/Index.js @@ -494,6 +494,40 @@ const router = createRouter({ } ] }, + { + path: 'comercial-classifications', + name: 'admin.comercial-classifications', + meta: { + title: 'Clasificaciones Comerciales', + icon: 'category', + }, + redirect: '/admin/comercial-classifications', + children: [ + { + path: '', + name: 'admin.comercial-classifications.index', + component: () => import('@Pages/ComercialClassifications/Index.vue'), + }, + { + path: 'create', + name: 'admin.comercial-classifications.create', + component: () => import('@Pages/ComercialClassifications/Create.vue'), + meta: { + title: 'Crear', + icon: 'add', + }, + }, + { + path: ':id/edit', + name: 'admin.comercial-classifications.edit', + component: () => import('@Pages/ComercialClassifications/Edit.vue'), + meta: { + title: 'Editar', + icon: 'edit', + }, + } + ] + }, { path: 'roles', name: 'admin.roles',