From efcad3fe1daeb90d037ac8ec451452fd2393cc6c Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Thu, 25 Sep 2025 15:44:54 -0600 Subject: [PATCH] =?UTF-8?q?Add:=20M=C3=B3dulo=20catalogo=20de=20clasificac?= =?UTF-8?q?iones=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