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/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/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/components/Holos/Button/Button.vue b/src/components/Holos/Button/Button.vue new file mode 100644 index 0000000..362eba4 --- /dev/null +++ b/src/components/Holos/Button/Button.vue @@ -0,0 +1,147 @@ + + + \ 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 0b5a612..2b26cc4 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -81,6 +81,24 @@ onMounted(() => { to="admin.vacations.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/WarehouseClassifications/Create.vue b/src/pages/WarehouseClassifications/Create.vue new file mode 100644 index 0000000..4b08382 --- /dev/null +++ b/src/pages/WarehouseClassifications/Create.vue @@ -0,0 +1,89 @@ + + + \ 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/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 @@ + + +