From c95d40787d7c0b3d8d6a77b311db7236a30efd17 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Tue, 23 Sep 2025 16:18:45 -0600 Subject: [PATCH 1/3] WIP --- .env.example | 7 +- docker-compose.yml | 1 + src/layouts/AdminLayout.vue | 8 + src/pages/Warehouses/Index.vue | 386 +++++++++++++++++++++++++++++++++ src/pages/Warehouses/Module.js | 16 ++ src/router/Index.js | 37 ++++ 6 files changed, 452 insertions(+), 3 deletions(-) create mode 100644 src/pages/Warehouses/Index.vue create mode 100644 src/pages/Warehouses/Module.js diff --git a/.env.example b/.env.example index 94a2f7d..37712f0 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ -VITE_API_URL=http://backend.holos.test:8080 -VITE_BASE_URL=http://frontend.holos.test +VITE_API_URL=http://localhost:8080 +VITE_BASE_URL=http://localhost:3000 VITE_REVERB_APP_ID= VITE_REVERB_APP_KEY= VITE_REVERB_APP_SECRET= -VITE_REVERB_HOST="backend.holos.test" +VITE_REVERB_HOST="localhost" VITE_REVERB_PORT=8080 VITE_REVERB_SCHEME=http VITE_REVERB_ACTIVE=false APP_PORT=3000 + diff --git a/docker-compose.yml b/docker-compose.yml index 050a3c6..25c89cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - frontend-v1:/var/www/gols-frontend-v1/node_modules networks: - gols-network + mem_limit: 512m volumes: frontend-v1: driver: local diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 0b5a612..09880a8 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -81,6 +81,14 @@ onMounted(() => { to="admin.vacations.index" /> + +
+ +
+import { ref, computed, onMounted } from 'vue' +import GoogleIcon from '@Shared/GoogleIcon.vue' +import Searcher from '@Holos/Searcher.vue' +import Adding from '@Holos/Button/ButtonRh.vue' +import { api } from '@Services/Api' +import { apiTo } from './Module' + +// Reactive state +const warehouses = ref([]) +const loading = ref(false) +const error = ref('') +const inventoryMovements = ref([ + // kept mock movements for demo; in real app these would come from API + { + id: 1, + date: '2024-01-15T10:30:00', + type: 'entrada', + product: 'Laptop Dell Inspiron 15', + code: 'PROD-001', + quantity: 10, + warehouse: 'Almacén Principal', + reference: 'PO-2024-001', + user: 'Ana García', + status: 'completed' + }, + { + id: 2, + date: '2024-01-15T09:15:00', + type: 'salida', + product: 'Mouse Inalámbrico Logitech', + code: 'PROD-002', + quantity: 25, + warehouse: 'Almacén Norte', + reference: 'SO-2024-045', + user: 'Carlos López', + status: 'completed' + } +]) + +const stockByWarehouse = ref([]) + +// UI state +const activeTab = ref('warehouses') +const searchTerm = ref('') + +const fetchWarehousesFromApi = (q = '') => { + loading.value = true + error.value = '' + + api.get(apiTo('index'), { + params: { q }, + onStart: () => { + loading.value = true + }, + onSuccess: (data, fullPayload) => { + const payload = Array.isArray(data) ? data : (fullPayload && Array.isArray(fullPayload.data) ? fullPayload.data : []) + + warehouses.value = payload.map((w) => ({ + id: w.id, + code: w.code ?? `WH-${String(w.id).padStart(3, '0')}`, + name: w.name, + location: w.address ?? '', + type: w.type ?? '', + totalProducts: (w.classifications && Array.isArray(w.classifications)) ? w.classifications.length : 0, + totalValue: w.total_value ?? 0, + capacity: w.capacity ?? '0%', + status: w.is_active ? 'active' : 'inactive', + classifications: w.classifications ?? [] + })) + }, + onFail: (data) => { + error.value = data?.message || 'Error al obtener almacenes' + }, + onError: (err) => { + console.error('API Error:', err) + try { + error.value = err?.message || err?.data?.message || 'Error desconocido' + } catch (e) { + error.value = 'Error desconocido' + } + }, + onFinish: () => { + loading.value = false + } + }) +} + +onMounted(() => { + fetchWarehousesFromApi() +}) + +// Computed totals +const totalInventoryValue = computed(() => warehouses.value.reduce((sum, wh) => sum + (wh.totalValue || 0), 0)) +const totalProducts = computed(() => warehouses.value.reduce((sum, wh) => sum + (wh.totalProducts || 0), 0)) + +// Filtered movements by searchTerm +const filteredMovements = computed(() => { + const q = searchTerm.value.trim().toLowerCase() + if (!q) return inventoryMovements.value + return inventoryMovements.value.filter(m => { + return [m.product, m.code, m.warehouse, m.user, m.reference] + .filter(Boolean) + .some(f => f.toLowerCase().includes(q)) + }) +}) + +// Helpers +const parseCapacity = (cap) => { + if (typeof cap === 'string') return parseInt(cap.replace('%', ''), 10) || 0 + if (typeof cap === 'number') return Math.round(cap) + return 0 +} + +const getMovementIconClass = (type) => { + switch (type) { + case 'entrada': + return 'icon-arrow-down-left text-success' + case 'salida': + return 'icon-arrow-up-right text-destructive' + case 'transferencia': + return 'icon-refresh text-warning' + case 'ajuste': + return 'icon-trending-up text-primary' + default: + return 'icon-refresh' + } +} + +const movementVariant = (type) => { + const map = { entrada: 'default', salida: 'destructive', transferencia: 'secondary', ajuste: 'outline' } + return map[type] || 'default' +} + +const formatCurrency = (value) => { + try { + return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(value) + } catch (e) { + return `$${value}` + } +} + + + \ No newline at end of file diff --git a/src/pages/Warehouses/Module.js b/src/pages/Warehouses/Module.js new file mode 100644 index 0000000..9630d8d --- /dev/null +++ b/src/pages/Warehouses/Module.js @@ -0,0 +1,16 @@ +import { lang } from '@Lang/i18n'; + +// Ruta API +const apiTo = (name, params = {}) => route(`warehouses.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `warehouses.${name}`, params, query }) + +// Obtener traducción del componente +const transl = (str) => lang(`warehouses.${str}`) + +export { + viewTo, + apiTo, + transl +} \ No newline at end of file diff --git a/src/router/Index.js b/src/router/Index.js index 99cac92..79ba040 100644 --- a/src/router/Index.js +++ b/src/router/Index.js @@ -306,6 +306,43 @@ const router = createRouter({ } ] }, + { + path: 'warehouses', + name: 'admin.warehouses', + meta: { + title: 'Bodegas', + icon: 'inventory_2', + }, + redirect: '/admin/warehouses', + children: [ + { + path: '', + name: 'admin.warehouses.index', + component: () => import('@Pages/Warehouses/Index.vue'), + + }, + { + path: 'create', + name: 'admin.warehouses.create', + component: () => import('@Pages/Admin/Roles/Index.vue'), + + meta: { + title: 'Crear', + icon: 'add', + }, + }, + { + path: ':id/edit', + name: 'admin.warehouses.edit', + component: () => import('@Pages/Admin/Roles/Index.vue'), + + meta: { + title: 'Editar', + icon: 'edit', + }, + } + ] + }, { path: 'roles', name: 'admin.roles', From efcad3fe1daeb90d037ac8ec451452fd2393cc6c Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Thu, 25 Sep 2025 15:44:54 -0600 Subject: [PATCH 2/3] =?UTF-8?q?Add:=20M=C3=B3dulo=20catalogo=20de=20clasif?= =?UTF-8?q?icaciones=20de=20almacenes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent.md | 200 ++++++++ src/components/Holos/Button/Button.vue | 138 ++++++ src/components/Holos/Card/Card.vue | 42 ++ src/components/Holos/Card/CardContent.vue | 22 + src/components/Holos/Card/CardDescription.vue | 23 + src/components/Holos/Card/CardFooter.vue | 24 + src/components/Holos/Card/CardHeader.vue | 24 + src/components/Holos/Card/CardTitle.vue | 24 + src/components/Holos/SectionTitle.vue | 5 +- src/layouts/AdminLayout.vue | 5 + src/pages/WarehouseClassifications/Create.vue | 89 ++++ src/pages/WarehouseClassifications/Edit.vue | 141 ++++++ src/pages/WarehouseClassifications/Form.vue | 68 +++ src/pages/WarehouseClassifications/Index.vue | 212 ++++++++ .../WarehouseClassifications/Modals/Show.vue | 362 ++++++++++++++ src/pages/WarehouseClassifications/Module.js | 23 + .../warehouse-classifications.interfaces.js | 62 +++ .../WarehouseClassificationService.js | 187 +++++++ src/pages/Warehouses/Index.vue | 461 ++++-------------- src/router/Index.js | 34 ++ src/stores/WarehouseClassifications.js | 218 +++++++++ tailwind.config.js | 82 ++++ 22 files changed, 2087 insertions(+), 359 deletions(-) create mode 100644 agent.md create mode 100644 src/components/Holos/Button/Button.vue create mode 100644 src/components/Holos/Card/Card.vue create mode 100644 src/components/Holos/Card/CardContent.vue create mode 100644 src/components/Holos/Card/CardDescription.vue create mode 100644 src/components/Holos/Card/CardFooter.vue create mode 100644 src/components/Holos/Card/CardHeader.vue create mode 100644 src/components/Holos/Card/CardTitle.vue create mode 100644 src/pages/WarehouseClassifications/Create.vue create mode 100644 src/pages/WarehouseClassifications/Edit.vue create mode 100644 src/pages/WarehouseClassifications/Form.vue create mode 100644 src/pages/WarehouseClassifications/Index.vue create mode 100644 src/pages/WarehouseClassifications/Modals/Show.vue create mode 100644 src/pages/WarehouseClassifications/Module.js create mode 100644 src/pages/WarehouseClassifications/interfaces/warehouse-classifications.interfaces.js create mode 100644 src/pages/WarehouseClassifications/services/WarehouseClassificationService.js create mode 100644 src/stores/WarehouseClassifications.js create mode 100644 tailwind.config.js diff --git a/agent.md b/agent.md new file mode 100644 index 0000000..9b02518 --- /dev/null +++ b/agent.md @@ -0,0 +1,200 @@ +# Prompt: Generador de Módulos CRUD siguiendo la Estructura de Users + +Eres un agente especializado en crear módulos CRUD completos para aplicaciones Vue 3 + Composition API siguiendo la estructura establecida en el módulo Users como referencia. + +## 🏗️ ESTRUCTURA OBLIGATORIA + +Cada módulo debe seguir exactamente esta estructura de archivos: + +``` +ModuleName/ +├── Index.vue # Vista principal con listado paginado +├── Create.vue # Formulario de creación +├── Edit.vue # Formulario de edición +├── Form.vue # Componente de formulario compartido +├── Settings.vue # Configuraciones (si aplica) +├── Module.js # Utilidades centralizadas del módulo +├── Modals/ +│ └── Show.vue # Modal para mostrar detalles +└── [OtrosArchivos].vue # Archivos específicos del módulo +``` + +## 📋 ESPECIFICACIONES TÉCNICAS + +### 1. **Module.js - Archivo Central** (OBLIGATORIO) +```javascript +import { lang } from '@Lang/i18n'; +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`admin.{module}.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ + name: `admin.{module}.${name}`, params, query +}) + +// Obtener traducción del componente +const transl = (str) => lang(`{module}.${str}`) + +// Control de permisos +const can = (permission) => hasPermission(`{module}.${permission}`) + +export { can, viewTo, apiTo, transl } +``` + +### 2. **Index.vue - Vista Principal** (OBLIGATORIO) +Debe incluir: +- ✅ `useSearcher` para búsqueda paginada +- ✅ `SearcherHead` con título y botones de acción +- ✅ `Table` component con templates: `#head`, `#body`, `#empty` +- ✅ Modales para `Show` y `Destroy` +- ✅ Control de permisos para cada acción (`can()`) +- ✅ Acciones estándar: Ver, Editar, Eliminar, Settings (si aplica) + +### 3. **Create.vue - Creación** (OBLIGATORIO) +```javascript +// Estructura mínima: +const form = useForm({ + // campos del formulario +}); + +function submit() { + form.post(apiTo('store'), { + onSuccess: () => { + Notify.success(Lang('register.create.onSuccess')) + router.push(viewTo({ name: 'index' })); + } + }) +} +``` + +### 4. **Edit.vue - Edición** (OBLIGATORIO) +```javascript +// Estructura mínima: +const form = useForm({ + id: null, + // otros campos +}); + +function submit() { + form.put(apiTo('update', { [resource]: form.id }), { + onSuccess: () => { + Notify.success(Lang('register.edit.onSuccess')) + router.push(viewTo({ name: 'index' })); + }, + }) +} + +onMounted(() => { + api.get(apiTo('show', { [resource]: vroute.params.id }), { + onSuccess: (r) => form.fill(r.[resource]) + }); +}) +``` + +### 5. **Form.vue - Formulario Compartido** (OBLIGATORIO) +- ✅ Props: `action` (create/update), `form` (objeto de formulario) +- ✅ Emit: `submit` evento +- ✅ Grid responsive: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4` +- ✅ Descripción dinámica: `transl(\`\${action}.description\`)` +- ✅ Slot para campos adicionales +- ✅ Botón de submit con estado de carga + +### 6. **Modals/Show.vue** (OBLIGATORIO) +- ✅ `ShowModal` base component +- ✅ `defineExpose({ open: (data) => {...} })` +- ✅ Header con información principal +- ✅ Detalles organizados con iconos +- ✅ Enlaces de contacto (email, teléfono) +- ✅ Fechas formateadas con `getDateTime` + +## 🎨 PATRONES DE DISEÑO OBLIGATORIOS + +### **Imports Estándar** +```javascript +// Vue +import { onMounted, ref } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; + +// Services +import { api, useForm, useSearcher } from '@Services/Api'; + +// Module +import { apiTo, transl, viewTo, can } from './Module'; + +// Components +import IconButton from '@Holos/Button/Icon.vue' +import PageHeader from '@Holos/PageHeader.vue'; +``` + +### **Control de Permisos** +```html + + + +``` + +### **Navegación Consistente** +```html + + + + + + +``` + +### **Grid Responsive** +```html + +
+``` + +## 🔧 COMPONENTES REUTILIZABLES OBLIGATORIOS + +### **De @Holos:** +- `Button/Icon.vue`, `Button/Primary.vue` +- `Form/Input.vue`, `Form/Selectable.vue` +- `Modal/Template/Destroy.vue`, `Modal/Show.vue` +- `PageHeader.vue`, `Searcher.vue`, `Table.vue` +- `FormSection.vue`, `SectionBorder.vue` + +### **De @Services:** +- `useForm`, `useSearcher`, `api` + +### **De @Plugins:** +- `hasPermission`, sistema de roles + +## 📝 INSTRUCCIONES PARA EL AGENTE + +Cuando se te solicite crear un módulo: + +1. **PREGUNTA PRIMERO:** + - Nombre del módulo + - Campos principales del formulario + - Relaciones con otros módulos + - Permisos específicos necesarios + - Si requiere configuraciones especiales + +2. **GENERA SIEMPRE:** + - Todos los archivos de la estructura obligatoria + - Module.js con las 4 funciones principales + - Permisos específicos del módulo + - Traducciones básicas necesarias + +3. **RESPETA SIEMPRE:** + - Los patrones de naming + - La estructura de componentes + - Los imports estándar + - El sistema de permisos + - La navegación consistente + +4. **VALIDA QUE:** + - Todos los archivos tengan la estructura correcta + - Los permisos estén implementados + - Las rutas sean consistentes + - Los formularios tengan validación + - Los modales funcionen correctamente + +¿Entendido? Responde "ESTRUCTURA CONFIRMADA" y luego solicita los detalles del módulo que debo crear. \ No newline at end of file diff --git a/src/components/Holos/Button/Button.vue b/src/components/Holos/Button/Button.vue new file mode 100644 index 0000000..128be0b --- /dev/null +++ b/src/components/Holos/Button/Button.vue @@ -0,0 +1,138 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/Card.vue b/src/components/Holos/Card/Card.vue new file mode 100644 index 0000000..6dd6b64 --- /dev/null +++ b/src/components/Holos/Card/Card.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/CardContent.vue b/src/components/Holos/Card/CardContent.vue new file mode 100644 index 0000000..151396d --- /dev/null +++ b/src/components/Holos/Card/CardContent.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/CardDescription.vue b/src/components/Holos/Card/CardDescription.vue new file mode 100644 index 0000000..db4bbad --- /dev/null +++ b/src/components/Holos/Card/CardDescription.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/CardFooter.vue b/src/components/Holos/Card/CardFooter.vue new file mode 100644 index 0000000..4cc1605 --- /dev/null +++ b/src/components/Holos/Card/CardFooter.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/CardHeader.vue b/src/components/Holos/Card/CardHeader.vue new file mode 100644 index 0000000..a3081c3 --- /dev/null +++ b/src/components/Holos/Card/CardHeader.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/Card/CardTitle.vue b/src/components/Holos/Card/CardTitle.vue new file mode 100644 index 0000000..e855a2f --- /dev/null +++ b/src/components/Holos/Card/CardTitle.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/SectionTitle.vue b/src/components/Holos/SectionTitle.vue index 1b0b91e..afff327 100644 --- a/src/components/Holos/SectionTitle.vue +++ b/src/components/Holos/SectionTitle.vue @@ -1,11 +1,11 @@ + diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 09880a8..58fc3a2 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -88,6 +88,11 @@ onMounted(() => { name="Almacén" to="admin.warehouses.index" /> +
+import { onMounted, ref } from 'vue'; +import { useRouter, useRoute } 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/WarehouseClassifications/Edit.vue b/src/pages/WarehouseClassifications/Edit.vue new file mode 100644 index 0000000..b165782 --- /dev/null +++ b/src/pages/WarehouseClassifications/Edit.vue @@ -0,0 +1,141 @@ + + + \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/Form.vue b/src/pages/WarehouseClassifications/Form.vue new file mode 100644 index 0000000..bf02116 --- /dev/null +++ b/src/pages/WarehouseClassifications/Form.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/Index.vue b/src/pages/WarehouseClassifications/Index.vue new file mode 100644 index 0000000..1b4aab2 --- /dev/null +++ b/src/pages/WarehouseClassifications/Index.vue @@ -0,0 +1,212 @@ + + + \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/Modals/Show.vue b/src/pages/WarehouseClassifications/Modals/Show.vue new file mode 100644 index 0000000..9e33a2f --- /dev/null +++ b/src/pages/WarehouseClassifications/Modals/Show.vue @@ -0,0 +1,362 @@ + + + \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/Module.js b/src/pages/WarehouseClassifications/Module.js new file mode 100644 index 0000000..f7caf91 --- /dev/null +++ b/src/pages/WarehouseClassifications/Module.js @@ -0,0 +1,23 @@ +import { lang } from '@Lang/i18n'; +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`warehouse-classifications.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ + name: `admin.warehouse-classifications.${name}`, params, query +}) + +// Obtener traducción del componente +const transl = (str) => lang(`admin.warehouse_classifications.${str}`) + +// Control de permisos +const can = (permission) => hasPermission(`admin.warehouse-classifications.${permission}`) + +export { + can, + viewTo, + apiTo, + transl +} \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/interfaces/warehouse-classifications.interfaces.js b/src/pages/WarehouseClassifications/interfaces/warehouse-classifications.interfaces.js new file mode 100644 index 0000000..767608b --- /dev/null +++ b/src/pages/WarehouseClassifications/interfaces/warehouse-classifications.interfaces.js @@ -0,0 +1,62 @@ +/** + * Interfaces para Warehouse Classifications + * + * @author Sistema + * @version 1.0.0 + */ + +/** + * @typedef {Object} WarehouseClassification + * @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 {WarehouseClassification|null} parent - Clasificación padre + * @property {WarehouseClassification[]} children - Clasificaciones hijas + */ + +/** + * @typedef {Object} WarehouseClassificationResponse + * @property {string} status - Estado de la respuesta + * @property {Object} data - Datos de la respuesta + * @property {string} data.message - Mensaje de la respuesta + * @property {WarehouseClassification} data.warehouse_classification - Clasificación de almacén + */ + +/** + * @typedef {Object} WarehouseClassificationsListResponse + * @property {string} status - Estado de la respuesta + * @property {Object} data - Datos de la respuesta + * @property {WarehouseClassification[]} data.warehouse_classifications - Lista de clasificaciones + */ + +/** + * @typedef {Object} CreateWarehouseClassificationData + * @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} UpdateWarehouseClassificationData + * @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 { + WarehouseClassification, + WarehouseClassificationResponse, + WarehouseClassificationsListResponse, + CreateWarehouseClassificationData, + UpdateWarehouseClassificationData +}; \ No newline at end of file diff --git a/src/pages/WarehouseClassifications/services/WarehouseClassificationService.js b/src/pages/WarehouseClassifications/services/WarehouseClassificationService.js new file mode 100644 index 0000000..f92f8fd --- /dev/null +++ b/src/pages/WarehouseClassifications/services/WarehouseClassificationService.js @@ -0,0 +1,187 @@ +/** + * Servicio para Warehouse Classifications + * + * @author Sistema + * @version 1.0.0 + */ + +import { api, apiURL } from '@Services/Api'; + +export default class WarehouseClassificationService { + + /** + * Obtener todas las clasificaciones de almacén + * @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('catalogs/warehouse-classifications'), { + params, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Obtener una clasificación de almacén 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(`catalogs/warehouse-classifications/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Crear una nueva clasificación de almacén + * @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|null} data.description - Descripción de la clasificación + * @param {boolean} data.is_active - Estado activo/inactivo + * @param {number|null} data.parent_id - ID del padre + * @returns {Promise} Promesa con la respuesta + */ + async create(data) { + return new Promise((resolve, reject) => { + api.post(apiURL('catalogs/warehouse-classifications'), { + data, + onSuccess: (response) => { + resolve(response); + }, + onError: (error) => { reject(error); } + }); + }); + } + + /** + * Actualizar una clasificación de almacén + * @param {number} id - ID de la clasificación + * @param {Object} data - Datos a actualizar + * @param {string} [data.code] - Código de la clasificación + * @param {string} [data.name] - Nombre de la clasificación + * @param {string|null} [data.description] - Descripción de la clasificación + * @param {boolean} [data.is_active] - Estado activo/inactivo + * @param {number|null} [data.parent_id] - ID del padre + * @returns {Promise} Promesa con la respuesta + */ + async update(id, data) { + return new Promise((resolve, reject) => { + api.put(apiURL(`catalogs/warehouse-classifications/${id}`), { + data, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Actualizar solo el estado de una clasificación de almacén + * @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(`catalogs/warehouse-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); + } + }); + }); + } + + /** + * Eliminar una clasificación de almacén + * @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(`catalogs/warehouse-classifications/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * 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('catalogs/warehouse-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.warehouse_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/Warehouses/Index.vue b/src/pages/Warehouses/Index.vue index 8fba40c..0d3051b 100644 --- a/src/pages/Warehouses/Index.vue +++ b/src/pages/Warehouses/Index.vue @@ -1,386 +1,133 @@ \ No newline at end of file diff --git a/src/router/Index.js b/src/router/Index.js index 79ba040..4b9db26 100644 --- a/src/router/Index.js +++ b/src/router/Index.js @@ -343,6 +343,40 @@ const router = createRouter({ } ] }, + { + path: 'warehouse-classifications', + name: 'admin.warehouse-classifications', + meta: { + title: 'Clasificaciones de Bodegas', + icon: 'category', + }, + redirect: '/admin/warehouse-classifications', + children: [ + { + path: '', + name: 'admin.warehouse-classifications.index', + component: () => import('@Pages/WarehouseClassifications/Index.vue'), + }, + { + path: 'create', + name: 'admin.warehouse-classifications.create', + component: () => import('@Pages/WarehouseClassifications/Create.vue'), + meta: { + title: 'Crear', + icon: 'add', + }, + }, + { + path: ':id/edit', + name: 'admin.warehouse-classifications.edit', + component: () => import('@Pages/WarehouseClassifications/Edit.vue'), + meta: { + title: 'Editar', + icon: 'edit', + }, + } + ] + }, { path: 'roles', name: 'admin.roles', diff --git a/src/stores/WarehouseClassifications.js b/src/stores/WarehouseClassifications.js new file mode 100644 index 0000000..efc57ee --- /dev/null +++ b/src/stores/WarehouseClassifications.js @@ -0,0 +1,218 @@ +import { defineStore } from 'pinia' +import { api } from '../services/Api' + +// Store para las clasificaciones de almacén +const useWarehouseClassifications = defineStore('warehouseClassifications', { + state: () => ({ + classifications: [], + loading: false, + error: null + }), + + getters: { + // Obtener todas las clasificaciones + allClassifications(state) { + return state.classifications + }, + + // Obtener solo las clasificaciones principales (sin padre) + mainClassifications(state) { + return state.classifications.filter(c => !c.parent_id) + }, + + // Obtener clasificaciones por estado + activeClassifications(state) { + return state.classifications.filter(c => c.is_active) + }, + + // Obtener subcategorías de una clasificación específica + getSubcategoriesByParentId: (state) => (parentId) => { + return state.classifications.filter(c => c.parent_id === parentId) + }, + + // Obtener una clasificación por ID + getClassificationById: (state) => (id) => { + return state.classifications.find(c => c.id === id) + }, + + // Verificar si está cargando + isLoading(state) { + return state.loading + }, + + // Obtener error actual + currentError(state) { + return state.error + } + }, + + actions: { + // Cargar todas las clasificaciones + async fetchClassifications() { + this.loading = true + this.error = null + + try { + const response = await api.get('/warehouse-classifications') + this.classifications = response.data.data || response.data + return response.data + } catch (error) { + this.error = error.message || 'Error al cargar las clasificaciones' + console.error('Error fetching classifications:', error) + throw error + } finally { + this.loading = false + } + }, + + // Crear nueva clasificación + async createClassification(data) { + this.loading = true + this.error = null + + try { + const response = await api.post('/warehouse-classifications', data) + const newClassification = response.data.data || response.data + + // Agregar la nueva clasificación al estado + this.classifications.push(newClassification) + + // Si tiene parent_id, actualizar la lista de children del padre + if (newClassification.parent_id) { + const parent = this.getClassificationById(newClassification.parent_id) + if (parent) { + if (!parent.children) { + parent.children = [] + } + parent.children.push(newClassification) + } + } + + return response.data + } catch (error) { + this.error = error.message || 'Error al crear la clasificación' + console.error('Error creating classification:', error) + throw error + } finally { + this.loading = false + } + }, + + // Actualizar clasificación + async updateClassification(id, data) { + this.loading = true + this.error = null + + try { + const response = await api.put(`/warehouse-classifications/${id}`, data) + const updatedClassification = response.data.data || response.data + + // Actualizar en el estado local + const index = this.classifications.findIndex(c => c.id === id) + if (index !== -1) { + this.classifications[index] = { ...this.classifications[index], ...updatedClassification } + } + + // También actualizar en children si existe + this.classifications.forEach(classification => { + if (classification.children) { + const childIndex = classification.children.findIndex(c => c.id === id) + if (childIndex !== -1) { + classification.children[childIndex] = { ...classification.children[childIndex], ...updatedClassification } + } + } + }) + + return response.data + } catch (error) { + this.error = error.message || 'Error al actualizar la clasificación' + console.error('Error updating classification:', error) + throw error + } finally { + this.loading = false + } + }, + + // Eliminar clasificación + async deleteClassification(id) { + this.loading = true + this.error = null + + try { + await api.delete(`/warehouse-classifications/${id}`) + + // Remover del estado local + this.classifications = this.classifications.filter(c => c.id !== id) + + // También remover de children si existe + this.classifications.forEach(classification => { + if (classification.children) { + classification.children = classification.children.filter(c => c.id !== id) + } + }) + + return true + } catch (error) { + this.error = error.message || 'Error al eliminar la clasificación' + console.error('Error deleting classification:', error) + throw error + } finally { + this.loading = false + } + }, + + // Cambiar estado de una clasificación + async toggleClassificationStatus(classification) { + this.loading = true + this.error = null + + try { + const newStatus = !classification.is_active + const response = await api.put(`/warehouse-classifications/${classification.id}`, { + ...classification, + is_active: newStatus + }) + + const updatedClassification = response.data.data || response.data + + // Actualizar en el estado local + const index = this.classifications.findIndex(c => c.id === classification.id) + if (index !== -1) { + this.classifications[index].is_active = newStatus + } + + // También actualizar en children si existe + this.classifications.forEach(parentClassification => { + if (parentClassification.children) { + const childIndex = parentClassification.children.findIndex(c => c.id === classification.id) + if (childIndex !== -1) { + parentClassification.children[childIndex].is_active = newStatus + } + } + }) + + return response.data + } catch (error) { + this.error = error.message || 'Error al cambiar el estado' + console.error('Error toggling status:', error) + throw error + } finally { + this.loading = false + } + }, + + // Limpiar errores + clearError() { + this.error = null + }, + + // Reset del store + $reset() { + this.classifications = [] + this.loading = false + this.error = null + } + } +}) + +export { useWarehouseClassifications } \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..9c7debf --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,82 @@ +// tailwind.config.js +import { defineConfig } from 'tailwindcss'; + +export default defineConfig({ + theme: { + extend: { + colors: { + primary: { + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + }, + secondary: { + 100: '#dbeafe', + 200: '#bfdbfe', + 300: '#93c5fd', + 400: '#60a5fa', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + 800: '#1e40af', + 900: '#1e3a8a', + DEFAULT: '#3b82f6', + }, + info: { + 100: '#cffafe', + 200: '#a5f3fc', + 300: '#67e8f9', + 400: '#22d3ee', + 500: '#06b6d4', + 600: '#0891b2', + 700: '#0e7490', + 800: '#155e75', + 900: '#164e63', + DEFAULT: '#06b6d4', + }, + success: { + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + DEFAULT: '#22c55e', + }, + danger: { + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + DEFAULT: '#ef4444', + }, + warning: { + 100: '#fef9c3', + 200: '#fef08a', + 300: '#fde047', + 400: '#facc15', + 500: '#eab308', + 600: '#ca8a04', + 700: '#a16207', + 800: '#854d0e', + 900: '#713f12', + DEFAULT: '#eab308', + }, + }, + }, + }, +}); \ No newline at end of file From 6b7f80d53a11c3c38e538b08605be1632c7ad747 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Tue, 30 Sep 2025 10:28:36 -0600 Subject: [PATCH 3/3] Add: Feature warehouse --- src/components/Holos/Button/Button.vue | 11 +- src/layouts/AdminLayout.vue | 5 + src/pages/UnitsMeasure/Create.vue | 63 + src/pages/UnitsMeasure/Edit.vue | 104 ++ src/pages/UnitsMeasure/Form.vue | 108 ++ src/pages/UnitsMeasure/Index.vue | 124 ++ src/pages/UnitsMeasure/Modals/Show.vue | 210 +++ src/pages/UnitsMeasure/Module.js | 23 + .../services/UnitsMeasureService.js | 227 ++++ src/pages/Warehouses/Create.vue | 66 + src/pages/Warehouses/Details.vue | 457 +++++++ src/pages/Warehouses/Edit.vue | 129 ++ src/pages/Warehouses/Form.vue | 338 +++++ src/pages/Warehouses/Index.vue | 241 ++-- src/pages/Warehouses/Modals/Show.vue | 311 +++++ src/pages/Warehouses/Module.js | 13 +- src/pages/Warehouses/e.jsx | 1125 +++++++++++++++++ .../interfaces/warehouses.interfaces.js | 125 ++ .../Warehouses/services/WarehouseService.js | 239 ++++ src/router/Index.js | 69 +- 20 files changed, 3873 insertions(+), 115 deletions(-) create mode 100644 src/pages/UnitsMeasure/Create.vue create mode 100644 src/pages/UnitsMeasure/Edit.vue create mode 100644 src/pages/UnitsMeasure/Form.vue create mode 100644 src/pages/UnitsMeasure/Index.vue create mode 100644 src/pages/UnitsMeasure/Modals/Show.vue create mode 100644 src/pages/UnitsMeasure/Module.js create mode 100644 src/pages/UnitsMeasure/services/UnitsMeasureService.js create mode 100644 src/pages/Warehouses/Create.vue create mode 100644 src/pages/Warehouses/Details.vue create mode 100644 src/pages/Warehouses/Edit.vue create mode 100644 src/pages/Warehouses/Form.vue create mode 100644 src/pages/Warehouses/Modals/Show.vue create mode 100644 src/pages/Warehouses/e.jsx create mode 100644 src/pages/Warehouses/interfaces/warehouses.interfaces.js create mode 100644 src/pages/Warehouses/services/WarehouseService.js diff --git a/src/components/Holos/Button/Button.vue b/src/components/Holos/Button/Button.vue index 128be0b..362eba4 100644 --- a/src/components/Holos/Button/Button.vue +++ b/src/components/Holos/Button/Button.vue @@ -23,6 +23,7 @@ interface Props { loading?: boolean; fullWidth?: boolean; iconOnly?: boolean; + asLink?: boolean; // Nueva prop para comportamiento de link } const props = withDefaults(defineProps(), { @@ -34,6 +35,7 @@ const props = withDefaults(defineProps(), { loading: false, fullWidth: false, iconOnly: false, + asLink: true, // Por defecto no es link }); @@ -42,8 +44,15 @@ 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(() => { diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 58fc3a2..2b26cc4 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -93,6 +93,11 @@ onMounted(() => { name="Clasificaciones de almacenes" to="admin.warehouse-classifications.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: '', + abbreviation: '', + type: 4, // Unidad por defecto + is_active: true +}); + +/** Métodos */ +function submit() { + form.transform(data => ({ + ...data, + type: data.type?.value || data.type, // Extraer solo el value si es objeto, sino usar tal como está + is_active: data.is_active ? 1 : 0 // Convertir boolean a número + })).post(apiTo('store'), { + onSuccess: () => { + Notify.success('Unidad de medida creada con éxito') + router.push(viewTo({ name: 'index' })); + } + }) +} + +/** Ciclos */ +onMounted(() => { + // Inicialización si es necesaria +}); + + \ No newline at end of file diff --git a/src/pages/UnitsMeasure/Edit.vue b/src/pages/UnitsMeasure/Edit.vue new file mode 100644 index 0000000..d18fd41 --- /dev/null +++ b/src/pages/UnitsMeasure/Edit.vue @@ -0,0 +1,104 @@ + + + \ No newline at end of file diff --git a/src/pages/UnitsMeasure/Form.vue b/src/pages/UnitsMeasure/Form.vue new file mode 100644 index 0000000..7e3f0bb --- /dev/null +++ b/src/pages/UnitsMeasure/Form.vue @@ -0,0 +1,108 @@ + + + \ No newline at end of file diff --git a/src/pages/UnitsMeasure/Index.vue b/src/pages/UnitsMeasure/Index.vue new file mode 100644 index 0000000..0d66f19 --- /dev/null +++ b/src/pages/UnitsMeasure/Index.vue @@ -0,0 +1,124 @@ + + + \ No newline at end of file diff --git a/src/pages/UnitsMeasure/Modals/Show.vue b/src/pages/UnitsMeasure/Modals/Show.vue new file mode 100644 index 0000000..e025418 --- /dev/null +++ b/src/pages/UnitsMeasure/Modals/Show.vue @@ -0,0 +1,210 @@ + + + \ No newline at end of file diff --git a/src/pages/UnitsMeasure/Module.js b/src/pages/UnitsMeasure/Module.js new file mode 100644 index 0000000..2130417 --- /dev/null +++ b/src/pages/UnitsMeasure/Module.js @@ -0,0 +1,23 @@ +import { lang } from '@Lang/i18n'; +import { hasPermission } from '@Plugins/RolePermission.js'; + +// Ruta API +const apiTo = (name, params = {}) => route(`units-of-measure.${name}`, params) + +// Ruta visual +const viewTo = ({ name = '', params = {}, query = {} }) => view({ + name: `admin.units-measure.${name}`, params, query +}) + +// Obtener traducción del componente +const transl = (str) => lang(`admin.units_measure.${str}`) + +// Control de permisos +const can = (permission) => hasPermission(`admin.units-measure.${permission}`) + +export { + can, + viewTo, + apiTo, + transl +} \ No newline at end of file diff --git a/src/pages/UnitsMeasure/services/UnitsMeasureService.js b/src/pages/UnitsMeasure/services/UnitsMeasureService.js new file mode 100644 index 0000000..fc0f3a6 --- /dev/null +++ b/src/pages/UnitsMeasure/services/UnitsMeasureService.js @@ -0,0 +1,227 @@ +/** + * Servicio para Units of Measure + * + * @author Sistema + * @version 1.0.0 + */ + +import { api, apiURL } from '@Services/Api'; + +export default class UnitsMeasureService { + + /** + * Obtener todas las unidades de medida + * @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('catalogs/units-of-measure'), { + params, + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Obtener una unidad de medida por ID + * @param {number} id - ID de la unidad de medida + * @returns {Promise} Promesa con la respuesta + */ + async getById(id) { + return new Promise((resolve, reject) => { + api.get(apiURL(`catalogs/units-of-measure/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Crear una nueva unidad de medida + * @param {Object} data - Datos de la unidad de medida + * @param {string} data.code - Código de la unidad + * @param {string} data.name - Nombre de la unidad + * @param {string} data.abbreviation - Abreviación de la unidad + * @param {number} data.type - Tipo de medida (1-7) + * @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('catalogs/units-of-measure'), data, { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Actualizar una unidad de medida existente + * @param {number} id - ID de la unidad de medida + * @param {Object} data - Datos actualizados + * @returns {Promise} Promesa con la respuesta + */ + async update(id, data) { + return new Promise((resolve, reject) => { + api.put(apiURL(`catalogs/units-of-measure/${id}`), data, { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Eliminar una unidad de medida + * @param {number} id - ID de la unidad de medida + * @returns {Promise} Promesa con la respuesta + */ + async delete(id) { + return new Promise((resolve, reject) => { + api.delete(apiURL(`catalogs/units-of-measure/${id}`), { + onSuccess: (response) => resolve(response), + onError: (error) => reject(error) + }); + }); + } + + /** + * Actualizar el estado de una unidad de medida + * @param {number} id - ID de la unidad de medida + * @param {boolean} isActive - Nuevo estado + * @returns {Promise} Promesa con la respuesta + */ + async updateStatus(id, isActive) { + return new Promise((resolve, reject) => { + api.put(apiURL(`catalogs/units-of-measure/${id}`), { + data: { is_active: isActive }, + onSuccess: (response) => { + resolve(response); + }, + onError: (error) => { + // Mejorar el manejo de errores + const enhancedError = { + ...error, + timestamp: new Date().toISOString(), + action: 'updateStatus', + id: id, + is_active: isActive + }; + reject(enhancedError); + } + }); + }); + } + + /** + * Obtener tipos de medida disponibles desde la API + * @returns {Promise} Promesa con la respuesta + */ + async getUnitTypes() { + return new Promise((resolve, reject) => { + api.get(apiURL('catalogs/unit-types'), { + onSuccess: (response) => { + // Extraer los tipos de la respuesta y formatearlos para el Selectable + const unitTypes = response.data?.unit_types || response.unit_types || []; + const formattedTypes = unitTypes.map(type => ({ + value: type.id, + label: type.name + })); + resolve(formattedTypes); + }, + onError: (error) => reject(error) + }); + }); + } + + /** + * Obtener tipos de medida como objetos completos (para mapping) + * @returns {Promise} Promesa con los tipos completos + */ + async getUnitTypesMap() { + return new Promise((resolve, reject) => { + api.get(apiURL('catalogs/unit-types'), { + onSuccess: (response) => { + const unitTypes = response.data?.unit_types || response.unit_types || []; + // Crear un mapa para acceso rápido por ID + const typesMap = {}; + unitTypes.forEach(type => { + typesMap[type.id] = type; + }); + resolve(typesMap); + }, + onError: (error) => reject(error) + }); + }); + } + + /** + * Obtener tipos de medida disponibles (método legacy - mantener por compatibilidad) + * @returns {Array} Array con los tipos de medida + */ + getTypes() { + return [ + { id: 1, name: 'Distancia', description: 'Medidas de longitud (metros, kilómetros, etc.)' }, + { id: 2, name: 'Peso', description: 'Medidas de masa (kilogramos, gramos, etc.)' }, + { id: 3, name: 'Temperatura', description: 'Medidas de temperatura (grados Celsius, etc.)' }, + { id: 4, name: 'Unidad', description: 'Unidades discretas (piezas, unidades, etc.)' }, + { id: 5, name: 'Volumen', description: 'Medidas de volumen (litros, mililitros, etc.)' } + ]; + } + + /** + * Obtener unidades de medida activas + * @returns {Promise} Promesa con las unidades activas + */ + async getActive() { + return new Promise((resolve, reject) => { + this.getAll({ is_active: true }) + .then(response => { + const activeUnits = response.data?.units_of_measure?.data || []; + resolve(activeUnits.filter(unit => unit.is_active)); + }) + .catch(error => reject(error)); + }); + } + + /** + * Buscar unidades de medida por término + * @param {string} term - Término de búsqueda + * @returns {Promise} Promesa con los resultados + */ + async search(term) { + return new Promise((resolve, reject) => { + this.getAll({ search: term }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); + } + + /** + * Validar datos antes de enviar + * @param {Object} data - Datos a validar + * @returns {Object} Objeto con errores si los hay + */ + validate(data) { + const errors = {}; + + if (!data.code || data.code.trim() === '') { + errors.code = 'El código es requerido'; + } + + if (!data.name || data.name.trim() === '') { + errors.name = 'El nombre es requerido'; + } + + if (!data.abbreviation || data.abbreviation.trim() === '') { + errors.abbreviation = 'La abreviación es requerida'; + } + + if (!data.type || ![1, 2, 3, 4, 5, 6, 7].includes(data.type)) { + errors.type = 'El tipo de medida es requerido y debe ser válido'; + } + + return errors; + } +} \ No newline at end of file diff --git a/src/pages/Warehouses/Create.vue b/src/pages/Warehouses/Create.vue new file mode 100644 index 0000000..e719482 --- /dev/null +++ b/src/pages/Warehouses/Create.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/src/pages/Warehouses/Details.vue b/src/pages/Warehouses/Details.vue new file mode 100644 index 0000000..910e705 --- /dev/null +++ b/src/pages/Warehouses/Details.vue @@ -0,0 +1,457 @@ + + + \ No newline at end of file diff --git a/src/pages/Warehouses/Edit.vue b/src/pages/Warehouses/Edit.vue new file mode 100644 index 0000000..1adc8dd --- /dev/null +++ b/src/pages/Warehouses/Edit.vue @@ -0,0 +1,129 @@ + + + \ No newline at end of file diff --git a/src/pages/Warehouses/Form.vue b/src/pages/Warehouses/Form.vue new file mode 100644 index 0000000..15f8ba6 --- /dev/null +++ b/src/pages/Warehouses/Form.vue @@ -0,0 +1,338 @@ + + +