362 lines
16 KiB
Vue
362 lines
16 KiB
Vue
<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> |