diff --git a/install.sh b/install.sh old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json index 6e1802a..1111854 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.9.12", "dependencies": { "@popperjs/core": "^2.11.8", + "@primeuix/themes": "^1.2.5", "@tailwindcss/postcss": "^4.0.9", "@tailwindcss/vite": "^4.0.9", "@vitejs/plugin-vue": "^5.2.1", @@ -17,8 +18,10 @@ "axios": "^1.8.1", "laravel-echo": "^2.0.2", "luxon": "^3.5.0", + "material-symbols": "^0.36.2", "pdf-lib": "^1.17.1", "pinia": "^3.0.1", + "primevue": "^4.4.1", "pusher-js": "^8.4.0", "tailwindcss": "^4.0", "toastr": "^2.1.4", @@ -701,6 +704,74 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@primeuix/styled": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", + "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.5.tgz", + "integrity": "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.3" + } + }, + "node_modules/@primeuix/themes": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-1.2.5.tgz", + "integrity": "sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.3" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.1.tgz", + "integrity": "sha512-tQL/ZOPgCdD+NTimlUmhyD0ey8J1XmpZE4hDHM+/fnuBicVVmlKOd5HpS748LcOVRUKbWjmEPdHX4hi5XZoC1Q==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.4.1.tgz", + "integrity": "sha512-RG56iDKIJT//EtntjQzOiWOHZZJczw/qWWtdL5vFvw8/QDS9DPKn8HLpXK7N5Le6KK1MLXUsxoiGTZK+poUFUg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.4.1.tgz", + "integrity": "sha512-UfDimrIjVdY6EziwieyV4zPKzW6mnKHKhy4Dgyjv2oI6pNeuim+onbJo1ce22PEGXW78vfblG/3/JIzVHFweqQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1", + "@primevue/core": "4.4.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -2976,6 +3047,12 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/material-symbols": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.36.2.tgz", + "integrity": "sha512-FbxzGgQSmAb53Kajv+jyqcZ3Ck0ebfTBSMwHkMoyThsbrINiJb5mzheoiFXA/9MGc3cIl9XbhW8JxPM5vEP6iA==", + "license": "Apache-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3308,6 +3385,22 @@ "dev": true, "license": "MIT" }, + "node_modules/primevue": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz", + "integrity": "sha512-JbHBa5k30pZ7mn/z4vYBOnyt5GrR15eM3X0wa3VanonxnFLYkTEx8OMh33aU6ndWeOfi7Ef57dOL3bTH+3f4hQ==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.4", + "@primeuix/styles": "^1.2.5", + "@primeuix/utils": "^0.6.1", + "@primevue/core": "4.4.1", + "@primevue/icons": "4.4.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/package.json b/package.json index cc69e1c..9bd1552 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@popperjs/core": "^2.11.8", + "@primeuix/themes": "^1.2.5", "@tailwindcss/postcss": "^4.0.9", "@tailwindcss/vite": "^4.0.9", "@vitejs/plugin-vue": "^5.2.1", @@ -19,8 +20,10 @@ "axios": "^1.8.1", "laravel-echo": "^2.0.2", "luxon": "^3.5.0", + "material-symbols": "^0.36.2", "pdf-lib": "^1.17.1", "pinia": "^3.0.1", + "primevue": "^4.4.1", "pusher-js": "^8.4.0", "tailwindcss": "^4.0", "toastr": "^2.1.4", diff --git a/src/components/Holos/Button/Button.vue b/src/components/Holos/Button/Button.vue index 362eba4..33bf969 100644 --- a/src/components/Holos/Button/Button.vue +++ b/src/components/Holos/Button/Button.vue @@ -23,7 +23,6 @@ interface Props { loading?: boolean; fullWidth?: boolean; iconOnly?: boolean; - asLink?: boolean; // Nueva prop para comportamiento de link } const props = withDefaults(defineProps(), { @@ -35,7 +34,6 @@ const props = withDefaults(defineProps(), { loading: false, fullWidth: false, iconOnly: false, - asLink: true, // Por defecto no es link }); @@ -44,15 +42,8 @@ const emit = defineEmits<{ }>(); function handleClick(event: MouseEvent) { - // Si es usado como link, no bloquear la navegación - if (props.asLink) { - emit('click', event); - return; - } - - // Para botones normales, validar estados if (props.disabled || props.loading) return; - + emit('click', event); } const buttonClasses = computed(() => { @@ -82,7 +73,7 @@ const buttonClasses = computed(() => { solid: ['shadow-sm'], outline: ['border', 'bg-white', 'hover:bg-gray-50'], ghost: ['bg-transparent', 'hover:bg-gray-100'], - smooth: ['bg-opacity-20', 'font-bold', 'shadow-none'], + smooth: ['bg-opacity-20', 'font-bold', 'uppercase', 'shadow-none'], }; // Colores por tipo diff --git a/src/components/Holos/Inbox.vue b/src/components/Holos/Inbox.vue old mode 100755 new mode 100644 diff --git a/src/components/Holos/Inbox/Item.vue b/src/components/Holos/Inbox/Item.vue old mode 100755 new mode 100644 diff --git a/src/components/Holos/Inbox/ItemTitle.vue b/src/components/Holos/Inbox/ItemTitle.vue old mode 100755 new mode 100644 diff --git a/src/components/Holos/Inbox/Menu/Item.vue b/src/components/Holos/Inbox/Menu/Item.vue old mode 100755 new mode 100644 diff --git a/src/components/Holos/Inbox/Menu/Static.vue b/src/components/Holos/Inbox/Menu/Static.vue old mode 100755 new mode 100644 diff --git a/src/components/ui/Icons/MaterialIcon.vue b/src/components/ui/Icons/MaterialIcon.vue new file mode 100644 index 0000000..1dbbcad --- /dev/null +++ b/src/components/ui/Icons/MaterialIcon.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/ui/Input.vue b/src/components/ui/Input.vue new file mode 100644 index 0000000..1ca7f58 --- /dev/null +++ b/src/components/ui/Input.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/src/components/ui/Table/Table.vue b/src/components/ui/Table/Table.vue new file mode 100644 index 0000000..269620d --- /dev/null +++ b/src/components/ui/Table/Table.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/src/components/ui/Table/TableBody.vue b/src/components/ui/Table/TableBody.vue new file mode 100644 index 0000000..9f95778 --- /dev/null +++ b/src/components/ui/Table/TableBody.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/src/components/ui/Table/TableHeader.vue b/src/components/ui/Table/TableHeader.vue new file mode 100644 index 0000000..b7625f8 --- /dev/null +++ b/src/components/ui/Table/TableHeader.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/components/ui/Table/TablePagination.vue b/src/components/ui/Table/TablePagination.vue new file mode 100644 index 0000000..b7a6d3a --- /dev/null +++ b/src/components/ui/Table/TablePagination.vue @@ -0,0 +1,148 @@ + + + diff --git a/src/components/ui/Table/composables/usePagination.js b/src/components/ui/Table/composables/usePagination.js new file mode 100644 index 0000000..9cc490c --- /dev/null +++ b/src/components/ui/Table/composables/usePagination.js @@ -0,0 +1,60 @@ +import { computed, ref } from 'vue'; + +export function usePagination(initialConfig) { + const currentPage = ref(initialConfig.currentPage); + const pageSize = ref(initialConfig.pageSize); + const totalItems = ref(initialConfig.totalItems); + + const totalPages = computed(() => Math.ceil(totalItems.value / pageSize.value)); + + const startIndex = computed(() => (currentPage.value - 1) * pageSize.value); + + const endIndex = computed(() => Math.min(startIndex.value + pageSize.value, totalItems.value)); + + const hasNextPage = computed(() => currentPage.value < totalPages.value); + + const hasPreviousPage = computed(() => currentPage.value > 1); + + const goToPage = (page) => { + if (page >= 1 && page <= totalPages.value) { + currentPage.value = page; + } + }; + + const nextPage = () => { + if (hasNextPage.value) { + currentPage.value++; + } + }; + + const previousPage = () => { + if (hasPreviousPage.value) { + currentPage.value--; + } + }; + + const setPageSize = (size) => { + pageSize.value = size; + currentPage.value = 1; + }; + + const updateTotalItems = (total) => { + totalItems.value = total; + }; + + return { + currentPage, + pageSize, + totalItems, + totalPages, + startIndex, + endIndex, + hasNextPage, + hasPreviousPage, + goToPage, + nextPage, + previousPage, + setPageSize, + updateTotalItems, + }; +} diff --git a/src/components/ui/Table/composables/useSort.js b/src/components/ui/Table/composables/useSort.js new file mode 100644 index 0000000..9a3e992 --- /dev/null +++ b/src/components/ui/Table/composables/useSort.js @@ -0,0 +1,50 @@ +import { ref, computed } from 'vue'; + +export function useSort(initialData) { + const sortConfig = ref(null); + + const sortedData = computed(() => { + if (!sortConfig.value) { + return initialData; + } + + const { key, direction } = sortConfig.value; + const sorted = [...initialData]; + + sorted.sort((a, b) => { + const aValue = a[key]; + const bValue = b[key]; + + if (aValue === bValue) return 0; + + const comparison = aValue > bValue ? 1 : -1; + return direction === 'asc' ? comparison : -comparison; + }); + + return sorted; + }); + + const toggleSort = (key) => { + if (!sortConfig.value || sortConfig.value.key !== key) { + sortConfig.value = { key, direction: 'asc' }; + } else if (sortConfig.value.direction === 'asc') { + sortConfig.value = { key, direction: 'desc' }; + } else { + sortConfig.value = null; + } + }; + + const getSortDirection = (key) => { + if (!sortConfig.value || sortConfig.value.key !== key) { + return null; + } + return sortConfig.value.direction; + }; + + return { + sortConfig, + sortedData, + toggleSort, + getSortDirection, + }; +} diff --git a/src/components/ui/Tags/Badge.vue b/src/components/ui/Tags/Badge.vue new file mode 100644 index 0000000..cf4be8a --- /dev/null +++ b/src/components/ui/Tags/Badge.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/controllers/PrintController.js b/src/controllers/PrintController.js old mode 100755 new mode 100644 diff --git a/src/css/icons.css b/src/css/icons.css index e673e8f..d81e33f 100644 --- a/src/css/icons.css +++ b/src/css/icons.css @@ -75,3 +75,58 @@ font-weight: 400; src: url(./icons/google/gNNBW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4c2_whPnoY9ruReYU3rHmz74m0ZkGH-VBYe1x0TV6x4yFH8F-H5OdzEL3sVTgJtfbYxOLojCL.woff2) format('woff2'); } + +/* Clases para MaterialIcon component */ +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: 'liga'; +} + +.material-symbols-rounded { + font-family: 'Material Symbols Rounded'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: 'liga'; +} + +.material-symbols-sharp { + font-family: 'Material Symbols Sharp'; + font-weight: normal; + font-style: normal; + font-size: 24px; + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-feature-settings: 'liga'; +} diff --git a/src/index.js b/src/index.js index c62b0fc..7944ce4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +import Aura from '@primeuix/themes/aura'; import './css/base.css' import axios from 'axios'; @@ -13,10 +14,16 @@ import { pagePlugin } from '@Services/Page'; import { defineApp, reloadApp, view } from '@Services/Page'; import { apiURL } from '@Services/Api'; import VueApexCharts from "vue3-apexcharts"; -import VCalendar from 'v-calendar' +import VCalendar from 'v-calendar'; +import PrimeVue from 'primevue/config'; import 'v-calendar/style.css'; -import App from '@Components/App.vue' +import { definePreset } from '@primeuix/themes'; +import Button from 'primevue/button'; + + + +import App from '@Components/App.vue' import Error503 from '@Pages/Errors/503.vue' import { hasToken } from './services/Api'; @@ -24,9 +31,9 @@ import { hasToken } from './services/Api'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // Elementos globales -window.axios = axios; -window.Lang = lang; -window.Notify = new Notify(); +window.axios = axios; +window.Lang = lang; +window.Notify = new Notify(); window.TwScreen = new TailwindScreen(); async function boot() { @@ -40,26 +47,44 @@ async function boot() { window.Ziggy = routes.data; defineApp(appData.data); window.route = useRoute(); - window.view = view; - initRoutes = true; + window.view = view; + initRoutes = true; } catch (error) { window.Notify.error(window.Lang('server.api.noAvailable')); } - if(initRoutes) { + if (initRoutes) { // Iniciar permisos - if(hasToken()) { + if (hasToken()) { await bootPermissions(); await bootRoles(); - + // Iniciar broadcast - if(import.meta.env.VITE_REVERB_ACTIVE === 'true') { + if (import.meta.env.VITE_REVERB_ACTIVE === 'true') { await import('@Services/Broadcast') } } - + reloadApp(); - + + const MyPreset = definePreset(Aura, { + semantic: { + primary: { + 50: '{neutral.50}', + 100: '{neutral.100}', + 200: '{neutral.200}', + 300: '{neutral.300}', + 400: '{neutral.400}', + 500: '{neutral.500}', + 600: '{neutral.600}', + 700: '{neutral.700}', + 800: '{neutral.800}', + 900: '{neutral.900}', + 950: '{neutral.950}' + }, + } + }); + createApp(App) .use(createPinia()) .use(i18n) @@ -68,6 +93,11 @@ async function boot() { .use(ZiggyVue) .use(VCalendar, {}) .use(VueApexCharts) + .use(PrimeVue, { + theme: { + preset: MyPreset + } + }) .mount('#app'); } else { createApp(Error503) diff --git a/src/lang/es.js b/src/lang/es.js index 698d571..d410a72 100644 --- a/src/lang/es.js +++ b/src/lang/es.js @@ -78,6 +78,19 @@ export default { activity: { title: 'Historial de acciones', description: 'Historial de acciones realizadas por los usuarios en orden cronológico.' + }, + products: { + name: 'Productos', + title: 'Productos', + description: 'Gestión del catálogo de productos', + create: { + title: 'Crear producto', + description: 'Permite crear un nuevo producto en el catálogo con sus clasificaciones y atributos personalizados.' + }, + edit: { + title: 'Editar producto', + description: 'Actualiza la información del producto, sus clasificaciones y atributos.' + } } }, app: { @@ -193,6 +206,8 @@ export default { done:'Hecho.', edit:'Editar', edited:'Registro creado', + active:'Activo', + inactive:'Inactivo', email:{ title:'Correo', verification:'Verificar correo' diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 89cef95..c7b5031 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -94,6 +94,24 @@ 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/pages/Pos/Index.vue b/src/pages/Pos/Index.vue new file mode 100644 index 0000000..04691b4 --- /dev/null +++ b/src/pages/Pos/Index.vue @@ -0,0 +1,851 @@ + + + \ No newline at end of file diff --git a/src/pages/Pos/Module.js b/src/pages/Pos/Module.js new file mode 100644 index 0000000..943a378 --- /dev/null +++ b/src/pages/Pos/Module.js @@ -0,0 +1,23 @@ +import { lang } from '@Lang/i18n'; +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`products.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ + name: `admin.products.${name}`, params, query +}) + +// Obtener traducción del componente +const transl = (str) => lang(`admin.products.${str}`) + +// Control de permisos +const can = (permission) => hasPermission(`admin.products.${permission}`) + +export { + can, + viewTo, + apiTo, + transl +} \ No newline at end of file diff --git a/src/pages/Products/Create.vue b/src/pages/Products/Create.vue new file mode 100644 index 0000000..85bdc08 --- /dev/null +++ b/src/pages/Products/Create.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/src/pages/Products/Edit.vue b/src/pages/Products/Edit.vue new file mode 100644 index 0000000..0e17d5c --- /dev/null +++ b/src/pages/Products/Edit.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/pages/Products/Form.vue b/src/pages/Products/Form.vue new file mode 100644 index 0000000..a3decb0 --- /dev/null +++ b/src/pages/Products/Form.vue @@ -0,0 +1,546 @@ + + +