343 lines
15 KiB
Vue
343 lines
15 KiB
Vue
<script setup>
|
|
import { ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { getDateTime } from '@Controllers/DateController';
|
|
import { viewTo, apiTo } from '../Module';
|
|
import ProductService from '../services/ProductService';
|
|
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';
|
|
|
|
/** Eventos */
|
|
const emit = defineEmits([
|
|
'close',
|
|
'reload'
|
|
]);
|
|
|
|
/** Servicios */
|
|
const productService = new ProductService();
|
|
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 actualizar el estado del producto */
|
|
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 productService.updateStatus(item.id, newStatus);
|
|
|
|
// Actualizar el modelo local
|
|
item.is_active = newStatus;
|
|
|
|
// Notificación de éxito
|
|
const statusText = newStatus ? 'activado' : 'desactivado';
|
|
Notify.success(
|
|
`Producto "${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 producto';
|
|
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 producto */
|
|
function editProduct() {
|
|
const editUrl = viewTo({ name: 'edit', params: { id: model.value.id } });
|
|
router.push(editUrl);
|
|
close();
|
|
}
|
|
|
|
/** Función para duplicar producto */
|
|
async function duplicateProduct() {
|
|
if (loading.value) return;
|
|
|
|
try {
|
|
loading.value = true;
|
|
|
|
await productService.duplicate(model.value.id);
|
|
|
|
Notify.success(
|
|
`Producto "${model.value.code}" duplicado exitosamente`,
|
|
'Producto duplicado'
|
|
);
|
|
|
|
emit('reload');
|
|
close();
|
|
|
|
} catch (error) {
|
|
console.error('Error duplicando producto:', error);
|
|
Notify.error('Error al duplicar el producto');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
/** Función para formatear atributos */
|
|
const formatAttributesDisplay = (attributes) => {
|
|
if (!attributes || typeof attributes !== 'object') return [];
|
|
return Object.entries(attributes).map(([key, value]) => ({ key, value }));
|
|
};
|
|
|
|
/** 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="inventory_2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Header>
|
|
|
|
<div class="flex w-full p-4 space-y-6">
|
|
<!-- Información básica -->
|
|
<div class="w-full space-y-6">
|
|
<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>SKU: </b>
|
|
<code class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
|
{{ model.sku }}
|
|
</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>
|
|
|
|
<!-- Atributos personalizados -->
|
|
<div v-if="model.attributes && Object.keys(model.attributes).length > 0" class="pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-start">
|
|
<GoogleIcon
|
|
class="text-xl text-warning mt-1"
|
|
name="tune"
|
|
/>
|
|
<div class="pl-3 w-full">
|
|
<p class="font-bold text-lg leading-none pb-3">
|
|
{{ $t('Atributos Personalizados') }}
|
|
</p>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div
|
|
v-for="attr in formatAttributesDisplay(model.attributes)"
|
|
:key="attr.key"
|
|
class="flex items-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
|
>
|
|
<div class="flex-1">
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
{{ attr.key }}
|
|
</p>
|
|
<p class="font-semibold text-sm">
|
|
{{ attr.value }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clasificaciones -->
|
|
<div v-if="(model.warehouse_classifications && model.warehouse_classifications.length > 0) || (model.comercial_classifications && model.comercial_classifications.length > 0)" class="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="category"
|
|
/>
|
|
<div class="pl-3 w-full">
|
|
<p class="font-bold text-lg leading-none pb-3">
|
|
{{ $t('Clasificaciones') }}
|
|
</p>
|
|
|
|
<!-- Clasificaciones de Almacén -->
|
|
<div v-if="model.warehouse_classifications && model.warehouse_classifications.length > 0" class="mb-4">
|
|
<p class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">
|
|
Clasificaciones de Almacén
|
|
</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="classification in model.warehouse_classifications"
|
|
:key="'w-' + classification.id"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100"
|
|
>
|
|
<code class="mr-2 text-xs">{{ classification.code }}</code>
|
|
{{ classification.name }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clasificaciones Comerciales -->
|
|
<div v-if="model.comercial_classifications && model.comercial_classifications.length > 0">
|
|
<p class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">
|
|
Clasificaciones Comerciales
|
|
</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="classification in model.comercial_classifications"
|
|
:key="'c-' + classification.id"
|
|
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100"
|
|
>
|
|
<code class="mr-2 text-xs">{{ classification.code }}</code>
|
|
{{ classification.name }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Acciones rápidas -->
|
|
<div class="pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-center gap-3">
|
|
<Button
|
|
:variant="'outline'"
|
|
:color="'primary'"
|
|
:loading="loading"
|
|
@click="editProduct"
|
|
>
|
|
<GoogleIcon name="edit" class="mr-2" />
|
|
{{ $t('crud.edit') }}
|
|
</Button>
|
|
|
|
<Button
|
|
:variant="'outline'"
|
|
:color="'info'"
|
|
:loading="loading"
|
|
@click="duplicateProduct"
|
|
>
|
|
<GoogleIcon name="content_copy" class="mr-2" />
|
|
{{ $t('Duplicar') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ShowModal>
|
|
</template>
|