From 2bd5d00827e61105a654cbcbcc40c44dc05dd3dc Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Thu, 2 Oct 2025 16:16:12 -0600 Subject: [PATCH 1/5] =?UTF-8?q?Add:=20Administraci=C3=B3n=20de=20clasifica?= =?UTF-8?q?ciones=20comerciales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/AdminLayout.vue | 8 + src/pages/ComercialClassifications/Create.vue | 89 +++++ src/pages/ComercialClassifications/Edit.vue | 139 +++++++ src/pages/ComercialClassifications/Form.vue | 68 ++++ src/pages/ComercialClassifications/Index.vue | 204 ++++++++++ .../ComercialClassifications/Modals/Show.vue | 362 ++++++++++++++++++ src/pages/ComercialClassifications/Module.js | 23 ++ .../comercial-classifications.interfaces.js | 62 +++ .../ComercialClassificationsService.js | 180 +++++++++ src/router/Index.js | 34 ++ 10 files changed, 1169 insertions(+) create mode 100644 src/pages/ComercialClassifications/Create.vue create mode 100644 src/pages/ComercialClassifications/Edit.vue create mode 100644 src/pages/ComercialClassifications/Form.vue create mode 100644 src/pages/ComercialClassifications/Index.vue create mode 100644 src/pages/ComercialClassifications/Modals/Show.vue create mode 100644 src/pages/ComercialClassifications/Module.js create mode 100644 src/pages/ComercialClassifications/interfaces/comercial-classifications.interfaces.js create mode 100644 src/pages/ComercialClassifications/services/ComercialClassificationsService.js 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', -- 2.45.2 From 83f0abff133f7007ce1ac9e0c423b40476843ca4 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Sat, 4 Oct 2025 09:23:04 -0600 Subject: [PATCH 2/5] ADD: Administrador de productos(WIP) --- src/components/ui/Input.vue | 114 +++++++++++++ src/layouts/AdminLayout.vue | 5 + src/pages/Products/Index.vue | 229 ++++++++++++++++++++++++++ src/pages/Products/Module.js | 23 +++ src/pages/Products/a.jsx | 300 +++++++++++++++++++++++++++++++++++ src/router/Index.js | 34 ++++ 6 files changed, 705 insertions(+) create mode 100644 src/components/ui/Input.vue create mode 100644 src/pages/Products/Index.vue create mode 100644 src/pages/Products/Module.js create mode 100644 src/pages/Products/a.jsx 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/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 2733699..3956bd5 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -100,6 +100,11 @@ onMounted(() => { name="Clasificaciones Comerciales" to="admin.comercial-classifications.index" /> +
diff --git a/src/pages/Products/Index.vue b/src/pages/Products/Index.vue new file mode 100644 index 0000000..663d362 --- /dev/null +++ b/src/pages/Products/Index.vue @@ -0,0 +1,229 @@ + + + \ No newline at end of file diff --git a/src/pages/Products/Module.js b/src/pages/Products/Module.js new file mode 100644 index 0000000..943a378 --- /dev/null +++ b/src/pages/Products/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/a.jsx b/src/pages/Products/a.jsx new file mode 100644 index 0000000..4bbb939 --- /dev/null +++ b/src/pages/Products/a.jsx @@ -0,0 +1,300 @@ +import { useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Plus, + Search, + Filter, + Edit, + Trash2, + Package, + MoreHorizontal +} from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +// Mock data +const products = [ + { + id: 1, + code: "PROD-001", + name: "Laptop Dell Inspiron 15", + description: "Laptop empresarial de alto rendimiento", + category: "Electrónicos > Computadoras", + brand: "Dell", + price: 15999.00, + stock: 45, + minStock: 10, + status: "active", + lastUpdated: "2024-01-15" + }, + { + id: 2, + code: "PROD-002", + name: "Mouse Inalámbrico Logitech", + description: "Mouse óptico inalámbrico con receptor USB", + category: "Electrónicos > Accesorios", + brand: "Logitech", + price: 599.00, + stock: 120, + minStock: 25, + status: "active", + lastUpdated: "2024-01-14" + }, + { + id: 3, + code: "PROD-003", + name: "Monitor LG 24 pulgadas", + description: "Monitor LED Full HD con conexión HDMI", + category: "Electrónicos > Monitores", + brand: "LG", + price: 3299.00, + stock: 8, + minStock: 15, + status: "low-stock", + lastUpdated: "2024-01-13" + }, + { + id: 4, + code: "PROD-004", + name: "Teclado Mecánico RGB", + description: "Teclado mecánico para gaming con iluminación RGB", + category: "Electrónicos > Accesorios", + brand: "Corsair", + price: 2199.00, + stock: 0, + minStock: 5, + status: "out-of-stock", + lastUpdated: "2024-01-12" + }, + { + id: 5, + code: "PROD-005", + name: "Webcam HD Logitech", + description: "Cámara web HD para videoconferencias", + category: "Electrónicos > Accesorios", + brand: "Logitech", + price: 899.00, + stock: 32, + minStock: 10, + status: "active", + lastUpdated: "2024-01-11" + } +] + +const getStatusBadge = (status: string, stock: number, minStock: number) => { + if (stock === 0) { + return Sin Stock + } + if (stock <= minStock) { + return Stock Bajo + } + return Activo +} + +export default function Products() { + const [searchTerm, setSearchTerm] = useState("") + const [filteredProducts, setFilteredProducts] = useState(products) + + const handleSearch = (term: string) => { + setSearchTerm(term) + const filtered = products.filter(product => + product.name.toLowerCase().includes(term.toLowerCase()) || + product.code.toLowerCase().includes(term.toLowerCase()) || + product.brand.toLowerCase().includes(term.toLowerCase()) + ) + setFilteredProducts(filtered) + } + + return ( +
+ {/* Header */} +
+
+

Productos

+

+ Gestión del catálogo de productos +

+
+ +
+ + {/* Stats Cards */} +
+ + +
+ +
+

{products.length}

+

Total Productos

+
+
+
+
+ + +
+
+ +
+
+

+ {products.filter(p => p.status === "active").length} +

+

Activos

+
+
+
+
+ + +
+
+ ! +
+
+

+ {products.filter(p => p.status === "low-stock").length} +

+

Stock Bajo

+
+
+
+
+ + +
+
+ × +
+
+

+ {products.filter(p => p.status === "out-of-stock").length} +

+

Sin Stock

+
+
+
+
+
+ + {/* Filters and Search */} + + + Catálogo de Productos + + +
+
+ + handleSearch(e.target.value)} + className="pl-10" + /> +
+ +
+ + {/* Products Table */} +
+ + + + Código + Producto + Categoría + Marca + Precio + Stock + Estado + + + + + {filteredProducts.map((product) => ( + + + {product.code} + + +
+

{product.name}

+

+ {product.description} +

+
+
+ + {product.category} + + {product.brand} + + ${product.price.toLocaleString('es-MX')} + + +
+

{product.stock}

+

+ Min: {product.minStock} +

+
+
+ + {getStatusBadge(product.status, product.stock, product.minStock)} + + + + + + + + + + Editar + + + + Ver Stock + + + + Eliminar + + + + +
+ ))} +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/router/Index.js b/src/router/Index.js index 5802ee6..cc5f7f2 100644 --- a/src/router/Index.js +++ b/src/router/Index.js @@ -528,6 +528,40 @@ const router = createRouter({ } ] }, + { + path: 'products', + name: 'admin.products', + meta: { + title: 'Productos', + icon: 'inventory', + }, + redirect: '/admin/products', + children: [ + { + path: '', + name: 'admin.products.index', + component: () => import('@Pages/Products/Index.vue'), + }, + /* { + path: 'create', + name: 'admin.products.create', + component: () => import('@Pages/Products/Create.vue'), + meta: { + title: 'Crear', + icon: 'add', + }, + }, + { + path: ':id/edit', + name: 'admin.products.edit', + component: () => import('@Pages/Products/Edit.vue'), + meta: { + title: 'Editar', + icon: 'edit', + }, + } */ + ] + }, { path: 'roles', name: 'admin.roles', -- 2.45.2 From c222b66ceff1fe8ddc80b7880085259db71b4294 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Wed, 8 Oct 2025 12:45:48 -0600 Subject: [PATCH 3/5] Add: Punto de venta y UX para productos --- package-lock.json | 7 + package.json | 1 + src/components/Holos/Button/Button.vue | 13 +- src/components/ui/Icons/MaterialIcon.vue | 49 + src/components/ui/Table/Table.vue | 117 +++ src/components/ui/Table/TableBody.vue | 56 ++ src/components/ui/Table/TableHeader.vue | 65 ++ src/components/ui/Table/TablePagination.vue | 148 +++ .../ui/Table/composables/usePagination.js | 60 ++ .../ui/Table/composables/useSort.js | 50 + src/components/ui/Tags/Badge.vue | 109 +++ src/css/icons.css | 55 ++ src/lang/es.js | 15 + src/layouts/AdminLayout.vue | 5 + src/pages/Pos/Index.vue | 851 ++++++++++++++++++ src/pages/Pos/Module.js | 23 + src/pages/Products/Create.vue | 54 ++ src/pages/Products/Edit.vue | 81 ++ src/pages/Products/Form.vue | 546 +++++++++++ src/pages/Products/Index.vue | 281 +++--- src/pages/Products/Modals/Show.vue | 342 +++++++ src/pages/Products/Module.js | 2 + .../interfaces/products.interfaces.js | 82 ++ src/pages/Products/services/ProductService.js | 263 ++++++ src/router/Index.js | 23 +- 25 files changed, 3128 insertions(+), 170 deletions(-) create mode 100644 src/components/ui/Icons/MaterialIcon.vue create mode 100644 src/components/ui/Table/Table.vue create mode 100644 src/components/ui/Table/TableBody.vue create mode 100644 src/components/ui/Table/TableHeader.vue create mode 100644 src/components/ui/Table/TablePagination.vue create mode 100644 src/components/ui/Table/composables/usePagination.js create mode 100644 src/components/ui/Table/composables/useSort.js create mode 100644 src/components/ui/Tags/Badge.vue create mode 100644 src/pages/Pos/Index.vue create mode 100644 src/pages/Pos/Module.js create mode 100644 src/pages/Products/Create.vue create mode 100644 src/pages/Products/Edit.vue create mode 100644 src/pages/Products/Form.vue create mode 100644 src/pages/Products/Modals/Show.vue create mode 100644 src/pages/Products/interfaces/products.interfaces.js create mode 100644 src/pages/Products/services/ProductService.js 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 @@ + + +