Compare commits
No commits in common. "feature-warehouse-model" and "main" have entirely different histories.
feature-wa
...
main
@ -1,13 +1,12 @@
|
|||||||
VITE_API_URL=http://localhost:8080
|
VITE_API_URL=http://backend.holos.test:8080
|
||||||
VITE_BASE_URL=http://localhost:3000
|
VITE_BASE_URL=http://frontend.holos.test
|
||||||
|
|
||||||
VITE_REVERB_APP_ID=
|
VITE_REVERB_APP_ID=
|
||||||
VITE_REVERB_APP_KEY=
|
VITE_REVERB_APP_KEY=
|
||||||
VITE_REVERB_APP_SECRET=
|
VITE_REVERB_APP_SECRET=
|
||||||
VITE_REVERB_HOST="localhost"
|
VITE_REVERB_HOST="backend.holos.test"
|
||||||
VITE_REVERB_PORT=8080
|
VITE_REVERB_PORT=8080
|
||||||
VITE_REVERB_SCHEME=http
|
VITE_REVERB_SCHEME=http
|
||||||
VITE_REVERB_ACTIVE=false
|
VITE_REVERB_ACTIVE=false
|
||||||
|
|
||||||
APP_PORT=3000
|
APP_PORT=3000
|
||||||
|
|
||||||
|
|||||||
200
agent.md
200
agent.md
@ -1,200 +0,0 @@
|
|||||||
# 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
|
|
||||||
<!-- Siempre usar can() del módulo -->
|
|
||||||
<RouterLink v-if="can('create')" :to="viewTo({ name: 'create' })">
|
|
||||||
<IconButton v-if="can('destroy')" @click="destroyModal.open(model)">
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Navegación Consistente**
|
|
||||||
```html
|
|
||||||
<!-- Header con botón de retorno -->
|
|
||||||
<PageHeader :title="transl('create.title')">
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton icon="arrow_back" :title="$t('return')" filled />
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Grid Responsive**
|
|
||||||
```html
|
|
||||||
<!-- Formularios siempre con esta estructura -->
|
|
||||||
<form class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 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.
|
|
||||||
@ -10,7 +10,6 @@ services:
|
|||||||
- frontend-v1:/var/www/gols-frontend-v1/node_modules
|
- frontend-v1:/var/www/gols-frontend-v1/node_modules
|
||||||
networks:
|
networks:
|
||||||
- gols-network
|
- gols-network
|
||||||
mem_limit: 512m
|
|
||||||
volumes:
|
volumes:
|
||||||
frontend-v1:
|
frontend-v1:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@ -1,147 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button :type="type" :disabled="disabled || loading" :class="buttonClasses" @click="handleClick">
|
|
||||||
<svg v-if="loading" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
<slot></slot>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
variant?: 'solid' | 'outline' | 'ghost' | 'smooth';
|
|
||||||
color?: 'primary' | 'danger' | 'success' | 'info' | 'warning';
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
type?: 'button' | 'submit' | 'reset';
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
iconOnly?: boolean;
|
|
||||||
asLink?: boolean; // Nueva prop para comportamiento de link
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
variant: 'solid',
|
|
||||||
color: 'primary',
|
|
||||||
size: 'md',
|
|
||||||
type: 'button',
|
|
||||||
disabled: false,
|
|
||||||
loading: false,
|
|
||||||
fullWidth: false,
|
|
||||||
iconOnly: false,
|
|
||||||
asLink: true, // Por defecto no es link
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
click: [event: MouseEvent];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonClasses = computed(() => {
|
|
||||||
const baseClasses = [
|
|
||||||
'inline-flex',
|
|
||||||
'items-center',
|
|
||||||
'justify-center',
|
|
||||||
'font-medium',
|
|
||||||
'rounded-lg',
|
|
||||||
'transition-all',
|
|
||||||
'duration-200',
|
|
||||||
'focus:outline-none',
|
|
||||||
'focus:ring-2',
|
|
||||||
'focus:ring-offset-2',
|
|
||||||
'disabled:opacity-50',
|
|
||||||
'disabled:cursor-not-allowed',
|
|
||||||
];
|
|
||||||
|
|
||||||
const sizeClasses = {
|
|
||||||
sm: ['px-3', 'py-1.5', 'text-sm'],
|
|
||||||
md: ['px-4', 'py-2', 'text-sm'],
|
|
||||||
lg: ['px-6', 'py-3', 'text-base'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Estilos base por tipo
|
|
||||||
const variantClasses = {
|
|
||||||
solid: ['shadow-sm'],
|
|
||||||
outline: ['border', 'bg-white', 'hover:bg-gray-50'],
|
|
||||||
ghost: ['bg-transparent', 'hover:bg-gray-100'],
|
|
||||||
smooth: ['bg-opacity-20', 'font-bold', 'shadow-none'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Colores por tipo
|
|
||||||
const colorClasses = {
|
|
||||||
primary: {
|
|
||||||
solid: ['bg-primary-600', 'text-white', 'hover:bg-primary-700', 'focus:ring-primary-500'],
|
|
||||||
outline: ['border-primary-600', 'text-primary-600', 'focus:ring-primary-500'],
|
|
||||||
ghost: ['text-primary-600', 'focus:ring-primary-500'],
|
|
||||||
smooth: ['bg-primary-100', 'text-primary-600', 'hover:bg-primary-200', 'focus:ring-primary-300'],
|
|
||||||
},
|
|
||||||
danger: {
|
|
||||||
solid: ['bg-red-600', 'text-white', 'hover:bg-red-700', 'focus:ring-red-500'],
|
|
||||||
outline: ['border-red-600', 'text-red-600', 'focus:ring-red-500'],
|
|
||||||
ghost: ['text-red-600', 'focus:ring-red-500'],
|
|
||||||
smooth: ['bg-red-100', 'text-red-600', 'hover:bg-red-200', 'focus:ring-red-300'],
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
solid: ['bg-green-600', 'text-white', 'hover:bg-green-700', 'focus:ring-green-500'],
|
|
||||||
outline: ['border-green-600', 'text-green-600', 'focus:ring-green-500'],
|
|
||||||
ghost: ['text-green-600', 'focus:ring-green-500'],
|
|
||||||
smooth: ['bg-green-100', 'text-green-600', 'hover:bg-green-200', 'focus:ring-green-300'],
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
solid: ['bg-blue-600', 'text-white', 'hover:bg-blue-700', 'focus:ring-blue-500'],
|
|
||||||
outline: ['border-blue-600', 'text-blue-600', 'focus:ring-blue-500'],
|
|
||||||
ghost: ['text-blue-600', 'focus:ring-blue-500'],
|
|
||||||
smooth: ['bg-blue-100', 'text-blue-600', 'hover:bg-blue-200', 'focus:ring-blue-300'],
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
solid: ['bg-yellow-500', 'text-white', 'hover:bg-yellow-600', 'focus:ring-yellow-400'],
|
|
||||||
outline: ['border-yellow-500', 'text-yellow-600', 'focus:ring-yellow-400'],
|
|
||||||
ghost: ['text-yellow-600', 'focus:ring-yellow-400'],
|
|
||||||
smooth: ['bg-yellow-100', 'text-yellow-600', 'hover:bg-yellow-200', 'focus:ring-yellow-200'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Asegura que siempre haya un array válido para los estilos
|
|
||||||
const safeVariant = props.variant && variantClasses[props.variant] ? props.variant : 'solid';
|
|
||||||
const safeColor = props.color && colorClasses[props.color] ? props.color : 'primary';
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
...baseClasses,
|
|
||||||
...sizeClasses[props.size],
|
|
||||||
...(variantClasses[safeVariant] || []),
|
|
||||||
...(colorClasses[safeColor]?.[safeVariant] || []),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (props.fullWidth) {
|
|
||||||
classes.push('w-full');
|
|
||||||
}
|
|
||||||
if (props.iconOnly) {
|
|
||||||
if (props.size === 'sm') classes.push('w-8', 'h-8', 'p-0');
|
|
||||||
else if (props.size === 'lg') classes.push('w-12', 'h-12', 'p-0');
|
|
||||||
else classes.push('w-10', 'h-10', 'p-0');
|
|
||||||
} else if (props.fullWidth) {
|
|
||||||
classes.push('w-full');
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
variant: {
|
|
||||||
type: String,
|
|
||||||
default: 'outline',
|
|
||||||
validator: (value) => ['default', 'outline', 'ghost'].includes(value)
|
|
||||||
},
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardClasses = computed(() => {
|
|
||||||
const baseClasses = [
|
|
||||||
'rounded-lg',
|
|
||||||
'shadow-sm',
|
|
||||||
'transition-all',
|
|
||||||
'duration-200',
|
|
||||||
];
|
|
||||||
|
|
||||||
const variantClasses = {
|
|
||||||
default: ['border', 'bg-white', 'dark:bg-gray-800', 'text-gray-900', 'dark:text-gray-100'],
|
|
||||||
outline: ['border-2', 'border-gray-100', 'dark:border-gray-700', 'bg-transparent'],
|
|
||||||
ghost: ['shadow-none', 'border-none', 'bg-transparent'],
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
...baseClasses,
|
|
||||||
...variantClasses[props.variant],
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="cardClasses">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentClasses = computed(() => [
|
|
||||||
'p-6',
|
|
||||||
'pt-0',
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="contentClasses">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const descriptionClasses = computed(() => [
|
|
||||||
'text-sm',
|
|
||||||
'text-gray-600',
|
|
||||||
'dark:text-gray-400',
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<p :class="descriptionClasses">
|
|
||||||
<slot />
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const footerClasses = computed(() => [
|
|
||||||
'flex',
|
|
||||||
'items-center',
|
|
||||||
'p-6',
|
|
||||||
'pt-0',
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="footerClasses">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerClasses = computed(() => [
|
|
||||||
'flex',
|
|
||||||
'flex-col',
|
|
||||||
'space-y-1.5',
|
|
||||||
'p-6',
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="headerClasses">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
className: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const titleClasses = computed(() => [
|
|
||||||
'text-2xl',
|
|
||||||
'font-semibold',
|
|
||||||
'leading-none',
|
|
||||||
'tracking-tight',
|
|
||||||
props.className,
|
|
||||||
].filter(Boolean));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h3 :class="titleClasses">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="md:col-span-1 flex justify-between">
|
<div class="md:col-span-1 flex justify-between">
|
||||||
<div class="px-4 sm:px-0">
|
<div class="px-4 sm:px-0">
|
||||||
<h3 class="text-3xl font-extrabold text-gray-900 dark:text-primary-dt">
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">
|
<p class="mt-1 text-sm text-gray-600 dark:text-white/50">
|
||||||
<slot name="description" />
|
<slot name="description" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -15,4 +15,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -81,24 +81,6 @@ onMounted(() => {
|
|||||||
to="admin.vacations.index"
|
to="admin.vacations.index"
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section name="Almacén">
|
|
||||||
<Link
|
|
||||||
icon="grid_view"
|
|
||||||
name="Almacén"
|
|
||||||
to="admin.warehouses.index"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
icon="tag"
|
|
||||||
name="Clasificaciones de almacenes"
|
|
||||||
to="admin.warehouse-classifications.index"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
icon="straighten"
|
|
||||||
name="Unidades de medida"
|
|
||||||
to="admin.units-measure.index"
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
<Section name="Capacitaciones">
|
<Section name="Capacitaciones">
|
||||||
<DropDown
|
<DropDown
|
||||||
icon="grid_view"
|
icon="grid_view"
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
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
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<PageHeader
|
|
||||||
:title="transl('create.title')"
|
|
||||||
>
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="create"
|
|
||||||
:form="form"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|
||||||
import { api, useForm } from '@Services/Api';
|
|
||||||
import { viewTo, apiTo, transl } from './Module';
|
|
||||||
import UnitsMeasureService from './services/UnitsMeasureService';
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
|
||||||
import PageHeader from '@Holos/PageHeader.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
import Form from './Form.vue'
|
|
||||||
|
|
||||||
/** Definiciones */
|
|
||||||
const vroute = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const unitsService = new UnitsMeasureService();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const form = useForm({
|
|
||||||
id: null,
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
abbreviation: '',
|
|
||||||
type: 4,
|
|
||||||
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
|
|
||||||
})).put(apiTo('update', { units_of_measure: form.id }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
Notify.success(lang('register.edit.onSuccess'))
|
|
||||||
router.push(viewTo({ name: 'index' }));
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadData() {
|
|
||||||
api.get(apiTo('show', { units_of_measure: vroute.params.id }), {
|
|
||||||
onSuccess: async (r) => {
|
|
||||||
const data = r.data || r.units_of_measure || r;
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
// Cargar los tipos para convertir el número a objeto
|
|
||||||
try {
|
|
||||||
const typeOptions = await unitsService.getUnitTypes();
|
|
||||||
const selectedType = typeOptions.find(option => option.id == data.type) || { value: data.type, label: data.name };
|
|
||||||
console.log('Tipos disponibles:', selectedType);
|
|
||||||
|
|
||||||
form.fill({
|
|
||||||
id: data.units.id,
|
|
||||||
code: data.units.code,
|
|
||||||
name: data.units.name,
|
|
||||||
abbreviation: data.units.abbreviation,
|
|
||||||
type: selectedType, // Convertir a objeto {value, label}
|
|
||||||
is_active: Boolean(data.is_active)
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando tipos:', error);
|
|
||||||
// Fallback: usar solo el valor numérico
|
|
||||||
form.fill({
|
|
||||||
id: data.units.id,
|
|
||||||
code: data.units.code,
|
|
||||||
name: data.units.name,
|
|
||||||
abbreviation: data.units.abbreviation,
|
|
||||||
type: data.type,
|
|
||||||
is_active: Boolean(data.is_active)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<PageHeader
|
|
||||||
:title="transl('update.title')"
|
|
||||||
>
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="update"
|
|
||||||
:form="form"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { transl } from './Module';
|
|
||||||
import UnitsMeasureService from './services/UnitsMeasureService';
|
|
||||||
|
|
||||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
|
||||||
import Input from '@Holos/Form/Input.vue';
|
|
||||||
import Selectable from '@Holos/Form/Selectable.vue';
|
|
||||||
import Checkbox from '@Holos/Checkbox.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'submit'
|
|
||||||
])
|
|
||||||
|
|
||||||
/** Servicios */
|
|
||||||
const unitsService = new UnitsMeasureService();
|
|
||||||
|
|
||||||
/** Propiedades reactivas */
|
|
||||||
const typeOptions = ref([]);
|
|
||||||
const loadingTypes = ref(true);
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
defineProps({
|
|
||||||
action: {
|
|
||||||
default: 'create',
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
form: Object
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
emit('submit')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cargar tipos de medida desde la API */
|
|
||||||
async function loadUnitTypes() {
|
|
||||||
try {
|
|
||||||
loadingTypes.value = true;
|
|
||||||
typeOptions.value = await unitsService.getUnitTypes();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando tipos de medida:', error);
|
|
||||||
// Usar tipos por defecto en caso de error
|
|
||||||
const fallbackTypes = unitsService.getTypes().map(type => ({
|
|
||||||
value: type.id,
|
|
||||||
label: type.name
|
|
||||||
}));
|
|
||||||
typeOptions.value = fallbackTypes;
|
|
||||||
} finally {
|
|
||||||
loadingTypes.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ciclo de vida */
|
|
||||||
onMounted(() => {
|
|
||||||
loadUnitTypes();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full pb-2">
|
|
||||||
<p class="text-justify text-sm" v-text="transl(`form.${action}.description`)" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
<Input
|
|
||||||
v-model="form.code"
|
|
||||||
id="code"
|
|
||||||
:onError="form.errors.code"
|
|
||||||
autofocus
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="form.name"
|
|
||||||
id="name"
|
|
||||||
:onError="form.errors.name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="form.abbreviation"
|
|
||||||
id="Abreviación"
|
|
||||||
:onError="form.errors.abbreviation"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Selectable
|
|
||||||
v-model="form.type"
|
|
||||||
id="type"
|
|
||||||
title="Tipo de medida"
|
|
||||||
label="label"
|
|
||||||
:onError="form.errors.type"
|
|
||||||
:options="typeOptions"
|
|
||||||
:disabled="loadingTypes"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
|
||||||
<PrimaryButton
|
|
||||||
:loading="form.processing"
|
|
||||||
:disabled="form.processing"
|
|
||||||
>
|
|
||||||
Crear unidad de medida
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRouter, RouterLink } from 'vue-router';
|
|
||||||
import { useSearcher } from '@Services/Api';
|
|
||||||
import { hasPermission } from '@Plugins/RolePermission';
|
|
||||||
import { can, apiTo, viewTo, transl } from './Module'
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
|
||||||
import Table from '@Holos/Table.vue';
|
|
||||||
import ShowView from './Modals/Show.vue';
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const models = ref([]);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const showModal = ref(null);
|
|
||||||
const destroyModal = ref(null);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
const searcher = useSearcher({
|
|
||||||
url: apiTo('index'),
|
|
||||||
onSuccess: (r) => models.value = r.units_of_measure,
|
|
||||||
onError: () => models.value = []
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Función para eliminar */
|
|
||||||
const deleteItem = (item) => {
|
|
||||||
destroyModal.value.open(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
searcher.search();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<SearcherHead :title="transl('name')" @search="(x) => searcher.search(x)">
|
|
||||||
<RouterLink :to="viewTo({ name: 'create' })">
|
|
||||||
<!-- v-if="can('create')" -->
|
|
||||||
<IconButton class="text-white" icon="add" :title="$t('crud.create')" filled />
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton icon="refresh" :title="$t('refresh')" @click="searcher.search()" />
|
|
||||||
</SearcherHead>
|
|
||||||
<div class="pt-2 w-full">
|
|
||||||
<Table :items="models" :processing="searcher.processing"
|
|
||||||
@send-pagination="(page) => searcher.pagination(page)">
|
|
||||||
<template #head>
|
|
||||||
<th v-text="$t('code')" />
|
|
||||||
<th v-text="$t('Abreviatura')" />
|
|
||||||
<th v-text="$t('Unidad de medida')" />
|
|
||||||
|
|
||||||
<th v-text="$t('Tipo de unidad')" />
|
|
||||||
<th v-text="$t('Estado')" class="w-20 text-center" />
|
|
||||||
<th v-text="$t('Acciones')" class="w-64 text-center" />
|
|
||||||
</template>
|
|
||||||
<template #body="{ items }">
|
|
||||||
<tr v-for="model in items" :key="model.id" class="table-row">
|
|
||||||
<td class="table-cell">
|
|
||||||
<code class="font-mono text-sm text-gray-900 dark:text-gray-100">
|
|
||||||
{{ model.code }}
|
|
||||||
</code>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="table-cell">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
|
||||||
{{ model.abbreviation }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<span class="font-semibold">{{ model.name }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{{ model.type_name || '-' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium" :class="model.is_active
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'">
|
|
||||||
{{ model.is_active ? $t('active') : $t('inactive') }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="table-actions">
|
|
||||||
<IconButton icon="visibility" :title="$t('crud.show')" @click="showModal.open(model)"
|
|
||||||
outline />
|
|
||||||
<RouterLink class="h-fit" :to="viewTo({ name: 'edit', params: { id: model.id } })">
|
|
||||||
<IconButton icon="edit" :title="$t('crud.edit')" outline />
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton icon="delete" :title="$t('crud.destroy')" @click="deleteItem(model)"
|
|
||||||
outline />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="flex items-center text-sm">
|
|
||||||
<p class="font-semibold">
|
|
||||||
{{ $t('registers.empty') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShowView ref="showModal" @reload="searcher.search()" />
|
|
||||||
|
|
||||||
<DestroyView ref="destroyModal" subtitle="name" :to="(id) => apiTo('destroy', { units_of_measure: id })"
|
|
||||||
@update="searcher.search()" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,210 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { getDateTime } from '@Controllers/DateController';
|
|
||||||
import { viewTo, apiTo, transl } from '../Module';
|
|
||||||
import UnitsMeasureService from '../services/UnitsMeasureService';
|
|
||||||
import Notify from '@Plugins/Notify';
|
|
||||||
|
|
||||||
import Header from '@Holos/Modal/Elements/Header.vue';
|
|
||||||
import ShowModal from '@Holos/Modal/Show.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
import Button from '@Holos/Button/Button.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'close',
|
|
||||||
'reload'
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Servicios */
|
|
||||||
const unitsService = new UnitsMeasureService();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const model = ref(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const modalRef = ref(null);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function close() {
|
|
||||||
model.value = null;
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para alternar estado */
|
|
||||||
async function toggleStatus(item) {
|
|
||||||
if (loading.value) return;
|
|
||||||
|
|
||||||
const newStatus = !item.is_active;
|
|
||||||
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
// Usar el servicio para actualizar el estado
|
|
||||||
await unitsService.updateStatus(item.id, newStatus);
|
|
||||||
|
|
||||||
// Actualizar el modelo local
|
|
||||||
item.is_active = newStatus;
|
|
||||||
|
|
||||||
// Notificación de éxito
|
|
||||||
const statusText = newStatus ? 'activada' : 'desactivada';
|
|
||||||
Notify.success(
|
|
||||||
`Unidad "${item.code}" ${statusText} exitosamente`,
|
|
||||||
'Estado actualizado'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emitir evento para recargar la lista principal si es necesario
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error actualizando estado:', error);
|
|
||||||
|
|
||||||
// Manejo de errores
|
|
||||||
let errorMessage = 'Error al actualizar el estado de la unidad de medida';
|
|
||||||
let errorTitle = 'Error';
|
|
||||||
|
|
||||||
if (error?.response?.data) {
|
|
||||||
const errorData = error.response.data;
|
|
||||||
|
|
||||||
if (errorData.status === 'error') {
|
|
||||||
if (errorData.errors) {
|
|
||||||
const firstField = Object.keys(errorData.errors)[0];
|
|
||||||
const firstError = errorData.errors[firstField];
|
|
||||||
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
|
||||||
errorTitle = 'Error de validación';
|
|
||||||
} else if (errorData.message) {
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
errorTitle = 'Error del servidor';
|
|
||||||
}
|
|
||||||
} else if (errorData.message) {
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
}
|
|
||||||
} else if (error?.message) {
|
|
||||||
errorMessage = `Error de conexión: ${error.message}`;
|
|
||||||
errorTitle = 'Error de red';
|
|
||||||
}
|
|
||||||
|
|
||||||
Notify.error(errorMessage, errorTitle);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Exposer métodos públicos */
|
|
||||||
defineExpose({
|
|
||||||
open: (data) => {
|
|
||||||
model.value = data;
|
|
||||||
modalRef.value.open();
|
|
||||||
},
|
|
||||||
close
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ShowModal ref="modalRef" @close="close">
|
|
||||||
<div v-if="model">
|
|
||||||
<Header :title="model.code" :subtitle="model.name">
|
|
||||||
<div class="flex w-full flex-col">
|
|
||||||
<div class="flex w-full justify-center items-center">
|
|
||||||
<div
|
|
||||||
class="w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
|
||||||
<GoogleIcon class="text-white text-3xl" name="straighten" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<div class="flex w-full p-4 space-y-6">
|
|
||||||
<div class="w-full space-y-6">
|
|
||||||
<!-- Información Principal -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<!-- Código y Estado -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('code') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-1">
|
|
||||||
<code
|
|
||||||
class="font-mono text-lg bg-gray-100 dark:bg-gray-700 px-3 py-2 rounded-lg block">
|
|
||||||
{{ model.code }}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('Estado') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-2">
|
|
||||||
<Button :variant="'smooth'" :color="model.is_active ? 'success' : 'danger'"
|
|
||||||
:size="'sm'" :loading="loading" @click="toggleStatus(model)">
|
|
||||||
{{ model.is_active ? $t('Activo') : $t('Inactivo') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nombre y Abreviación -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('Unidad de medida') }}
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 text-lg font-semibold">{{ model.name }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('Abreviación') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-1">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
|
||||||
{{ model.abbreviation }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tipo de Medida -->
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ $t('Tipo de unidad') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<GoogleIcon class="text-2xl mr-3 text-indigo-500" name="straighten" />
|
|
||||||
<div>
|
|
||||||
<p class="font-semibold">{{ model.type_name }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Información de Fechas -->
|
|
||||||
<div class="border-t pt-6">
|
|
||||||
<h4 class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
Información del Sistema
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Creado:</span>
|
|
||||||
<p class="mt-1">{{ getDateTime(model.created_at) }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Actualizado:</span>
|
|
||||||
<p class="mt-1">{{ getDateTime(model.updated_at) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ShowModal>
|
|
||||||
</template>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<PageHeader
|
|
||||||
:title="parentInfo ? `${transl('create.title')} - Subcategoría` : transl('create.title')"
|
|
||||||
>
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<!-- Mostrar información del padre si se está creando una subcategoría -->
|
|
||||||
<div v-if="parentInfo" class="mb-4 p-4 bg-blue-50 dark:bg-blue-900 rounded-lg border border-blue-200 dark:border-blue-700">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<GoogleIcon class="text-blue-600 text-xl mr-2" name="account_tree" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
|
||||||
Creando subcategoría para:
|
|
||||||
</p>
|
|
||||||
<p class="text-lg font-semibold text-blue-900 dark:text-blue-100">
|
|
||||||
<code class="bg-blue-100 dark:bg-blue-800 px-2 py-1 rounded text-sm mr-2">{{ parentInfo.code }}</code>
|
|
||||||
{{ parentInfo.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="create"
|
|
||||||
:form="form"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|
||||||
import { api, useForm } from '@Services/Api';
|
|
||||||
import { viewTo, apiTo , transl } 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'
|
|
||||||
|
|
||||||
/** Definiciones */
|
|
||||||
const vroute = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const form = useForm({
|
|
||||||
id: null,
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
is_active: true,
|
|
||||||
parent: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const parentOptions = ref([]);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
form.transform(data => ({
|
|
||||||
...data,
|
|
||||||
parent_id: data.parent?.id || null
|
|
||||||
})).put(apiTo('update', { warehouse_classification: form.id }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
Notify.success(Lang('register.edit.onSuccess'))
|
|
||||||
router.push(viewTo({ name: 'index' }));
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para obtener clasificaciones padre (excluyendo la actual y sus hijos) */
|
|
||||||
function loadParentOptions() {
|
|
||||||
api.get(apiTo('index'), {
|
|
||||||
onSuccess: (r) => {
|
|
||||||
// Función para excluir la clasificación actual y sus descendientes
|
|
||||||
const excludeCurrentAndChildren = (items, excludeId) => {
|
|
||||||
return items.filter(item => {
|
|
||||||
if (item.id === excludeId) return false;
|
|
||||||
if (hasDescendant(item, excludeId)) return false;
|
|
||||||
return true;
|
|
||||||
}).map(item => ({
|
|
||||||
...item,
|
|
||||||
children: excludeCurrentAndChildren(item.children || [], excludeId)
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para verificar si un item tiene como descendiente la clasificación actual
|
|
||||||
const hasDescendant = (item, targetId) => {
|
|
||||||
if (!item.children) return false;
|
|
||||||
return item.children.some(child =>
|
|
||||||
child.id === targetId || hasDescendant(child, targetId)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Aplanar la estructura jerárquica para el selector
|
|
||||||
const flattenOptions = (items, level = 0) => {
|
|
||||||
let options = [];
|
|
||||||
items.forEach(item => {
|
|
||||||
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 filteredData = excludeCurrentAndChildren(r.warehouse_classifications.data, form.id);
|
|
||||||
parentOptions.value = flattenOptions(filteredData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
api.get(apiTo('show', { warehouse_classification: vroute.params.id }), {
|
|
||||||
onSuccess: (r) => {
|
|
||||||
const classification = r.warehouse_classification;
|
|
||||||
form.fill({
|
|
||||||
id: classification.id,
|
|
||||||
code: classification.code,
|
|
||||||
name: classification.name,
|
|
||||||
description: classification.description,
|
|
||||||
is_active: classification.is_active,
|
|
||||||
parent: classification.parent || null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cargar opciones padre después de obtener los datos actuales
|
|
||||||
loadParentOptions();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<PageHeader :title="`${transl('edit.title')}${form.parent ? ' - Subcategoría' : ''}`">
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<!-- Mostrar información del padre si es una subcategoría -->
|
|
||||||
<div v-if="form.parent" class="mb-4 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg border border-yellow-200 dark:border-yellow-700">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<GoogleIcon class="text-yellow-600 text-xl mr-2" name="account_tree" />
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
|
||||||
Editando subcategoría de:
|
|
||||||
</p>
|
|
||||||
<p class="text-lg font-semibold text-yellow-900 dark:text-yellow-100">
|
|
||||||
<code class="bg-yellow-100 dark:bg-yellow-800 px-2 py-1 rounded text-sm mr-2">{{ form.parent.code }}</code>
|
|
||||||
{{ form.parent.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="update"
|
|
||||||
:form="form"
|
|
||||||
:parent-options="parentOptions"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { transl } from './Module';
|
|
||||||
|
|
||||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
|
||||||
import Input from '@Holos/Form/Input.vue';
|
|
||||||
import Selectable from '@Holos/Form/Selectable.vue';
|
|
||||||
import Checkbox from '@Holos/Checkbox.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'submit'
|
|
||||||
])
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
defineProps({
|
|
||||||
action: {
|
|
||||||
default: 'create',
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
form: Object,
|
|
||||||
parentOptions: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
emit('submit')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full pb-2">
|
|
||||||
<p class="text-justify text-sm" v-text="transl(`${action}.description`)" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
<Input
|
|
||||||
v-model="form.code"
|
|
||||||
id="code"
|
|
||||||
:onError="form.errors.code"
|
|
||||||
autofocus
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="form.name"
|
|
||||||
id="name"
|
|
||||||
:onError="form.errors.name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
v-model="form.description"
|
|
||||||
id="description"
|
|
||||||
:onError="form.errors.description"
|
|
||||||
class="md:col-span-2"
|
|
||||||
/>
|
|
||||||
<slot />
|
|
||||||
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
|
||||||
<PrimaryButton
|
|
||||||
v-text="$t(action)"
|
|
||||||
:class="{ 'opacity-25': form.processing }"
|
|
||||||
:disabled="form.processing"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,212 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useSearcher } from '@Services/Api';
|
|
||||||
import { hasPermission } from '@Plugins/RolePermission';
|
|
||||||
import { can, apiTo, viewTo, transl } from './Module'
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
|
||||||
import Table from '@Holos/Table.vue';
|
|
||||||
import ShowView from './Modals/Show.vue';
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const models = ref([]);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const showModal = ref(false);
|
|
||||||
const destroyModal = ref(false);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
const searcher = useSearcher({
|
|
||||||
url: apiTo('index'),
|
|
||||||
onSuccess: (r) => models.value = r.warehouse_classifications,
|
|
||||||
onError: () => models.value = []
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Función para renderizar estructura jerárquica */
|
|
||||||
const renderHierarchicalRows = (items, level = 0) => {
|
|
||||||
const rows = [];
|
|
||||||
items.forEach(item => {
|
|
||||||
rows.push({ ...item, level });
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
rows.push(...renderHierarchicalRows(item.children, level + 1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return rows;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para crear subcategoría */
|
|
||||||
const createSubcategory = (parentCategory) => {
|
|
||||||
// Redirigir a la vista de creación con el parent_id en query params
|
|
||||||
router.push(viewTo({
|
|
||||||
name: 'create',
|
|
||||||
query: {
|
|
||||||
parent_id: parentCategory.id,
|
|
||||||
parent_name: parentCategory.name,
|
|
||||||
parent_code: parentCategory.code
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para eliminar con información contextual */
|
|
||||||
const deleteItem = (item) => {
|
|
||||||
// Agregar información contextual al modal
|
|
||||||
const itemWithContext = {
|
|
||||||
...item,
|
|
||||||
contextInfo: item.parent ? `Subcategoría de: ${item.parent.code} - ${item.parent.name}` : 'Categoría principal'
|
|
||||||
};
|
|
||||||
destroyModal.value.open(itemWithContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
searcher.search();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<SearcherHead
|
|
||||||
:title="transl('name')"
|
|
||||||
@search="(x) => searcher.search(x)"
|
|
||||||
>
|
|
||||||
<RouterLink
|
|
||||||
|
|
||||||
:to="viewTo({ name: 'create' })"
|
|
||||||
>
|
|
||||||
<!-- v-if="can('create')" -->
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="add"
|
|
||||||
:title="$t('crud.create')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton
|
|
||||||
icon="refresh"
|
|
||||||
:title="$t('refresh')"
|
|
||||||
@click="searcher.search()"
|
|
||||||
/>
|
|
||||||
</SearcherHead>
|
|
||||||
<div class="pt-2 w-full">
|
|
||||||
<Table
|
|
||||||
:items="models"
|
|
||||||
:processing="searcher.processing"
|
|
||||||
@send-pagination="(page) => searcher.pagination(page)"
|
|
||||||
>
|
|
||||||
<template #head>
|
|
||||||
<th v-text="$t('code')" />
|
|
||||||
<th v-text="$t('name')" />
|
|
||||||
<th v-text="$t('description')" />
|
|
||||||
<th v-text="$t('status')" class="w-20 text-center" />
|
|
||||||
<th
|
|
||||||
v-text="$t('actions')"
|
|
||||||
class="w-32 text-center"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #body="{items}">
|
|
||||||
<tr
|
|
||||||
v-for="model in items"
|
|
||||||
:key="model.id"
|
|
||||||
class="table-row"
|
|
||||||
:class="{ 'bg-gray-50 dark:bg-gray-800': model.level > 0 }"
|
|
||||||
>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div :style="{ paddingLeft: `${model.level * 24}px` }" class="flex items-center">
|
|
||||||
<span v-if="model.level > 0" class="text-gray-400 mr-2">└─</span>
|
|
||||||
<code class="font-mono text-sm"
|
|
||||||
:class="model.level > 0 ? 'text-blue-600 dark:text-blue-400' : 'text-gray-900 dark:text-gray-100'">
|
|
||||||
{{ model.code }}
|
|
||||||
</code>
|
|
||||||
<span v-if="model.level > 0" class="ml-2 text-xs bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100 px-2 py-1 rounded-full">
|
|
||||||
Subcategoría
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<span class="font-semibold">{{ model.name }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{{ model.description || '-' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell text-center">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
|
||||||
:class="model.is_active
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'"
|
|
||||||
>
|
|
||||||
{{ model.is_active ? $t('active') : $t('inactive') }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="table-actions">
|
|
||||||
<IconButton
|
|
||||||
|
|
||||||
icon="add_circle"
|
|
||||||
:title="$t('Agregar subcategoría')"
|
|
||||||
@click="createSubcategory(model)"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
|
|
||||||
icon="visibility"
|
|
||||||
:title="$t('crud.show')"
|
|
||||||
@click="showModal.open(model)"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
<RouterLink
|
|
||||||
|
|
||||||
class="h-fit"
|
|
||||||
:to="viewTo({ name: 'edit', params: { id: model.id } })"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon="edit"
|
|
||||||
:title="$t('crud.edit')"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton
|
|
||||||
|
|
||||||
icon="delete"
|
|
||||||
:title="$t('crud.destroy')"
|
|
||||||
@click="deleteItem(model)"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="flex items-center text-sm">
|
|
||||||
<p class="font-semibold">
|
|
||||||
{{ $t('registers.empty') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
<td class="table-cell">-</td>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShowView
|
|
||||||
ref="showModal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DestroyView
|
|
||||||
ref="destroyModal"
|
|
||||||
subtitle="name"
|
|
||||||
:to="(id) => apiTo('destroy', { warehouse_classification: id })"
|
|
||||||
@update="searcher.search()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,362 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { getDateTime } from '@Controllers/DateController';
|
|
||||||
import { viewTo, apiTo } from '../Module';
|
|
||||||
import WarehouseClassificationService from '../services/WarehouseClassificationService';
|
|
||||||
import Notify from '@Plugins/Notify';
|
|
||||||
|
|
||||||
import Header from '@Holos/Modal/Elements/Header.vue';
|
|
||||||
import ShowModal from '@Holos/Modal/Show.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
import Button from '@Holos/Button/Button.vue';
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue';
|
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'close',
|
|
||||||
'reload'
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Servicios */
|
|
||||||
const warehouseService = new WarehouseClassificationService();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const model = ref(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
const deletingChildId = ref(null); // Para mostrar loading específico en subcategoría que se está eliminando
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const modalRef = ref(null);
|
|
||||||
const destroyModal = ref(null);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function close() {
|
|
||||||
model.value = null;
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para actualizar el estado de la clasificación */
|
|
||||||
async function toggleStatus(item) {
|
|
||||||
if (loading.value) return;
|
|
||||||
|
|
||||||
const newStatus = !item.is_active;
|
|
||||||
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
// Usar el servicio para actualizar el estado
|
|
||||||
await warehouseService.updateStatus(item.id, newStatus);
|
|
||||||
|
|
||||||
// Actualizar el modelo local
|
|
||||||
item.is_active = newStatus;
|
|
||||||
|
|
||||||
// Notificación de éxito
|
|
||||||
const statusText = newStatus ? 'activada' : 'desactivada';
|
|
||||||
Notify.success(
|
|
||||||
`Clasificación "${item.code}" ${statusText} exitosamente`,
|
|
||||||
'Estado actualizado'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emitir evento para recargar la lista principal si es necesario
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error actualizando estado:', error);
|
|
||||||
|
|
||||||
// Manejo específico de errores según la estructura de tu API
|
|
||||||
let errorMessage = 'Error al actualizar el estado de la clasificación';
|
|
||||||
let errorTitle = 'Error';
|
|
||||||
|
|
||||||
if (error?.response?.data) {
|
|
||||||
const errorData = error.response.data;
|
|
||||||
|
|
||||||
// Caso 1: Error con estructura específica de tu API
|
|
||||||
if (errorData.status === 'error') {
|
|
||||||
if (errorData.errors) {
|
|
||||||
// Errores de validación - extraer el primer error
|
|
||||||
const firstField = Object.keys(errorData.errors)[0];
|
|
||||||
const firstError = errorData.errors[firstField];
|
|
||||||
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
|
||||||
errorTitle = 'Error de validación';
|
|
||||||
} else if (errorData.message) {
|
|
||||||
// Mensaje general del error
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
errorTitle = 'Error del servidor';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Caso 2: Otros formatos de error
|
|
||||||
else if (errorData.message) {
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
}
|
|
||||||
} else if (error?.message) {
|
|
||||||
// Error genérico de la petición (red, timeout, etc.)
|
|
||||||
errorMessage = `Error de conexión: ${error.message}`;
|
|
||||||
errorTitle = 'Error de red';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notificación de error
|
|
||||||
Notify.error(errorMessage, errorTitle);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para editar subcategoría */
|
|
||||||
function editSubcategory(child) {
|
|
||||||
// Navegar a la vista de edición de la subcategoría
|
|
||||||
const editUrl = viewTo({ name: 'edit', params: { id: child.id } });
|
|
||||||
router.push(editUrl);
|
|
||||||
// Cerrar el modal actual
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para eliminar subcategoría */
|
|
||||||
function deleteSubcategory(child) {
|
|
||||||
// Marcar cuál subcategoría se va a eliminar para mostrar loading
|
|
||||||
deletingChildId.value = child.id;
|
|
||||||
destroyModal.value.open(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para recargar después de eliminar - Mejorada */
|
|
||||||
async function onSubcategoryDeleted() {
|
|
||||||
try {
|
|
||||||
// Mostrar que se está procesando
|
|
||||||
const deletedId = deletingChildId.value;
|
|
||||||
|
|
||||||
// Recargar los datos de la clasificación actual para obtener subcategorías actualizadas
|
|
||||||
const response = await warehouseService.getById(model.value.id);
|
|
||||||
|
|
||||||
// Actualizar el modelo local con los datos frescos
|
|
||||||
if (response && response.warehouse_classification) {
|
|
||||||
model.value = response.warehouse_classification;
|
|
||||||
|
|
||||||
// Notificación de éxito con información específica
|
|
||||||
const deletedCount = model.value.children ? model.value.children.length : 0;
|
|
||||||
Notify.success(
|
|
||||||
`Subcategoría eliminada exitosamente. ${deletedCount} subcategorías restantes.`,
|
|
||||||
'Eliminación exitosa'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emitir evento para recargar la lista principal
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error recargando datos después de eliminar:', error);
|
|
||||||
|
|
||||||
// En caso de error, cerrar modal y recargar lista principal
|
|
||||||
close();
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
// Notificación de error
|
|
||||||
Notify.error(
|
|
||||||
'Error al actualizar la vista. Los datos se han actualizado correctamente.',
|
|
||||||
'Error de actualización'
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
// Limpiar el estado de eliminación
|
|
||||||
deletingChildId.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para cancelar eliminación */
|
|
||||||
function onDeleteCancelled() {
|
|
||||||
// Limpiar el estado de eliminación si se cancela
|
|
||||||
deletingChildId.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para renderizar la jerarquía de hijos */
|
|
||||||
const renderChildren = (children, level = 1) => {
|
|
||||||
if (!children || children.length === 0) return null;
|
|
||||||
return children.map(child => ({
|
|
||||||
...child,
|
|
||||||
level,
|
|
||||||
children: renderChildren(child.children, level + 1)
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Exposiciones */
|
|
||||||
defineExpose({
|
|
||||||
open: (data) => {
|
|
||||||
model.value = data;
|
|
||||||
modalRef.value.open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ShowModal
|
|
||||||
ref="modalRef"
|
|
||||||
@close="close"
|
|
||||||
>
|
|
||||||
<div v-if="model">
|
|
||||||
<Header
|
|
||||||
:title="model.code"
|
|
||||||
:subtitle="model.name"
|
|
||||||
>
|
|
||||||
<div class="flex w-full flex-col">
|
|
||||||
<div class="flex w-full justify-center items-center">
|
|
||||||
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-white text-3xl"
|
|
||||||
name="category"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
<div class="flex w-full p-4 space-y-6">
|
|
||||||
<!-- Información básica -->
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-xl text-success mt-1"
|
|
||||||
name="info"
|
|
||||||
/>
|
|
||||||
<div class="pl-3 w-full">
|
|
||||||
<p class="font-bold text-lg leading-none pb-3">
|
|
||||||
{{ $t('details') }}
|
|
||||||
</p>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<b>{{ $t('code') }}: </b>
|
|
||||||
<code class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
|
||||||
{{ model.code }}
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
<p class="mt-2">
|
|
||||||
<b>{{ $t('name') }}: </b>
|
|
||||||
{{ model.name }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-2" v-if="model.description">
|
|
||||||
<b>{{ $t('description') }}: </b>
|
|
||||||
{{ model.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<b>{{ $t('status') }}: </b>
|
|
||||||
<Button
|
|
||||||
:variant="'smooth'"
|
|
||||||
:color="model.is_active ? 'success' : 'danger'"
|
|
||||||
:size="'sm'"
|
|
||||||
:loading="loading"
|
|
||||||
@click="toggleStatus(model)"
|
|
||||||
>
|
|
||||||
{{ model.is_active ? $t('Activo') : $t('Inactivo') }}
|
|
||||||
</Button>
|
|
||||||
</p>
|
|
||||||
<p class="mt-2">
|
|
||||||
<b>{{ $t('created_at') }}: </b>
|
|
||||||
{{ getDateTime(model.created_at) }}
|
|
||||||
</p>
|
|
||||||
<p class="mt-2">
|
|
||||||
<b>{{ $t('updated_at') }}: </b>
|
|
||||||
{{ getDateTime(model.updated_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Clasificaciones hijas -->
|
|
||||||
<div v-if="model.children && model.children.length > 0" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-xl text-primary mt-1"
|
|
||||||
name="account_tree"
|
|
||||||
/>
|
|
||||||
<div class="pl-3 w-full">
|
|
||||||
<p class="font-bold text-lg leading-none pb-3">
|
|
||||||
{{ $t('Subclasificaciones') }}
|
|
||||||
</p>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div
|
|
||||||
v-for="child in model.children"
|
|
||||||
:key="child.id"
|
|
||||||
class="flex items-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg transition-all duration-300"
|
|
||||||
:class="{
|
|
||||||
'opacity-50 pointer-events-none': deletingChildId === child.id,
|
|
||||||
'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700': deletingChildId === child.id
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<code class="font-mono text-sm bg-white dark:bg-gray-700 px-2 py-1 rounded mr-3">
|
|
||||||
{{ child.code }}
|
|
||||||
</code>
|
|
||||||
<span class="font-semibold">{{ child.name }}</span>
|
|
||||||
<!-- Indicador de eliminación -->
|
|
||||||
<span v-if="deletingChildId === child.id" class="ml-2 text-xs bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100 px-2 py-1 rounded-full animate-pulse">
|
|
||||||
Eliminando...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p v-if="child.description" class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{{ child.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botones de acción para subcategorías -->
|
|
||||||
<div class="flex items-center space-x-2 ml-4">
|
|
||||||
<!-- Botón de estado -->
|
|
||||||
<Button
|
|
||||||
:variant="'smooth'"
|
|
||||||
:color="child.is_active ? 'success' : 'danger'"
|
|
||||||
:size="'sm'"
|
|
||||||
:loading="loading && deletingChildId !== child.id"
|
|
||||||
:disabled="deletingChildId === child.id"
|
|
||||||
@click="toggleStatus(child)"
|
|
||||||
>
|
|
||||||
{{ child.is_active ? $t('Activo') : $t('Inactivo') }}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Botón editar -->
|
|
||||||
<IconButton
|
|
||||||
icon="edit"
|
|
||||||
:title="$t('crud.edit')"
|
|
||||||
:disabled="deletingChildId === child.id"
|
|
||||||
@click="editSubcategory(child)"
|
|
||||||
outline
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Botón eliminar -->
|
|
||||||
<IconButton
|
|
||||||
icon="delete"
|
|
||||||
:title="$t('crud.destroy')"
|
|
||||||
:loading="deletingChildId === child.id"
|
|
||||||
:disabled="deletingChildId && deletingChildId !== child.id"
|
|
||||||
@click="deleteSubcategory(child)"
|
|
||||||
outline
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mensaje cuando no hay subcategorías (después de eliminar todas) -->
|
|
||||||
<div v-if="!model.children || model.children.length === 0"
|
|
||||||
class="text-center p-4 text-gray-500 dark:text-gray-400">
|
|
||||||
<GoogleIcon class="text-2xl mb-2" name="folder_open" />
|
|
||||||
<p class="text-sm">No hay subcategorías</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal de eliminación para subcategorías -->
|
|
||||||
<DestroyView
|
|
||||||
ref="destroyModal"
|
|
||||||
subtitle="name"
|
|
||||||
:to="(id) => apiTo('destroy', { warehouse_classification: id })"
|
|
||||||
@update="onSubcategoryDeleted"
|
|
||||||
/>
|
|
||||||
</ShowModal>
|
|
||||||
</template>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
};
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
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 WarehouseService from './services/WarehouseService';
|
|
||||||
|
|
||||||
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();
|
|
||||||
const warehouseService = new WarehouseService();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const form = useForm({
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
address: '',
|
|
||||||
classifications: [], // Array de objetos {value, label}
|
|
||||||
is_active: true
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
form.transform(data => ({
|
|
||||||
...data,
|
|
||||||
classifications: warehouseService.processClassificationsForSubmit(data.classifications)
|
|
||||||
})).post(apiTo('store'), {
|
|
||||||
onSuccess: () => {
|
|
||||||
Notify.success('Almacén creado con éxito')
|
|
||||||
router.push(viewTo({ name: 'index' }));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
// Inicialización si es necesaria
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<PageHeader
|
|
||||||
:title="transl('create.title')"
|
|
||||||
>
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="create"
|
|
||||||
:form="form"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,457 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
|
||||||
import { api } from '@Services/Api';
|
|
||||||
import { apiTo, viewTo, transl } from './Module';
|
|
||||||
import WarehouseService from './services/WarehouseService';
|
|
||||||
|
|
||||||
import Card from '@Holos/Card/Card.vue';
|
|
||||||
import CardHeader from '@Holos/Card/CardHeader.vue';
|
|
||||||
import CardTitle from '@Holos/Card/CardTitle.vue';
|
|
||||||
import CardDescription from '@Holos/Card/CardDescription.vue';
|
|
||||||
import CardContent from '@Holos/Card/CardContent.vue';
|
|
||||||
import PageHeader from '@Holos/PageHeader.vue';
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue';
|
|
||||||
import Button from '@Holos/Button/Button.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
|
|
||||||
/** Definiciones */
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const warehouseService = new WarehouseService();
|
|
||||||
|
|
||||||
/** Estado reactivo */
|
|
||||||
const warehouse = ref(null);
|
|
||||||
const loading = ref(true);
|
|
||||||
const error = ref(null);
|
|
||||||
|
|
||||||
// Mock data para el dashboard (adaptado del JSX)
|
|
||||||
const totalInventoryValue = ref(2400000);
|
|
||||||
const totalProducts = ref(145);
|
|
||||||
const inventoryMovements = ref([
|
|
||||||
{
|
|
||||||
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([
|
|
||||||
{
|
|
||||||
warehouse: "Almacén Principal",
|
|
||||||
products: [
|
|
||||||
{ code: "PROD-001", name: "Laptop Dell Inspiron 15", stock: 45, value: 719955 },
|
|
||||||
{ code: "PROD-002", name: "Mouse Inalámbrico Logitech", stock: 85, value: 50915 },
|
|
||||||
{ code: "PROD-003", name: "Monitor LG 24 pulgadas", stock: 8, value: 26392 },
|
|
||||||
{ code: "PROD-004", name: "Teclado Mecánico RGB", stock: 15, value: 32985 },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
async function loadWarehouse() {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
const response = await api.get(apiTo('show', { warehouse: route.params.id }), {
|
|
||||||
onSuccess: (r) => {
|
|
||||||
warehouse.value = r.warehouse;
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
error.value = 'Error cargando el almacén';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
error.value = 'Error de conexión';
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para navegar a editar */
|
|
||||||
const editWarehouse = () => {
|
|
||||||
router.push(viewTo({ name: 'edit', params: { id: warehouse.value.id } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para formatear fecha */
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return 'N/A';
|
|
||||||
return new Date(dateString).toLocaleDateString('es-ES', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para formatear moneda */
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return new Intl.NumberFormat('es-MX', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'MXN'
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para obtener icono de movimiento */
|
|
||||||
const getMovementIcon = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'entrada': return 'arrow_downward';
|
|
||||||
case 'salida': return 'arrow_upward';
|
|
||||||
case 'transferencia': return 'swap_horiz';
|
|
||||||
case 'ajuste': return 'tune';
|
|
||||||
default: return 'inventory';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para obtener clase de badge de movimiento */
|
|
||||||
const getMovementBadgeClass = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'entrada':
|
|
||||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
|
||||||
case 'salida':
|
|
||||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
|
||||||
case 'transferencia':
|
|
||||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
|
||||||
case 'ajuste':
|
|
||||||
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAllChildren = (classifications) => {
|
|
||||||
let children = [];
|
|
||||||
const extractChildren = (items) => {
|
|
||||||
items.forEach(item => {
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
children = children.concat(item.children);
|
|
||||||
extractChildren(item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
extractChildren(classifications);
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función para obtener clase de estado */
|
|
||||||
const getStatusBadgeClass = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
|
||||||
case 'pending':
|
|
||||||
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
|
||||||
case 'cancelled':
|
|
||||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Ciclo de vida */
|
|
||||||
onMounted(() => {
|
|
||||||
loadWarehouse();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- Header con navegación -->
|
|
||||||
<PageHeader :title="warehouse?.name || 'Cargando...'" :subtitle="`Código: ${warehouse?.code || 'N/A'}`">
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton class="text-white" icon="arrow_back" :title="$t('return')" filled />
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<!-- Estado de carga -->
|
|
||||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
|
||||||
<p class="text-gray-600">Cargando información del almacén...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estado de error -->
|
|
||||||
<div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
|
||||||
<GoogleIcon class="w-12 h-12 text-red-400 mx-auto mb-4" name="error" />
|
|
||||||
<h3 class="text-lg font-medium text-red-800 mb-2">Error al cargar</h3>
|
|
||||||
<p class="text-red-600 mb-4">{{ error }}</p>
|
|
||||||
<Button @click="loadWarehouse()" color="danger" variant="outline">
|
|
||||||
Reintentar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenido principal -->
|
|
||||||
<div v-else-if="warehouse" class="space-y-6">
|
|
||||||
|
|
||||||
<!-- Información básica -->
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<GoogleIcon class="w-6 h-6" name="warehouse" />
|
|
||||||
<span class="text-xl font-semibold text-gray-900 dark:text-gray-100">{{ warehouse.name
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<Button @click="editWarehouse" color="warning" variant="smooth" size="sm">
|
|
||||||
<GoogleIcon class="w-4 h-4 mr-2" name="edit" />
|
|
||||||
Editar Almacén
|
|
||||||
</Button>
|
|
||||||
<Button variant="smooth" color="info" size="sm">
|
|
||||||
<GoogleIcon class="w-4 h-4 mr-2" name="settings" />
|
|
||||||
Configuración
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6">
|
|
||||||
|
|
||||||
<!-- Columna Izquierda -->
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- Código -->
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Código</p>
|
|
||||||
<p class="font-mono text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{{ warehouse.code }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ubicación -->
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Ubicación</p>
|
|
||||||
<p class="text-base text-gray-900 dark:text-gray-100">
|
|
||||||
{{ warehouse.address || 'Guadalajara, Zona Industrial' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Categoría -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Clasificaciones</p>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span v-for="classification in warehouse.classifications" :key="classification.id"
|
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
|
||||||
{{ classification.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Subclasificaciones</p>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span v-for="child in getAllChildren(warehouse.classifications)" :key="child.id"
|
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
|
||||||
{{ child.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Estado -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Estado</p>
|
|
||||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
|
||||||
:class="warehouse.is_active
|
|
||||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
|
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'">
|
|
||||||
{{ warehouse.is_active ? 'Activo' : 'Inactivo' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Columna Derecha -->
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- Responsable -->
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Responsable</p>
|
|
||||||
<p class="text-base text-gray-900 dark:text-gray-100">
|
|
||||||
{{ warehouse.manager || 'Sin usuario responsable' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Teléfono -->
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Teléfono</p>
|
|
||||||
<p class="text-base text-gray-900 dark:text-gray-100">
|
|
||||||
{{ warehouse.phone || 'Sin número de teléfono' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Fecha de creación</p>
|
|
||||||
<p class="text-base text-gray-900 dark:text-gray-100">
|
|
||||||
{{ formatDate(warehouse.created_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="space-y-1">
|
|
||||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Ultima actualización</p>
|
|
||||||
<p class="text-base text-gray-900 dark:text-gray-100">
|
|
||||||
{{ formatDate(warehouse.updated_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Recent Movements -->
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center space-x-2">
|
|
||||||
<GoogleIcon class="w-6 h-6 text-primary" name="history" />
|
|
||||||
<span>Movimientos Recientes</span>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Últimos movimientos de inventario en este almacén
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div v-for="movement in inventoryMovements" :key="movement.id"
|
|
||||||
class="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<div class="h-10 w-10 rounded-lg flex items-center justify-center" :class="movement.type === 'entrada' ? 'bg-green-100 dark:bg-green-900' :
|
|
||||||
movement.type === 'salida' ? 'bg-red-100 dark:bg-red-900' :
|
|
||||||
movement.type === 'transferencia' ? 'bg-blue-100 dark:bg-blue-900' :
|
|
||||||
'bg-yellow-100 dark:bg-yellow-900'">
|
|
||||||
<GoogleIcon class="w-5 h-5" :class="movement.type === 'entrada' ? 'text-green-600 dark:text-green-400' :
|
|
||||||
movement.type === 'salida' ? 'text-red-600 dark:text-red-400' :
|
|
||||||
movement.type === 'transferencia' ? 'text-blue-600 dark:text-blue-400' :
|
|
||||||
'text-yellow-600 dark:text-yellow-400'"
|
|
||||||
:name="getMovementIcon(movement.type)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100">{{ movement.product }}</p>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{{ movement.code }} • {{ movement.reference }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<div class="text-right">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
|
||||||
:class="getMovementBadgeClass(movement.type)">
|
|
||||||
{{ movement.type }}
|
|
||||||
</span>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Cantidad: {{ movement.quantity }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ movement.user }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500">{{ formatDate(movement.date) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium capitalize"
|
|
||||||
:class="getStatusBadgeClass(movement.status)">
|
|
||||||
{{ movement.status === 'completed' ? 'Completado' :
|
|
||||||
movement.status === 'pending' ? 'Pendiente' : 'Cancelado' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Stock by Products -->
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center space-x-2">
|
|
||||||
<GoogleIcon class="w-6 h-6 text-primary" name="inventory" />
|
|
||||||
<span>Stock por Productos</span>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Inventario actual de productos en este almacén
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div v-for="warehouseStock in stockByWarehouse" :key="warehouseStock.warehouse" class="space-y-4">
|
|
||||||
<h4
|
|
||||||
class="font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2">
|
|
||||||
{{ warehouseStock.warehouse }}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<div v-for="product in warehouseStock.products" :key="product.code"
|
|
||||||
class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<span
|
|
||||||
class="text-xs font-mono text-gray-500 bg-white dark:bg-gray-900 px-2 py-1 rounded">
|
|
||||||
{{ product.code }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
{{ product.stock }} unidades
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="font-medium text-gray-900 dark:text-gray-100 mb-2 text-sm">
|
|
||||||
{{ product.name }}
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-xs text-gray-600 dark:text-gray-400">Valor total:</span>
|
|
||||||
<span class="text-sm font-semibold text-green-600 dark:text-green-400">
|
|
||||||
{{ formatCurrency(product.value) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress bar for stock level -->
|
|
||||||
<div class="mt-3">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400 mb-1">
|
|
||||||
<span>Stock</span>
|
|
||||||
<span>{{ Math.round((product.stock / 100) * 100) }}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
||||||
<div class="h-2 rounded-full transition-all duration-300" :class="product.stock > 50 ? 'bg-green-500' :
|
|
||||||
product.stock > 20 ? 'bg-yellow-500' : 'bg-red-500'"
|
|
||||||
:style="`width: ${Math.min(100, (product.stock / 100) * 100)}%`">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|
||||||
import { api, useForm } from '@Services/Api';
|
|
||||||
import { viewTo, apiTo, transl } from './Module';
|
|
||||||
import WarehouseService from './services/WarehouseService';
|
|
||||||
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
|
||||||
import PageHeader from '@Holos/PageHeader.vue';
|
|
||||||
import Form from './Form.vue'
|
|
||||||
|
|
||||||
/** Definiciones */
|
|
||||||
const vroute = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
const warehouseService = new WarehouseService();
|
|
||||||
/** Propiedades */
|
|
||||||
const form = useForm({
|
|
||||||
id: null,
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
address: '',
|
|
||||||
classifications: [], // Array de objetos {value, label}
|
|
||||||
is_active: true
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
form.transform(data => ({
|
|
||||||
...data,
|
|
||||||
classifications: warehouseService.processClassificationsForSubmit(data.classifications)
|
|
||||||
})).put(apiTo('update', { warehouse: form.id }), {
|
|
||||||
onSuccess: () => {
|
|
||||||
Notify.success(transl('update.success'))
|
|
||||||
router.push(viewTo({ name: 'index' }));
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadData() {
|
|
||||||
api.get(apiTo('show', { warehouse: vroute.params.id }), {
|
|
||||||
onSuccess: async (r) => {
|
|
||||||
const data = r.warehouse;
|
|
||||||
console.log('Datos del warehouse:', data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Cargar las clasificaciones disponibles
|
|
||||||
const availableClassifications = await warehouseService.getAvailableClassifications();
|
|
||||||
|
|
||||||
// Procesar las clasificaciones del warehouse para el formulario
|
|
||||||
const warehouseClassifications = data.classifications || [];
|
|
||||||
const formattedClassifications = [];
|
|
||||||
|
|
||||||
warehouseClassifications.forEach(classification => {
|
|
||||||
// Agregar la clasificación padre
|
|
||||||
formattedClassifications.push({
|
|
||||||
value: classification.id,
|
|
||||||
label: classification.name,
|
|
||||||
id: classification.id,
|
|
||||||
name: classification.name
|
|
||||||
});
|
|
||||||
|
|
||||||
// Agregar los hijos si existen
|
|
||||||
if (classification.children && classification.children.length > 0) {
|
|
||||||
classification.children.forEach(child => {
|
|
||||||
formattedClassifications.push({
|
|
||||||
value: child.id,
|
|
||||||
label: child.name,
|
|
||||||
id: child.id,
|
|
||||||
name: child.name
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
form.fill({
|
|
||||||
id: data.id,
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
description: data.description || '',
|
|
||||||
address: data.address || '',
|
|
||||||
classifications: formattedClassifications,
|
|
||||||
is_active: Boolean(data.is_active)
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando clasificaciones:', error);
|
|
||||||
// Fallback: llenar sin formatear las clasificaciones
|
|
||||||
form.fill({
|
|
||||||
id: data.id,
|
|
||||||
code: data.code,
|
|
||||||
name: data.name,
|
|
||||||
description: data.description || '',
|
|
||||||
address: data.address || '',
|
|
||||||
classifications: [],
|
|
||||||
is_active: Boolean(data.is_active)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<PageHeader
|
|
||||||
:title="transl('update.title')"
|
|
||||||
>
|
|
||||||
<RouterLink :to="viewTo({ name: 'index' })">
|
|
||||||
<IconButton
|
|
||||||
class="text-white"
|
|
||||||
icon="arrow_back"
|
|
||||||
:title="$t('return')"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
action="update"
|
|
||||||
:form="form"
|
|
||||||
@submit="submit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,338 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted, computed, watch } from 'vue';
|
|
||||||
import { transl } from './Module';
|
|
||||||
import WarehouseService from './services/WarehouseService';
|
|
||||||
|
|
||||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
|
||||||
import Input from '@Holos/Form/Input.vue';
|
|
||||||
import Textarea from '@Holos/Form/Textarea.vue';
|
|
||||||
import Selectable from '@Holos/Form/Selectable.vue';
|
|
||||||
import Checkbox from '@Holos/Checkbox.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'submit'
|
|
||||||
])
|
|
||||||
|
|
||||||
/** Servicios */
|
|
||||||
const warehouseService = new WarehouseService();
|
|
||||||
|
|
||||||
/** Propiedades reactivas */
|
|
||||||
const allClassifications = ref([]);
|
|
||||||
const loadingClassifications = ref(true);
|
|
||||||
const selectedParentClassifications = ref([]);
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const props = defineProps({
|
|
||||||
action: {
|
|
||||||
default: 'create',
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
form: Object
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Computadas */
|
|
||||||
// Obtener solo las clasificaciones padre (sin parent_id)
|
|
||||||
const parentClassifications = computed(() => {
|
|
||||||
return allClassifications.value.filter(classification => !classification.parent_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obtener subclasificaciones de los padres seleccionados
|
|
||||||
const availableSubclassifications = computed(() => {
|
|
||||||
if (selectedParentClassifications.value.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return allClassifications.value.filter(classification =>
|
|
||||||
classification.parent_id &&
|
|
||||||
selectedParentClassifications.value.includes(classification.parent_id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function submit() {
|
|
||||||
emit('submit')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Manejar selección de clasificación padre */
|
|
||||||
function handleParentClassificationChange(parentId, isSelected) {
|
|
||||||
if (isSelected) {
|
|
||||||
if (!selectedParentClassifications.value.includes(parentId)) {
|
|
||||||
selectedParentClassifications.value.push(parentId);
|
|
||||||
// Agregar la clasificación padre al form.classifications
|
|
||||||
const parentClassification = allClassifications.value.find(c => c.id === parentId);
|
|
||||||
if (parentClassification) {
|
|
||||||
props.form.classifications.push({
|
|
||||||
value: parentClassification.id,
|
|
||||||
label: parentClassification.name,
|
|
||||||
id: parentClassification.id,
|
|
||||||
name: parentClassification.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedParentClassifications.value = selectedParentClassifications.value.filter(id => id !== parentId);
|
|
||||||
|
|
||||||
// Remover la clasificación padre del form.classifications
|
|
||||||
props.form.classifications = props.form.classifications.filter(classification => {
|
|
||||||
const classificationId = classification.value || classification.id || classification;
|
|
||||||
return classificationId !== parentId;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remover también las subclasificaciones de este padre
|
|
||||||
if (props.form.classifications) {
|
|
||||||
props.form.classifications = props.form.classifications.filter(classification => {
|
|
||||||
const classificationId = classification.value || classification.id || classification;
|
|
||||||
const subClass = allClassifications.value.find(c => c.id === classificationId);
|
|
||||||
return !subClass || subClass.parent_id !== parentId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cargar clasificaciones desde la API */
|
|
||||||
async function loadClassifications() {
|
|
||||||
try {
|
|
||||||
loadingClassifications.value = true;
|
|
||||||
const response = await warehouseService.getAvailableClassifications();
|
|
||||||
|
|
||||||
// La respuesta viene en el formato { data: { warehouse_classifications: { data: [...] } } }
|
|
||||||
const classificationsData = response.data?.warehouse_classifications?.data || response.warehouse_classifications?.data || response.data || [];
|
|
||||||
|
|
||||||
// Aplanar la estructura jerárquica para tener todos los elementos en un solo array
|
|
||||||
const flattenClassifications = (items) => {
|
|
||||||
let flattened = [];
|
|
||||||
items.forEach(item => {
|
|
||||||
// Agregar el item padre
|
|
||||||
flattened.push(item);
|
|
||||||
// Agregar los hijos si existen
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
flattened.push(...item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return flattened;
|
|
||||||
};
|
|
||||||
|
|
||||||
allClassifications.value = flattenClassifications(classificationsData);
|
|
||||||
console.log('Clasificaciones cargadas:', allClassifications.value);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error cargando clasificaciones:', error);
|
|
||||||
allClassifications.value = [];
|
|
||||||
} finally {
|
|
||||||
loadingClassifications.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Watch para sincronizar con form.classifications existentes */
|
|
||||||
watch(() => props.form?.classifications, (newClassifications) => {
|
|
||||||
console.log('Watch form.classifications triggered:', newClassifications);
|
|
||||||
if (newClassifications && newClassifications.length > 0 && allClassifications.value.length > 0) {
|
|
||||||
// Extraer padres de las clasificaciones ya seleccionadas
|
|
||||||
const parentIds = new Set();
|
|
||||||
newClassifications.forEach(classification => {
|
|
||||||
const classificationId = classification.value || classification.id || classification;
|
|
||||||
console.log('Processing classification ID:', classificationId);
|
|
||||||
const fullClassification = allClassifications.value.find(c => c.id === classificationId);
|
|
||||||
console.log('Found full classification:', fullClassification);
|
|
||||||
|
|
||||||
if (fullClassification) {
|
|
||||||
if (fullClassification.parent_id) {
|
|
||||||
// Es una subclasificación, agregar su padre
|
|
||||||
console.log('Adding parent ID:', fullClassification.parent_id);
|
|
||||||
parentIds.add(fullClassification.parent_id);
|
|
||||||
} else {
|
|
||||||
// Es una clasificación padre, agregarla directamente
|
|
||||||
console.log('Adding parent classification ID:', fullClassification.id);
|
|
||||||
parentIds.add(fullClassification.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Final parent IDs:', Array.from(parentIds));
|
|
||||||
selectedParentClassifications.value = Array.from(parentIds);
|
|
||||||
}
|
|
||||||
}, { deep: true, immediate: true });
|
|
||||||
|
|
||||||
/** Watch adicional para cuando se cargan las clasificaciones */
|
|
||||||
watch(() => allClassifications.value, () => {
|
|
||||||
console.log('Watch allClassifications triggered, length:', allClassifications.value.length);
|
|
||||||
if (props.form?.classifications && allClassifications.value.length > 0) {
|
|
||||||
console.log('Re-processing form.classifications:', props.form.classifications);
|
|
||||||
// Re-ejecutar la lógica de sincronización cuando se cargan las clasificaciones
|
|
||||||
const parentIds = new Set();
|
|
||||||
props.form.classifications.forEach(classification => {
|
|
||||||
const classificationId = classification.value || classification.id || classification;
|
|
||||||
console.log('Re-processing classification ID:', classificationId);
|
|
||||||
const fullClassification = allClassifications.value.find(c => c.id === classificationId);
|
|
||||||
|
|
||||||
if (fullClassification) {
|
|
||||||
if (fullClassification.parent_id) {
|
|
||||||
// Es una subclasificación, agregar su padre
|
|
||||||
console.log('Re-adding parent ID:', fullClassification.parent_id);
|
|
||||||
parentIds.add(fullClassification.parent_id);
|
|
||||||
} else {
|
|
||||||
// Es una clasificación padre, agregarla directamente
|
|
||||||
console.log('Re-adding parent classification ID:', fullClassification.id);
|
|
||||||
parentIds.add(fullClassification.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Re-processed parent IDs:', Array.from(parentIds));
|
|
||||||
selectedParentClassifications.value = Array.from(parentIds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Ciclo de vida */
|
|
||||||
onMounted(() => {
|
|
||||||
loadClassifications();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full pb-2">
|
|
||||||
<p class="text-justify text-sm" v-text="transl(`${action}.description`)" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
<!-- Código del almacén -->
|
|
||||||
<Input
|
|
||||||
v-model="props.form.code"
|
|
||||||
id="code"
|
|
||||||
title="Código"
|
|
||||||
:onError="props.form.errors.code"
|
|
||||||
placeholder="Ej: WH001"
|
|
||||||
autofocus
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Nombre del almacén -->
|
|
||||||
<Input
|
|
||||||
v-model="props.form.name"
|
|
||||||
id="name"
|
|
||||||
title="Nombre"
|
|
||||||
:onError="props.form.errors.name"
|
|
||||||
placeholder="Ej: Almacén Central"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Descripción -->
|
|
||||||
<Textarea
|
|
||||||
v-model="props.form.description"
|
|
||||||
id="description"
|
|
||||||
title="Descripción"
|
|
||||||
:onError="props.form.errors.description"
|
|
||||||
placeholder="Descripción del almacén..."
|
|
||||||
class="md:col-span-2"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Dirección -->
|
|
||||||
<Textarea
|
|
||||||
v-model="props.form.address"
|
|
||||||
id="address"
|
|
||||||
title="Dirección"
|
|
||||||
:onError="props.form.errors.address"
|
|
||||||
placeholder="Dirección física del almacén..."
|
|
||||||
class="md:col-span-2"
|
|
||||||
rows="2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Clasificaciones Jerárquicas -->
|
|
||||||
<div class="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Titulo de Clasificaciones -->
|
|
||||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Clasificaciones
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Mensaje de carga -->
|
|
||||||
<div v-if="loadingClassifications" class="text-sm text-gray-500">
|
|
||||||
Cargando clasificaciones...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Clasificaciones Padre -->
|
|
||||||
<div v-if="parentClassifications.length > 0" class="space-y-3">
|
|
||||||
<h4 class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Clasificaciones
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
||||||
<div
|
|
||||||
v-for="parent in parentClassifications"
|
|
||||||
:key="parent.id"
|
|
||||||
class="flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
:id="`parent-${parent.id}`"
|
|
||||||
:checked="selectedParentClassifications.includes(parent.id)"
|
|
||||||
@update:checked="(value) => handleParentClassificationChange(parent.id, value)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label
|
|
||||||
:for="`parent-${parent.id}`"
|
|
||||||
class="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
|
||||||
>
|
|
||||||
{{ parent.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags de clasificaciones seleccionadas -->
|
|
||||||
<div v-if="selectedParentClassifications.length > 0" class="flex flex-wrap gap-2 mt-2">
|
|
||||||
<span
|
|
||||||
v-for="parentId in selectedParentClassifications"
|
|
||||||
:key="parentId"
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
|
||||||
>
|
|
||||||
{{ parentClassifications.find(p => p.id === parentId)?.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subclasificaciones -->
|
|
||||||
<div v-if="availableSubclassifications.length > 0" class="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<h4 class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Subclasificaciones
|
|
||||||
</h4>
|
|
||||||
<Selectable
|
|
||||||
v-model="props.form.classifications"
|
|
||||||
id="subclassifications"
|
|
||||||
label="name"
|
|
||||||
value="id"
|
|
||||||
:onError="props.form.errors.classifications"
|
|
||||||
:options="availableSubclassifications.map(sub => ({
|
|
||||||
value: sub.id,
|
|
||||||
label: sub.name,
|
|
||||||
name: sub.name,
|
|
||||||
id: sub.id
|
|
||||||
}))"
|
|
||||||
placeholder="Seleccionar subclasificaciones"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mensaje cuando no hay clasificaciones padre seleccionadas -->
|
|
||||||
<div v-else-if="!loadingClassifications && selectedParentClassifications.length === 0" class="text-sm text-gray-500 italic">
|
|
||||||
Selecciona al menos una clasificación para ver las subclasificaciones disponibles
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error de clasificaciones -->
|
|
||||||
<div v-if="props.form.errors.classifications" class="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{{ props.form.errors.classifications }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Botón de envío -->
|
|
||||||
<div class="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
|
||||||
<PrimaryButton
|
|
||||||
:loading="props.form.processing"
|
|
||||||
:disabled="props.form.processing"
|
|
||||||
>
|
|
||||||
{{ transl(`${props.action}.button`) }}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
import { useRouter, RouterLink } from 'vue-router';
|
|
||||||
import { useSearcher } from '@Services/Api';
|
|
||||||
import { hasPermission } from '@Plugins/RolePermission';
|
|
||||||
import { can, apiTo, viewTo, transl } from './Module'
|
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
|
||||||
import Table from '@Holos/Table.vue';
|
|
||||||
import ShowView from './Modals/Show.vue';
|
|
||||||
|
|
||||||
import Card from '@Holos/Card/Card.vue';
|
|
||||||
import CardHeader from '@Holos/Card/CardHeader.vue';
|
|
||||||
import CardTitle from '@Holos/Card/CardTitle.vue';
|
|
||||||
import CardDescription from '@Holos/Card/CardDescription.vue';
|
|
||||||
import CardContent from '@Holos/Card/CardContent.vue';
|
|
||||||
import Button from '@Holos/Button/Button.vue';
|
|
||||||
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const models = ref([]);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const showModal = ref(null);
|
|
||||||
const destroyModal = ref(null);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
const searcher = useSearcher({
|
|
||||||
url: apiTo('index'),
|
|
||||||
onSuccess: (r) => {
|
|
||||||
console.log('Datos recibidos:', r);
|
|
||||||
console.log('Warehouses:', r.warehouses);
|
|
||||||
if (r.warehouses && r.warehouses.length > 0) {
|
|
||||||
console.log('Primer warehouse:', r.warehouses[0]);
|
|
||||||
}
|
|
||||||
models.value = r.warehouses;
|
|
||||||
},
|
|
||||||
onError: () => models.value = []
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Función para eliminar */
|
|
||||||
const deleteItem = (item) => {
|
|
||||||
destroyModal.value.open(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Función de debug para navegación */
|
|
||||||
const debugNavigation = (warehouse) => {
|
|
||||||
console.log('Debug navigation:', warehouse);
|
|
||||||
console.log('viewTo result:', viewTo({ name: 'edit', params: { id: warehouse.id } }));
|
|
||||||
router.push(viewTo({ name: 'edit', params: { id: warehouse.id } }));
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Ciclos */
|
|
||||||
onMounted(() => {
|
|
||||||
searcher.search();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-foreground">Inventario</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Control de stock y movimientos por almacén
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SearcherHead :title="transl('name')" @search="(x) => searcher.search(x)">
|
|
||||||
<RouterLink :to="viewTo({ name: 'create' })">
|
|
||||||
<IconButton class="text-white" icon="add" :title="$t('crud.create')" filled />
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton icon="refresh" :title="$t('refresh')" @click="searcher.search()" />
|
|
||||||
</SearcherHead>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Gestión de Almacenes</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Gestiona los almacenes donde se almacenan los productos. Puedes crear, editar y eliminar almacenes
|
|
||||||
según sea necesario.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
|
|
||||||
<Card v-for="warehouse in models.data" :key="warehouse.id" class="relative overflow-hidden">
|
|
||||||
<CardContent class="pt-4 pb-4 px-4">
|
|
||||||
<div class="flex items-start justify-between mb-3">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<GoogleIcon class="w-6 h-6 text-primary" name="warehouse" />
|
|
||||||
<div>
|
|
||||||
<h3 class="font-semibold text-sm">{{ warehouse.name }}</h3>
|
|
||||||
<p class="text-xs text-muted-foreground font-mono">
|
|
||||||
{{ warehouse.code }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
{{ warehouse.is_active ? 'Activo' : 'Inactivo' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-1 text-xs text-muted-foreground mb-2">
|
|
||||||
<GoogleIcon class="w-6 h-6" name="distance" />
|
|
||||||
<p>{{ warehouse.address ? warehouse.address : 'Sin dirección' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1 text-xs text-muted-foreground mb-3">
|
|
||||||
|
|
||||||
<span v-for="classification in warehouse.classifications" :key="classification.id"
|
|
||||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
|
|
||||||
{{ classification.code }}
|
|
||||||
</span>
|
|
||||||
<span v-if="warehouse.classifications.length > 3"
|
|
||||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
|
||||||
+{{ warehouse.classifications.length - 3 }}
|
|
||||||
</span>
|
|
||||||
<span v-if="warehouse.classifications.length === 0" class="text-xs text-gray-400">
|
|
||||||
Sin clasificaciones
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mt-3 pt-2 border-t border-border gap-3">
|
|
||||||
|
|
||||||
<RouterLink class="h-fit" :to="viewTo({ name: 'details', params: { id: warehouse.id } })">
|
|
||||||
<Button size="sm" variant="solid" color="info" asLink>
|
|
||||||
Ver Detalles
|
|
||||||
</Button>
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<!-- Opción 1: RouterLink + asLink -->
|
|
||||||
<RouterLink class="h-fit" :to="viewTo({ name: 'edit', params: { id: warehouse.id } })">
|
|
||||||
<Button size="sm" variant="solid" color="warning" asLink>
|
|
||||||
Editar
|
|
||||||
</Button>
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<Button size="sm" variant="solid" @click="deleteItem(warehouse)" color="danger">
|
|
||||||
Eliminar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShowView ref="showModal" @reload="searcher.search()" />
|
|
||||||
|
|
||||||
<DestroyView ref="destroyModal" subtitle="name" :to="(id) => apiTo('destroy', { warehouse: id })"
|
|
||||||
@update="searcher.search()" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,311 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { getDateTime } from '@Controllers/DateController';
|
|
||||||
import { viewTo, apiTo, transl } from '../Module';
|
|
||||||
import WarehouseService from '../services/WarehouseService';
|
|
||||||
import Notify from '@Plugins/Notify';
|
|
||||||
|
|
||||||
import Header from '@Holos/Modal/Elements/Header.vue';
|
|
||||||
import ShowModal from '@Holos/Modal/Show.vue';
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
import Button from '@Holos/Button/Button.vue';
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue';
|
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
|
||||||
|
|
||||||
/** Eventos */
|
|
||||||
const emit = defineEmits([
|
|
||||||
'close',
|
|
||||||
'reload'
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Servicios */
|
|
||||||
const warehouseService = new WarehouseService();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
/** Propiedades */
|
|
||||||
const model = ref(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
/** Referencias */
|
|
||||||
const modalRef = ref(null);
|
|
||||||
const destroyModal = ref(null);
|
|
||||||
|
|
||||||
/** Métodos */
|
|
||||||
function close() {
|
|
||||||
model.value = null;
|
|
||||||
emit('close');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para actualizar el estado del almacén */
|
|
||||||
async function toggleStatus(item) {
|
|
||||||
if (loading.value) return;
|
|
||||||
|
|
||||||
const newStatus = !item.is_active;
|
|
||||||
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
// Usar el servicio para actualizar el estado
|
|
||||||
await warehouseService.updateStatus(item.id, newStatus);
|
|
||||||
|
|
||||||
// Actualizar el modelo local
|
|
||||||
item.is_active = newStatus;
|
|
||||||
|
|
||||||
// Notificación de éxito
|
|
||||||
const statusText = newStatus ? 'activado' : 'desactivado';
|
|
||||||
Notify.success(
|
|
||||||
`Almacén "${item.code}" ${statusText} exitosamente`,
|
|
||||||
'Estado actualizado'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emitir evento para recargar la lista principal si es necesario
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error actualizando estado:', error);
|
|
||||||
|
|
||||||
// Manejo específico de errores según la estructura de tu API
|
|
||||||
let errorMessage = 'Error al actualizar el estado del almacén';
|
|
||||||
let errorTitle = 'Error';
|
|
||||||
|
|
||||||
if (error?.response?.data) {
|
|
||||||
const errorData = error.response.data;
|
|
||||||
|
|
||||||
// Caso 1: Error con estructura específica de tu API
|
|
||||||
if (errorData.status === 'error') {
|
|
||||||
if (errorData.errors) {
|
|
||||||
// Errores de validación - extraer el primer error
|
|
||||||
const firstField = Object.keys(errorData.errors)[0];
|
|
||||||
const firstError = errorData.errors[firstField];
|
|
||||||
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
|
||||||
errorTitle = 'Error de validación';
|
|
||||||
} else if (errorData.message) {
|
|
||||||
// Mensaje general del error
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
errorTitle = 'Error del servidor';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Caso 2: Otros formatos de error
|
|
||||||
else if (errorData.message) {
|
|
||||||
errorMessage = errorData.message;
|
|
||||||
}
|
|
||||||
} else if (error?.message) {
|
|
||||||
// Error genérico de la petición (red, timeout, etc.)
|
|
||||||
errorMessage = `Error de conexión: ${error.message}`;
|
|
||||||
errorTitle = 'Error de red';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notificación de error
|
|
||||||
Notify.error(errorMessage, errorTitle);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para editar almacén */
|
|
||||||
function editWarehouse() {
|
|
||||||
// Navegar a la vista de edición del almacén
|
|
||||||
const editUrl = viewTo({ name: 'edit', params: { id: model.value.id } });
|
|
||||||
router.push(editUrl);
|
|
||||||
// Cerrar el modal actual
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para eliminar almacén */
|
|
||||||
function deleteWarehouse() {
|
|
||||||
destroyModal.value.open(model.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para recargar después de eliminar */
|
|
||||||
function onWarehouseDeleted() {
|
|
||||||
close();
|
|
||||||
emit('reload');
|
|
||||||
|
|
||||||
Notify.success(
|
|
||||||
'Almacén eliminado exitosamente',
|
|
||||||
'Eliminación exitosa'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Función para cancelar eliminación */
|
|
||||||
function onDeleteCancelled() {
|
|
||||||
// No es necesario hacer nada especial
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Exposer métodos públicos */
|
|
||||||
defineExpose({
|
|
||||||
open: (data) => {
|
|
||||||
model.value = data;
|
|
||||||
modalRef.value.open();
|
|
||||||
},
|
|
||||||
close
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ShowModal
|
|
||||||
ref="modalRef"
|
|
||||||
@close="close"
|
|
||||||
>
|
|
||||||
<div v-if="model">
|
|
||||||
<Header
|
|
||||||
:title="model.code"
|
|
||||||
:subtitle="model.name"
|
|
||||||
>
|
|
||||||
<div class="flex w-full flex-col">
|
|
||||||
<div class="flex w-full justify-center items-center">
|
|
||||||
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-white text-3xl"
|
|
||||||
name="warehouse"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<div class="flex w-full p-4 space-y-6">
|
|
||||||
<div class="w-full space-y-6">
|
|
||||||
<!-- Información Principal -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<!-- Código y Estado -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('code') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-1">
|
|
||||||
<code class="font-mono text-lg bg-gray-100 dark:bg-gray-700 px-3 py-2 rounded-lg block">
|
|
||||||
{{ model.code }}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('is_active') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-2">
|
|
||||||
<Button
|
|
||||||
:variant="'smooth'"
|
|
||||||
:color="model.is_active ? 'success' : 'danger'"
|
|
||||||
:size="'sm'"
|
|
||||||
:loading="loading"
|
|
||||||
@click="toggleStatus(model)"
|
|
||||||
>
|
|
||||||
{{ model.is_active ? $t('Activo') : $t('Inactivo') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nombre y Dirección -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('name') }}
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 text-lg font-semibold">{{ model.name }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="model.address">
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('address') }}
|
|
||||||
</label>
|
|
||||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{{ model.address }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Descripción -->
|
|
||||||
<div v-if="model.description">
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('description') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<p class="text-sm">{{ model.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Clasificaciones -->
|
|
||||||
<div>
|
|
||||||
<label class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{{ transl('classifications') }}
|
|
||||||
</label>
|
|
||||||
<div class="mt-2">
|
|
||||||
<div v-if="model.classifications && model.classifications.length > 0" class="flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="classification in model.classifications"
|
|
||||||
:key="classification.id"
|
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"
|
|
||||||
>
|
|
||||||
<GoogleIcon class="w-4 h-4 mr-2" name="category" />
|
|
||||||
{{ classification.code }} - {{ classification.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<GoogleIcon class="w-5 h-5 mr-2 text-gray-400" name="info" />
|
|
||||||
<p class="text-sm text-gray-500">Sin clasificaciones asignadas</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Información de Fechas -->
|
|
||||||
<div class="border-t pt-6">
|
|
||||||
<h4 class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
Información del Sistema
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Creado:</span>
|
|
||||||
<p class="mt-1">{{ getDateTime(model.created_at) }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Actualizado:</span>
|
|
||||||
<p class="mt-1">{{ getDateTime(model.updated_at) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botones de acción -->
|
|
||||||
<div class="flex justify-between space-x-3 pt-4 border-t">
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
@click="editWarehouse"
|
|
||||||
>
|
|
||||||
<GoogleIcon class="w-4 h-4 mr-2" name="edit" />
|
|
||||||
{{ $t('crud.edit') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
color="danger"
|
|
||||||
@click="deleteWarehouse"
|
|
||||||
>
|
|
||||||
<GoogleIcon class="w-4 h-4 mr-2" name="delete" />
|
|
||||||
{{ $t('crud.destroy') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ $t('crud.close') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ShowModal>
|
|
||||||
|
|
||||||
<!-- Modal de confirmación para eliminar -->
|
|
||||||
<DestroyView
|
|
||||||
ref="destroyModal"
|
|
||||||
subtitle="name"
|
|
||||||
:to="(id) => apiTo('destroy', { warehouse: id })"
|
|
||||||
@update="onWarehouseDeleted"
|
|
||||||
@cancel="onDeleteCancelled"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { lang } from '@Lang/i18n';
|
|
||||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
|
||||||
|
|
||||||
// Ruta API
|
|
||||||
const apiTo = (name, params = {}) => route(`warehouses.${name}`, params)
|
|
||||||
|
|
||||||
// Ruta visual
|
|
||||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({
|
|
||||||
name: `admin.warehouses.${name}`, params, query
|
|
||||||
})
|
|
||||||
|
|
||||||
// Obtener traducción del componente
|
|
||||||
const transl = (str) => lang(`admin.warehouses.${str}`)
|
|
||||||
|
|
||||||
// Control de permisos
|
|
||||||
const can = (permission) => hasPermission(`admin.warehouses.${permission}`)
|
|
||||||
|
|
||||||
export {
|
|
||||||
can,
|
|
||||||
viewTo,
|
|
||||||
apiTo,
|
|
||||||
transl
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,125 +0,0 @@
|
|||||||
/**
|
|
||||||
* Interfaces para Warehouses
|
|
||||||
*
|
|
||||||
* @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 {Object} pivot - Relación many-to-many
|
|
||||||
* @property {number} pivot.warehouse_id - ID del almacén
|
|
||||||
* @property {number} pivot.classification_id - ID de la clasificación
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Warehouse
|
|
||||||
* @property {number} id - ID del almacén
|
|
||||||
* @property {string} code - Código del almacén
|
|
||||||
* @property {string} name - Nombre del almacén
|
|
||||||
* @property {string|null} description - Descripción del almacén
|
|
||||||
* @property {string|null} address - Dirección del almacén
|
|
||||||
* @property {boolean} is_active - Estado activo/inactivo
|
|
||||||
* @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[]} classifications - Clasificaciones asociadas
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehousePaginatedResponse
|
|
||||||
* @property {number} current_page - Página actual
|
|
||||||
* @property {Warehouse[]} data - Array de almacenes
|
|
||||||
* @property {string} first_page_url - URL de la primera página
|
|
||||||
* @property {number} from - Registro inicial
|
|
||||||
* @property {number} last_page - Última página
|
|
||||||
* @property {string} last_page_url - URL de la última página
|
|
||||||
* @property {Object[]} links - Enlaces de paginación
|
|
||||||
* @property {string|null} next_page_url - URL de la siguiente página
|
|
||||||
* @property {string} path - Path base de la API
|
|
||||||
* @property {number} per_page - Registros por página
|
|
||||||
* @property {string|null} prev_page_url - URL de la página anterior
|
|
||||||
* @property {number} to - Registro final
|
|
||||||
* @property {number} total - Total de registros
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseResponse
|
|
||||||
* @property {string} status - Estado de la respuesta
|
|
||||||
* @property {Object} data - Datos de la respuesta
|
|
||||||
* @property {WarehousePaginatedResponse} data.warehouses - Datos paginados de almacenes
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} SingleWarehouseResponse
|
|
||||||
* @property {string} status - Estado de la respuesta
|
|
||||||
* @property {Object} data - Datos de la respuesta
|
|
||||||
* @property {string} data.message - Mensaje de la respuesta
|
|
||||||
* @property {Warehouse} data.warehouse - Almacén individual
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseCreateRequest
|
|
||||||
* @property {string} code - Código del almacén
|
|
||||||
* @property {string} name - Nombre del almacén
|
|
||||||
* @property {string|null} description - Descripción del almacén
|
|
||||||
* @property {string|null} address - Dirección del almacén
|
|
||||||
* @property {boolean} is_active - Estado activo/inactivo
|
|
||||||
* @property {number[]} classifications - Array de IDs de clasificaciones
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseUpdateRequest
|
|
||||||
* @property {string} code - Código del almacén
|
|
||||||
* @property {string} name - Nombre del almacén
|
|
||||||
* @property {string|null} description - Descripción del almacén
|
|
||||||
* @property {string|null} address - Dirección del almacén
|
|
||||||
* @property {boolean} is_active - Estado activo/inactivo
|
|
||||||
* @property {number[]} classifications - Array de IDs de clasificaciones
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseFormData
|
|
||||||
* @property {number|null} id - ID del almacén (para edición)
|
|
||||||
* @property {string} code - Código del almacén
|
|
||||||
* @property {string} name - Nombre del almacén
|
|
||||||
* @property {string} description - Descripción del almacén
|
|
||||||
* @property {string} address - Dirección del almacén
|
|
||||||
* @property {boolean} is_active - Estado activo/inactivo
|
|
||||||
* @property {Object[]} classifications - Clasificaciones seleccionadas en formato {value, label}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} ClassificationSelectOption
|
|
||||||
* @property {number} value - ID de la clasificación
|
|
||||||
* @property {string} label - Nombre de la clasificación con indentación
|
|
||||||
* @property {string} code - Código de la clasificación
|
|
||||||
* @property {string|null} description - Descripción de la clasificación
|
|
||||||
* @property {number} level - Nivel jerárquico (para indentación)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseSearchFilters
|
|
||||||
* @property {string|null} search - Término de búsqueda
|
|
||||||
* @property {string|null} status - Estado del filtro (active/inactive)
|
|
||||||
* @property {number} page - Número de página
|
|
||||||
* @property {number} per_page - Elementos por página
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} WarehouseValidationErrors
|
|
||||||
* @property {string[]} code - Errores del campo código
|
|
||||||
* @property {string[]} name - Errores del campo nombre
|
|
||||||
* @property {string[]} description - Errores del campo descripción
|
|
||||||
* @property {string[]} address - Errores del campo dirección
|
|
||||||
* @property {string[]} classifications - Errores del campo clasificaciones
|
|
||||||
*/
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
/**
|
|
||||||
* Servicio para Warehouses
|
|
||||||
*
|
|
||||||
* @author Sistema
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { api, apiURL } from '@Services/Api';
|
|
||||||
|
|
||||||
export default class WarehouseService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener todos los almacenes
|
|
||||||
* @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('warehouses'), {
|
|
||||||
params,
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener un almacén por ID
|
|
||||||
* @param {number} id - ID del almacén
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async getById(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.get(apiURL(`warehouses/${id}`), {
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crear un nuevo almacén
|
|
||||||
* @param {Object} data - Datos del almacén
|
|
||||||
* @param {string} data.code - Código del almacén
|
|
||||||
* @param {string} data.name - Nombre del almacén
|
|
||||||
* @param {string|null} data.description - Descripción del almacén
|
|
||||||
* @param {string|null} data.address - Dirección del almacén
|
|
||||||
* @param {boolean} data.is_active - Estado activo/inactivo
|
|
||||||
* @param {number[]} data.classifications - Array de IDs de clasificaciones
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async create(data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.post(apiURL('warehouses'), data, {
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actualizar un almacén existente
|
|
||||||
* @param {number} id - ID del almacén
|
|
||||||
* @param {Object} data - Datos actualizados
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async update(id, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.put(apiURL(`warehouses/${id}`), data, {
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Eliminar un almacén
|
|
||||||
* @param {number} id - ID del almacén
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async delete(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.delete(apiURL(`warehouses/${id}`), {
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actualizar el estado de un almacén
|
|
||||||
* @param {number} id - ID del almacén
|
|
||||||
* @param {boolean} isActive - Nuevo estado
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async updateStatus(id, isActive) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.put(apiURL(`warehouses/${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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Buscar almacenes con filtros
|
|
||||||
* @param {Object} filters - Filtros de búsqueda
|
|
||||||
* @param {string} filters.search - Término de búsqueda
|
|
||||||
* @param {string} filters.status - Estado (active/inactive)
|
|
||||||
* @param {number} filters.page - Página
|
|
||||||
* @returns {Promise} Promesa con la respuesta
|
|
||||||
*/
|
|
||||||
async search(filters = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.get(apiURL('warehouses'), {
|
|
||||||
params: filters,
|
|
||||||
onSuccess: (response) => resolve(response),
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validar datos del almacén
|
|
||||||
* @param {Object} data - Datos a validar
|
|
||||||
* @returns {Object} Objeto con errores de validación 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';
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtener clasificaciones disponibles para asignar a almacenes
|
|
||||||
* @returns {Promise} Promesa con las clasificaciones
|
|
||||||
*/
|
|
||||||
async getAvailableClassifications() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api.get(apiURL('catalogs/warehouse-classifications'), {
|
|
||||||
onSuccess: (response) => {
|
|
||||||
// Retornar la respuesta completa tal como viene de la API
|
|
||||||
resolve(response);
|
|
||||||
},
|
|
||||||
onError: (error) => reject(error)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formatear clasificaciones jerárquicas para uso en Selectable
|
|
||||||
* @param {Array} classifications - Array de clasificaciones
|
|
||||||
* @param {number} level - Nivel de indentación
|
|
||||||
* @returns {Array} Clasificaciones formateadas
|
|
||||||
*/
|
|
||||||
formatClassificationsForSelect(classifications, level = 0) {
|
|
||||||
const formatted = [];
|
|
||||||
|
|
||||||
classifications.forEach(classification => {
|
|
||||||
formatted.push({
|
|
||||||
value: classification.id,
|
|
||||||
label: ' '.repeat(level) + classification.name,
|
|
||||||
code: classification.code,
|
|
||||||
description: classification.description,
|
|
||||||
level: level
|
|
||||||
});
|
|
||||||
|
|
||||||
// Agregar hijos recursivamente
|
|
||||||
if (classification.children && classification.children.length > 0) {
|
|
||||||
formatted.push(...this.formatClassificationsForSelect(classification.children, level + 1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Procesar clasificaciones seleccionadas para envío al backend
|
|
||||||
* Incluye tanto clasificaciones padre como subclasificaciones
|
|
||||||
* @param {Array} selectedClassifications - Clasificaciones seleccionadas del formulario
|
|
||||||
* @returns {Array} Array de IDs de clasificaciones (padres + hijos)
|
|
||||||
*/
|
|
||||||
processClassificationsForSubmit(selectedClassifications) {
|
|
||||||
if (!Array.isArray(selectedClassifications)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const classificationIds = selectedClassifications.map(classification => {
|
|
||||||
return typeof classification === 'object' ?
|
|
||||||
(classification.value || classification.id) :
|
|
||||||
classification;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remover duplicados y retornar
|
|
||||||
return [...new Set(classificationIds)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Procesar clasificaciones del backend para mostrar en formulario
|
|
||||||
* @param {Array} classifications - Clasificaciones del backend
|
|
||||||
* @param {Array} availableClassifications - Todas las clasificaciones disponibles
|
|
||||||
* @returns {Array} Clasificaciones formateadas para el formulario
|
|
||||||
*/
|
|
||||||
processClassificationsForForm(classifications, availableClassifications) {
|
|
||||||
if (!Array.isArray(classifications) || !Array.isArray(availableClassifications)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return classifications.map(classification => {
|
|
||||||
const found = availableClassifications.find(available => available.value === classification.id);
|
|
||||||
return found || {
|
|
||||||
value: classification.id,
|
|
||||||
label: classification.name,
|
|
||||||
code: classification.code
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -306,136 +306,6 @@ const router = createRouter({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'warehouses',
|
|
||||||
name: 'admin.warehouses',
|
|
||||||
meta: {
|
|
||||||
title: 'Bodegas',
|
|
||||||
icon: 'inventory_2',
|
|
||||||
},
|
|
||||||
redirect: '/admin/warehouses',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':id',
|
|
||||||
name: 'admin.warehouses.details',
|
|
||||||
component: () => import('@Pages/Warehouses/Details.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'Detalles',
|
|
||||||
icon: 'info',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'admin.warehouses.index',
|
|
||||||
component: () => import('@Pages/Warehouses/Index.vue'),
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'create',
|
|
||||||
name: 'admin.warehouses.create',
|
|
||||||
component: () => import('@Pages/Warehouses/Create.vue'),
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
title: 'Crear',
|
|
||||||
icon: 'add',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/edit',
|
|
||||||
name: 'admin.warehouses.edit',
|
|
||||||
component: () => import('@Pages/Warehouses/Edit.vue'),
|
|
||||||
|
|
||||||
meta: {
|
|
||||||
title: 'Editar',
|
|
||||||
icon: 'edit',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'units-measure',
|
|
||||||
name: 'admin.units-measure',
|
|
||||||
meta: {
|
|
||||||
title: 'Unidades de Medida',
|
|
||||||
icon: 'straighten',
|
|
||||||
},
|
|
||||||
redirect: '/admin/units-measure',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'admin.units-measure.index',
|
|
||||||
component: () => import('@Pages/UnitsMeasure/Index.vue'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 'units-measure',
|
|
||||||
name: 'admin.units-measure',
|
|
||||||
meta: {
|
|
||||||
title: 'Unidades de Medida',
|
|
||||||
icon: 'straighten',
|
|
||||||
},
|
|
||||||
redirect: '/admin/units-measure',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'admin.units-measure.index',
|
|
||||||
component: () => import('@Pages/UnitsMeasure/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'create',
|
|
||||||
name: 'admin.units-measure.create',
|
|
||||||
component: () => import('@Pages/UnitsMeasure/Create.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'Crear',
|
|
||||||
icon: 'add',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':id/edit',
|
|
||||||
name: 'admin.units-measure.edit',
|
|
||||||
component: () => import('@Pages/UnitsMeasure/Edit.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'Editar',
|
|
||||||
icon: 'edit',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
name: 'admin.roles',
|
name: 'admin.roles',
|
||||||
|
|||||||
@ -1,218 +0,0 @@
|
|||||||
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 }
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
// 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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Loading…
x
Reference in New Issue
Block a user