546 lines
24 KiB
Vue

<script setup>
import { ref, onMounted } from 'vue';
import { comercialTo, transl } from './Module';
import ProductService from './services/ProductService';
import { useSearcher } from '@Services/Api';
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 Switch from '@Holos/Form/Switch.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
import Button from '@Holos/Button/Button.vue';
import Badge from '@Components/ui/Tags/Badge.vue';
import Label from "@Holos/Form/Elements/Label.vue";
/** Eventos */
const emit = defineEmits(['submit']);
/** Propiedades */
const props = defineProps({
action: {
default: 'create',
type: String
},
form: Object
});
/** Referencias */
const productService = new ProductService();
const warehouseClassifications = ref([]);
const comercialClassifications = ref([]);
const loadingClassifications = ref(false);
const newAttrKey = ref('');
const newAttrValue = ref('');
const availableClassifications = ref([]);
const newClassification = ref('');
const newSubclassification = ref('');
const selectedParentForNew = ref(null);
/** Métodos */
function submit() {
emit('submit');
}
/** Agregar valor a un atributo */
function addAttributeValue() {
const key = newAttrKey.value.trim();
const value = newAttrValue.value.trim();
if (!key || !value) {
window.Notify?.warning('El nombre del atributo y el valor son requeridos');
return;
}
if (!props.form.attributes) {
props.form.attributes = {};
}
// Si el atributo no existe, crear array vacío
if (!props.form.attributes[key]) {
props.form.attributes[key] = [];
}
// Evitar valores duplicados en el mismo atributo
if (props.form.attributes[key].includes(value)) {
window.Notify?.warning(`El valor "${value}" ya existe en "${key}"`);
return;
}
// Agregar el valor al atributo
props.form.attributes[key].push(value);
// Solo limpiar el valor, mantener el nombre para seguir agregando
newAttrValue.value = '';
window.Notify?.success(`"${value}" agregado a "${key}"`);
}
/** Remover un atributo completo */
function removeAttributeKey(attrName) {
if (props.form.attributes && props.form.attributes[attrName]) {
delete props.form.attributes[attrName];
window.Notify?.success(`Atributo "${attrName}" eliminado`);
}
}
/** Remover un valor específico de un atributo */
function removeAttributeValue(attrName, value) {
if (props.form.attributes && props.form.attributes[attrName]) {
props.form.attributes[attrName] = props.form.attributes[attrName].filter(v => v !== value);
// Si no quedan valores, eliminar el atributo completo
if (props.form.attributes[attrName].length === 0) {
delete props.form.attributes[attrName];
}
}
}
/** Métodos para clasificaciones comerciales */
const handleAddClassification = (classificationId) => {
if (!props.form.comercial_classification_ids) {
props.form.comercial_classification_ids = [];
}
if (!props.form.comercial_classification_ids.includes(classificationId)) {
props.form.comercial_classification_ids.push(classificationId);
window.Notify?.success('Clasificación agregada correctamente');
}
};
const handleRemoveClassification = (classificationId) => {
if (!props.form.comercial_classification_ids) return;
const index = props.form.comercial_classification_ids.indexOf(classificationId);
if (index > -1) {
props.form.comercial_classification_ids.splice(index, 1);
}
};
const handleCreateNewClassification = () => {
if (!newClassification.value.trim()) {
window.Notify?.error('El nombre de la clasificación es requerido');
return;
}
const newId = Math.max(...availableClassifications.value.map(c => c.id)) + 1;
const newClass = {
id: newId,
name: newClassification.value,
children: []
};
availableClassifications.value.push(newClass);
window.Notify?.success(`"${newClassification.value}" agregada al sistema`);
newClassification.value = '';
};
const handleCreateNewSubclassification = (parentId) => {
if (!newSubclassification.value.trim()) {
window.Notify?.error('El nombre de la subclasificación es requerido');
return;
}
availableClassifications.value = availableClassifications.value.map(parent => {
if (parent.id === parentId) {
const newId = parent.children && parent.children.length > 0
? Math.max(...parent.children.map(c => c.id)) + 1
: parentId * 10 + 1;
const newChild = {
id: newId,
name: newSubclassification.value,
parent_id: parentId
};
return {
...parent,
children: [...(parent.children || []), newChild]
};
}
return parent;
});
window.Notify?.success(`"${newSubclassification.value}" agregada`);
newSubclassification.value = '';
selectedParentForNew.value = null;
};
const getClassificationName = (id) => {
for (const parent of availableClassifications.value) {
if (parent.id === id) return parent.name;
if (parent.children) {
const child = parent.children.find(c => c.id === id);
if (child) return `${parent.name} > ${child.name}`;
}
}
return "Desconocida";
};
/** Cargar clasificaciones de almacén */
async function loadClassifications() {
loadingClassifications.value = true;
try {
const warehouse = await productService.getWarehouseClassifications();
warehouseClassifications.value = warehouse || [];
} catch (error) {
console.error('Error cargando clasificaciones de almacén:', error);
window.Notify?.error('Error al cargar las clasificaciones de almacén');
} finally {
loadingClassifications.value = false;
}
}
/** Cargar atributos existentes al editar */
function loadExistingAttributes() {
console.log('Atributos cargados:', props.form.attributes);
}
const searcherComercial = useSearcher({
url: comercialTo('index'),
onSuccess: (r) => {
console.log('🔍 Respuesta completa (r):', r);
console.log('🔍 r.comercial_classifications:', r.comercial_classifications);
console.log('🔍 r.comercial_classifications.data:', r.comercial_classifications?.data);
// La respuesta viene en r.comercial_classifications.data
const classificationsData = r?.comercial_classifications?.data || [];
console.log('✅ classificationsData extraído:', classificationsData);
console.log('✅ Es array?:', Array.isArray(classificationsData));
console.log('✅ Length:', classificationsData.length);
comercialClassifications.value = classificationsData;
availableClassifications.value = classificationsData;
console.log('✅ availableClassifications.value asignado:', availableClassifications.value);
console.log('✅ Total de clasificaciones:', availableClassifications.value.length);
},
onError: (error) => {
console.error('❌ Error cargando clasificaciones:', error);
comercialClassifications.value = [];
availableClassifications.value = [];
}
});
/** Ciclos */
onMounted(() => {
loadClassifications();
searcherComercial.search(); // Cargar clasificaciones comerciales
if (props.action === 'update') {
loadExistingAttributes();
}
// Inicializar array de clasificaciones comerciales si no existe
if (!props.form.comercial_classification_ids) {
props.form.comercial_classification_ids = [];
}
});
</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="space-y-6">
<!-- Información básica -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<GoogleIcon name="info" class="text-blue-500 text-2xl mr-2" />
<h3 class="text-lg font-semibold">{{ $t('Información Básica') }}</h3>
</div>
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<Input v-model="form.code" id="code" :label="$t('codes')" :onError="form.errors.code" autofocus
required />
<Input v-model="form.sku" id="SKU" label="SKU" :onError="form.errors.sku" required />
<Input v-model="form.name" id="name" :label="$t('name')" :onError="form.errors.name" required />
<Input v-model="form.barcode" id="CÓDIGO DE BARRAS" :label="$t('CÓDIGO DE BARRAS')"
:onError="form.errors.barcode" required />
<div class="md:col-span-2 lg:col-span-3">
<Textarea v-model="form.description" id="description" :label="$t('description')"
:onError="form.errors.description" rows="3" />
</div>
<div class="flex items-center justify-between p-4 border rounded-lg">
<div>
<label for="">Estado del producto</label>
<p class="text-sm text-muted-foreground">
Activar o desactivar el producto en el catálogo
</p>
</div>
<Switch v-model:checked="form.is_active" />
</div>
</div>
</div>
<!-- Atributos dinámicos -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center mb-4">
<GoogleIcon name="tune" class="text-green-500 text-2xl mr-2" />
<h3 class="text-lg font-semibold">{{ $t('Atributos del Producto') }}</h3>
</div>
<div class="space-y-3 p-4 border rounded-lg bg-muted/20">
<div class="flex items-center justify-between">
<div>
<Label class="text-base">Atributos del Producto</Label>
<p class="text-sm text-muted-foreground">
Define atributos (ej: Color) y agrega múltiples valores (Negro, Azul, Rojo)
</p>
</div>
</div>
<div class="space-y-2">
<label class="text-sm font-medium">Agregar Atributo y Valores</label>
<div class="flex gap-2">
<div class="flex-1">
<Input v-model="newAttrKey"
placeholder="Nombre del atributo (ej: Color, Procesador, RAM)" />
</div>
<div class="flex-1 flex gap-2">
<Input v-model="newAttrValue"
placeholder="Valor (ej: Negro) - presiona Enter o + para agregar"
@keypress.enter.prevent="addAttributeValue" />
<Button color="info" variant="solid" @click="addAttributeValue">
<GoogleIcon name="add" class="mr-1" />
{{ $t('Agregar') }}
</Button>
</div>
</div>
<p v-if="newAttrKey" class="text-xs text-muted-foreground ml-1">
💡 Agregando valores al atributo: <span class="font-semibold">{{ newAttrKey }}</span>
</p>
</div>
<!-- Mostrar atributos configurados -->
<div v-if="form.attributes && Object.keys(form.attributes).length > 0"
class="space-y-3 mt-4 pt-4 border-t">
<label class="text-sm font-medium">Atributos Configurados</label>
<div v-for="(values, attrName) in form.attributes" :key="attrName"
class="space-y-2 p-3 border rounded-lg bg-background">
<div class="flex items-center justify-between">
<label class="text-sm font-semibold text-primary">{{ attrName }}</label>
<button type="button" @click="removeAttributeKey(attrName)"
class="px-2 py-1 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 rounded transition-colors flex items-center gap-1">
<GoogleIcon name="delete" class="text-sm" />
Eliminar atributo
</button>
</div>
<div class="flex flex-wrap gap-2">
<Badge v-for="value in values" :key="value" color="default"
class="flex items-center gap-1">
{{ value }}
<GoogleIcon name="close" class="text-xs cursor-pointer hover:text-red-600"
@click="removeAttributeValue(attrName, value)" />
</Badge>
</div>
</div>
</div>
<!-- Estado vacío -->
<div v-else class="text-center py-8 text-gray-500">
<GoogleIcon name="settings" class="text-4xl mb-2" />
<p class="text-sm">No hay atributos personalizados</p>
<p class="text-xs">Haz clic en "Agregar" para crear uno</p>
</div>
</div>
</div>
<!-- Clasificaciones -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="grid gap-4 grid-cols-1">
<!-- Clasificaciones Comerciales con jerarquía -->
<div class="space-y-4 p-4 border rounded-lg bg-muted/20">
<div>
<Label class="text-base">Clasificaciones Comerciales</Label>
<p class="text-sm text-muted-foreground">
Selecciona clasificaciones y subclasificaciones, o crea nuevas
</p>
</div>
<!-- Estado de carga -->
<div v-if="searcherComercial.loading" class="text-center py-8">
<GoogleIcon name="sync" class="text-4xl text-blue-500 animate-spin mb-2" />
<p class="text-sm text-muted-foreground">Cargando clasificaciones...</p>
</div>
<!-- Lista de clasificaciones disponibles con jerarquía -->
<div v-else-if="availableClassifications.length > 0" class="space-y-3">
<div v-for="parent in availableClassifications" :key="parent.id"
class="space-y-2 p-3 border rounded-lg bg-background">
<!-- Clasificación Principal -->
<div class="flex items-center justify-between">
<Button
type="button"
:color="form.comercial_classification_ids?.includes(parent.id) ? 'success' :'info'"
:variant="form.comercial_classification_ids?.includes(parent.id) ? 'solid' : 'outline'"
size="sm"
@click="form.comercial_classification_ids?.includes(parent.id)
? handleRemoveClassification(parent.id)
: handleAddClassification(parent.id)"
class="font-semibold"
>
{{ parent.name }}
</Button>
<Button
type="button"
variant="ghost"
size="sm"
@click="selectedParentForNew = selectedParentForNew === parent.id ? null : parent.id"
>
<GoogleIcon name="add" class="mr-1" />
Agregar subclasificación
</Button>
</div>
<!-- Subclasificaciones -->
<div v-if="parent.children && parent.children.length > 0"
class="flex flex-wrap gap-2 ml-4 pl-4 border-l-2">
<Button
v-for="child in parent.children"
:key="child.id"
type="button"
:color="form.comercial_classification_ids?.includes(child.id) ? 'success' :'warning'"
:variant="form.comercial_classification_ids?.includes(child.id) ? 'solid' : 'outline'"
size="sm"
@click="form.comercial_classification_ids?.includes(child.id)
? handleRemoveClassification(child.id)
: handleAddClassification(child.id)"
>
{{ child.name }}
<GoogleIcon v-if="form.comercial_classification_ids?.includes(child.id)"
name="close" class="ml-2 text-xs" />
</Button>
</div>
<!-- Formulario para agregar subclasificación -->
<div v-if="selectedParentForNew === parent.id" class="flex gap-2 ml-4 mt-2">
<Input
v-model="newSubclassification"
placeholder="Nombre de la subclasificación"
@keypress.enter.prevent="handleCreateNewSubclassification(parent.id)"
class="flex-1"
/>
<Button
type="button"
@click="handleCreateNewSubclassification(parent.id)"
size="sm"
color="primary"
>
<GoogleIcon name="add" />
</Button>
<Button
type="button"
@click="() => { selectedParentForNew = null; newSubclassification = ''; }"
size="sm"
variant="ghost"
>
<GoogleIcon name="close" />
</Button>
</div>
</div>
</div>
<!-- Estado vacío -->
<div v-else class="text-center py-8 text-muted-foreground">
<GoogleIcon name="category" class="text-4xl mb-2" />
<p class="text-sm">No hay clasificaciones disponibles</p>
<p class="text-xs mt-1">Crea una nueva clasificación para comenzar</p>
<p class="text-xs mt-2 text-red-500">Debug: {{ availableClassifications.length }} items</p>
</div>
<!-- Crear nueva clasificación principal -->
<div v-if="!searcherComercial.loading" class="flex gap-2 pt-2 border-t">
<Input
v-model="newClassification"
placeholder="Nueva clasificación principal"
@keypress.enter.prevent="handleCreateNewClassification"
/>
<Button
type="button"
@click="handleCreateNewClassification"
size="sm"
color="info"
>
<GoogleIcon name="add" class="mr-2" />
Crear
</Button>
</div>
<!-- Clasificaciones seleccionadas -->
<div v-if="form.comercial_classification_ids && form.comercial_classification_ids.length > 0" class="pt-3 border-t">
<Label class="text-sm">Seleccionadas ({{ form.comercial_classification_ids.length }}):</Label>
<div class="flex flex-wrap gap-2 mt-2">
<Badge
v-for="classId in form.comercial_classification_ids"
:key="classId"
color="primary"
class="flex items-center gap-1"
>
{{ getClassificationName(classId) }}
<GoogleIcon
name="close"
class="text-xs cursor-pointer hover:text-red-600"
@click="handleRemoveClassification(classId)"
/>
</Badge>
</div>
</div>
<!-- Mensaje cuando no hay seleccionadas -->
<div v-else class="text-center py-6 text-muted-foreground text-sm">
<GoogleIcon name="category" class="text-4xl mb-2" />
<p>No hay clasificaciones seleccionadas</p>
<p class="text-xs mt-1">Selecciona clasificaciones para organizar tu producto</p>
</div>
</div>
</div>
</div>
<!-- Slot para campos adicionales -->
<slot />
<!-- Botón de submit -->
<div class="flex justify-center">
<PrimaryButton v-text="$t(action)" :class="{ 'opacity-25': form.processing }"
:disabled="form.processing" class="px-8 py-3" />
</div>
</form>
</div>
</template>
<style scoped>
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>