diff --git a/package-lock.json b/package-lock.json index 6e1802a..4f8b8d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "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", "pusher-js": "^8.4.0", @@ -2976,6 +2977,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", diff --git a/package.json b/package.json index cc69e1c..740790c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "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", "pusher-js": "^8.4.0", 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/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/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/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/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 3956bd5..c7b5031 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -105,6 +105,11 @@ onMounted(() => { name="Productos" to="admin.products.index" /> +
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 @@ + + +