862 lines
39 KiB
Vue

<script setup>
import { ref, watch, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useForm, useApi, apiURL } from '@Services/Api';
import Modal from '@Holos/Modal.vue';
import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
import CategoryCreateModal from '@Pages/POS/Category/CreateModal.vue';
import SubcategoryCreateModal from '@Pages/POS/Category/Subcategories/CreateModal.vue';
const router = useRouter();
/** Eventos */
const emit = defineEmits(['close', 'updated']);
/** Propiedades */
const props = defineProps({
show: Boolean,
product: Object
});
/** Estado */
const categories = ref([]);
const subcategories = ref([]);
const units = ref([]);
const activeTab = ref('general');
const showCategoryCreate = ref(false);
const showSubcategoryCreate = ref(false);
// Estado de equivalencias
const equivalences = ref([]);
const baseUnit = ref(null);
const loadingEquivalences = ref(false);
const showEquivalenceForm = ref(false);
const editingEquivalence = ref(null);
const api = useApi();
/** Formulario principal */
const form = useForm({
name: '',
key_sat: '',
sku: '',
barcode: '',
category_id: '',
subcategory_id: '',
unit_of_measure_id: null,
retail_price: 0,
tax: 16,
track_serials: false
});
/** Formulario de equivalencia */
const eqForm = useForm({
unit_of_measure_id: null,
conversion_factor: '',
retail_price: ''
});
/** Computed */
const selectedUnit = computed(() => {
if (!form.unit_of_measure_id) return null;
return units.value.find(u => u.id === form.unit_of_measure_id);
});
const canUseSerials = computed(() => {
if (!selectedUnit.value) return true;
if (selectedUnit.value.allows_decimals) return false;
return equivalences.value.length === 0; // No puede tener seriales si ya tiene equivalencias
});
const canHaveEquivalences = computed(() => {
return props.product && !props.product.track_serials;
});
// La unidad se bloquea si el producto ya tiene movimientos de inventario
const unitLocked = computed(() => {
if (!props.product?.id) return false;
return (
Number(props.product?.movements_count || 0) > 0 ||
Number(props.product?.stock || 0) > 0 ||
Number(props.product?.serials_count || 0) > 0
);
});
// Unidades disponibles para agregar equivalencia (excluir la base y las ya usadas)
const availableUnitsForEquivalence = computed(() => {
const usedIds = equivalences.value.map(e => e.unit_of_measure_id);
const editingId = editingEquivalence.value?.unit_of_measure_id;
return units.value.filter(u => {
if (u.id === form.unit_of_measure_id) return false; // excluir unidad base
if (usedIds.includes(u.id) && u.id !== editingId) return false; // excluir ya usadas (excepto la que se edita)
const name = (u.name || '').toLowerCase();
const abbr = (u.abbreviation || '').toLowerCase();
if (name.includes('serial') || abbr.includes('serial')) return false; // no aplica como equivalencia
return true;
});
});
/** Métodos */
const loadCategories = async () => {
try {
const response = await fetch(apiURL('categorias'), {
headers: {
'Authorization': `Bearer ${sessionStorage.token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.data && result.data.categories && result.data.categories.data) {
categories.value = result.data.categories.data;
// Cargar subcategorías si ya hay una categoría seleccionada (producto existente)
if (form.category_id) {
api.get(apiURL(`categorias/${form.category_id}/subcategorias`), {
onSuccess: (data) => {
subcategories.value = data.subcategories?.data || data.subcategories || [];
}
});
}
}
} catch (error) {
console.error('Error loading categories:', error);
}
};
const onCategoryChange = () => {
form.subcategory_id = '';
subcategories.value = [];
if (!form.category_id) return;
api.get(apiURL(`categorias/${form.category_id}/subcategorias`), {
onSuccess: (data) => {
subcategories.value = data.subcategories?.data || data.subcategories || [];
}
});
};
const loadUnits = async () => {
try {
const response = await fetch(apiURL('unidades-medida/active'), {
headers: {
'Authorization': `Bearer ${sessionStorage.token}`,
'Accept': 'application/json'
}
});
const result = await response.json();
if (result.data && result.data.units) {
units.value = result.data.units;
}
} catch (error) {
console.error('Error loading units:', error);
}
};
const loadEquivalences = () => {
if (!props.product?.id) return;
loadingEquivalences.value = true;
api.get(apiURL(`inventario/${props.product.id}/equivalencias`), {
onSuccess: (data) => {
equivalences.value = data.equivalences || [];
baseUnit.value = data.base_unit || null;
},
onFail: () => {
equivalences.value = [];
},
onFinish: () => {
loadingEquivalences.value = false;
}
});
};
const validateSerialsAndUnit = () => {
if (!selectedUnit.value) return;
if (selectedUnit.value.allows_decimals) {
if (form.track_serials) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
}
form.track_serials = false;
return;
}
const unitName = (selectedUnit.value.name || '').toLowerCase();
const unitAbbr = (selectedUnit.value.abbreviation || '').toLowerCase();
if (unitName.includes('serial') || unitAbbr.includes('serial')) {
form.track_serials = true;
}
};
const updateProduct = () => {
const hasSerials = Number(props.product?.serials_count || 0) > 0;
form.transform((data) => ({
...data,
track_serials: selectedUnit.value && !selectedUnit.value.allows_decimals
? (hasSerials || !!data.track_serials)
: false,
category_id: data.category_id || null,
subcategory_id: data.category_id ? (data.subcategory_id || null) : null,
})).put(apiURL(`inventario/${props.product.id}`), {
onSuccess: () => {
Notify.success('Producto actualizado exitosamente');
emit('updated');
closeModal();
},
onError: () => {
Notify.error('Error al actualizar el producto');
}
});
};
const closeModal = () => {
form.reset();
activeTab.value = 'general';
showEquivalenceForm.value = false;
editingEquivalence.value = null;
equivalences.value = [];
baseUnit.value = null;
emit('close');
};
const onCategoryCreated = async (newCategory) => {
await loadCategories();
if (newCategory) {
form.category_id = newCategory.id;
onCategoryChange();
}
};
const onSubcategoryCreated = (newSubcategory) => {
if (newSubcategory) {
subcategories.value.push(newSubcategory);
form.subcategory_id = newSubcategory.id;
}
};
const openSerials = () => {
if (selectedUnit.value && selectedUnit.value.allows_decimals) {
Notify.warning('No se pueden usar números de serie con esta unidad de medida.');
return;
}
form.track_serials = true;
closeModal();
router.push({ name: 'pos.inventory.serials', params: { id: props.product.id } });
};
// Equivalencias
const openAddEquivalence = () => {
editingEquivalence.value = null;
eqForm.unit_of_measure_id = null;
eqForm.conversion_factor = '';
eqForm.retail_price = '';
showEquivalenceForm.value = true;
};
const openEditEquivalence = (eq) => {
editingEquivalence.value = eq;
eqForm.unit_of_measure_id = eq.unit_of_measure_id;
eqForm.conversion_factor = parseFloat(eq.conversion_factor);
eqForm.retail_price = eq.retail_price ? parseFloat(eq.retail_price) : '';
showEquivalenceForm.value = true;
};
const cancelEquivalenceForm = () => {
showEquivalenceForm.value = false;
editingEquivalence.value = null;
eqForm.reset();
};
const saveEquivalence = () => {
const productId = props.product.id;
if (editingEquivalence.value) {
const eqId = editingEquivalence.value.id;
eqForm.transform((data) => ({
conversion_factor: data.conversion_factor,
retail_price: data.retail_price || undefined,
})).put(apiURL(`inventario/${productId}/equivalencias/${eqId}`), {
onSuccess: () => {
Notify.success('Equivalencia actualizada');
cancelEquivalenceForm();
loadEquivalences();
},
onError: () => {
Notify.error('Error al actualizar la equivalencia');
}
});
} else {
eqForm.transform((data) => ({
unit_of_measure_id: data.unit_of_measure_id,
conversion_factor: data.conversion_factor,
retail_price: data.retail_price || undefined,
})).post(apiURL(`inventario/${productId}/equivalencias`), {
onSuccess: () => {
Notify.success('Equivalencia creada');
cancelEquivalenceForm();
loadEquivalences();
},
onError: () => {
Notify.error('Error al crear la equivalencia');
}
});
}
};
const deleteEquivalence = (eq) => {
if (!confirm(`¿Eliminar la equivalencia con "${eq.unit_name}"?`)) return;
api.delete(apiURL(`inventario/${props.product.id}/equivalencias/${eq.id}`), {
onSuccess: () => {
Notify.success('Equivalencia eliminada');
loadEquivalences();
},
onFail: (data) => {
Notify.error(data.message || 'Error al eliminar la equivalencia');
},
onError: () => {
Notify.error('Error de conexión');
}
});
};
/** Observadores */
watch(() => props.product, (newProduct) => {
if (newProduct) {
form.name = newProduct.name || '';
form.key_sat = newProduct.key_sat || '';
form.sku = newProduct.sku || '';
form.barcode = newProduct.barcode || '';
form.category_id = newProduct.category_id || '';
form.subcategory_id = newProduct.subcategory_id || '';
form.unit_of_measure_id = newProduct.unit_of_measure_id || null;
form.cost = parseFloat(newProduct.price?.cost || 0);
form.retail_price = parseFloat(newProduct.price?.retail_price || 0);
form.tax = parseFloat(newProduct.price?.tax || 16);
const serialCount = Number(newProduct.serials_count || 0);
form.track_serials = !!newProduct.track_serials || serialCount > 0;
}
}, { immediate: true });
watch(() => props.show, async (newValue) => {
if (newValue) {
loadUnits();
activeTab.value = 'general';
await loadCategories();
}
});
watch(selectedUnit, () => {
validateSerialsAndUnit();
});
watch(activeTab, (tab) => {
if (tab === 'equivalences' && props.product?.id) {
loadEquivalences();
}
});
</script>
<template>
<Modal :show="show" max-width="md" @close="closeModal">
<div class="p-6">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
Editar Producto
</h3>
<button
@click="closeModal"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Tabs -->
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-5">
<button
@click="activeTab = 'general'"
:class="[
'px-4 py-2 text-sm font-medium border-b-2 transition-colors',
activeTab === 'general'
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
Información General
</button>
<button
v-if="canHaveEquivalences"
@click="activeTab = 'equivalences'"
:class="[
'px-4 py-2 text-sm font-medium border-b-2 transition-colors flex items-center gap-1.5',
activeTab === 'equivalences'
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
]"
>
Equivalencias
<span
v-if="equivalences.length > 0"
class="inline-flex items-center justify-center w-4 h-4 text-xs font-bold rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300"
>
{{ equivalences.length }}
</span>
</button>
<div
v-else
class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-300 dark:text-gray-600 cursor-not-allowed"
title="No disponible para productos con rastreo de seriales"
>
Equivalencias
</div>
</div>
<!-- Tab: Información General -->
<div v-if="activeTab === 'general'">
<form @submit.prevent="updateProduct" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<!-- Nombre -->
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
NOMBRE
</label>
<FormInput
v-model="form.name"
type="text"
placeholder="Nombre del producto"
required
/>
<FormError :message="form.errors?.name" />
</div>
<!-- Clave SAT -->
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
CLAVE SAT
</label>
<FormInput
v-model="form.key_sat"
type="string"
placeholder="Clave SAT del producto"
/>
<FormError :message="form.errors?.key_sat" />
</div>
<!-- SKU -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
SKU
</label>
<FormInput
v-model="form.sku"
type="text"
placeholder="SKU"
required
/>
<FormError :message="form.errors?.sku" />
</div>
<!-- Código de Barras -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
CÓDIGO DE BARRAS
</label>
<FormInput
v-model="form.barcode"
type="text"
placeholder="1234567890123"
maxlength="100"
/>
<FormError :message="form.errors?.barcode" />
</div>
<!-- Categoría -->
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
CLASIFICACIÓN
</label>
<div class="flex gap-2">
<select
v-model="form.category_id"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
@change="onCategoryChange"
>
<option value="">Seleccionar clasificación</option>
<option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</option>
</select>
<button
type="button"
@click="showCategoryCreate = true"
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0"
title="Crear nueva clasificación"
>
<GoogleIcon name="add" class="text-xl" />
</button>
</div>
<FormError :message="form.errors?.category_id" />
</div>
<!-- Subclasificación -->
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
SUBCLASIFICACIÓN
</label>
<div class="flex gap-2">
<select
v-model="form.subcategory_id"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!form.category_id || !subcategories.length"
:required="!!form.category_id"
>
<option value="">
{{ !form.category_id ? 'Selecciona una clasificación primero' : subcategories.length ? 'Seleccionar subclasificación...' : 'Sin subclasificaciones disponibles' }}
</option>
<option
v-for="subcategory in subcategories"
:key="subcategory.id"
:value="subcategory.id"
>
{{ subcategory.name }}
</option>
</select>
<button
type="button"
@click="showSubcategoryCreate = true"
:disabled="!form.category_id"
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
title="Crear nueva subclasificación"
>
<GoogleIcon name="add" class="text-xl" />
</button>
</div>
<FormError :message="form.errors?.subcategory_id" />
</div>
<!-- Unidad de Medida -->
<div class="col-span-2">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
UNIDAD DE MEDIDA *
</label>
<select
v-model="form.unit_of_measure_id"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="unitLocked"
required
>
<option :value="null">Seleccionar unidad</option>
<option
v-for="unit in units"
:key="unit.id"
:value="unit.id"
>
{{ unit.name }} ({{ unit.abbreviation }})
</option>
</select>
<FormError :message="form.errors?.unit_of_measure_id" />
<p v-if="unitLocked" class="mt-1 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
<GoogleIcon name="lock" class="text-xs" />
No se puede cambiar: el producto ya tiene movimientos de inventario
</p>
<p v-else-if="selectedUnit" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<span v-if="selectedUnit.allows_decimals">Esta unidad permite cantidades decimales (ej: 25.750)</span>
<span v-else>Esta unidad solo permite cantidades enteras</span>
</p>
</div>
<!-- Precio de Venta -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
PRECIO VENTA
</label>
<FormInput
v-model.number="form.retail_price"
type="number"
min="0"
step="0.01"
placeholder="0.00"
required
/>
<FormError :message="form.errors?.retail_price" />
</div>
<!-- Impuesto/Tax -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
IMPUESTO (%)
</label>
<FormInput
v-model.number="form.tax"
type="number"
min="0"
max="100"
step="0.01"
placeholder="16.00"
required
/>
<FormError :message="form.errors?.tax" />
</div>
</div>
<!-- Botones -->
<div class="flex items-center justify-between mt-6">
<button
v-if="canUseSerials"
type="button"
@click="openSerials"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-lg hover:bg-emerald-100 transition-colors"
>
<GoogleIcon name="qr_code_2" class="text-lg" />
Gestionar Seriales
</button>
<div
v-else
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-400 bg-gray-50 border border-gray-200 rounded-lg cursor-not-allowed"
:title="equivalences.length > 0 ? 'No disponible: el producto tiene equivalencias de unidad' : selectedUnit?.allows_decimals ? `No disponible: ${selectedUnit.name} permite decimales` : 'Selecciona una unidad de medida'"
>
<GoogleIcon name="qr_code_2" class="text-lg opacity-50" />
Gestionar Seriales
</div>
<div class="flex items-center gap-3">
<button
type="button"
@click="closeModal"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
>
Cancelar
</button>
<button
type="submit"
:disabled="form.processing"
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<span v-if="form.processing">Actualizando...</span>
<span v-else>Actualizar</span>
</button>
</div>
</div>
</form>
</div>
<!-- Tab: Equivalencias -->
<div v-else-if="activeTab === 'equivalences'">
<!-- Unidad base -->
<div v-if="baseUnit" class="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg flex items-center gap-2">
<GoogleIcon name="info" class="text-gray-400 text-sm" />
<p class="text-sm text-gray-600 dark:text-gray-400">
Unidad base:
<span class="font-semibold text-gray-900 dark:text-gray-100">
{{ baseUnit.name }} ({{ baseUnit.abbreviation }})
</span>
</p>
</div>
<!-- Loading -->
<div v-if="loadingEquivalences" class="flex justify-center py-8">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-indigo-600"></div>
</div>
<div v-else>
<!-- Lista de equivalencias -->
<div v-if="equivalences.length > 0" class="space-y-2 mb-4">
<div
v-for="eq in equivalences"
:key="eq.id"
class="flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded-lg"
:class="{ 'opacity-50': !eq.is_active }"
>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ eq.unit_name }}
<span class="font-mono text-gray-500 dark:text-gray-400 text-xs">({{ eq.unit_abbreviation }})</span>
</p>
<span
v-if="!eq.is_active"
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
Inactivo
</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1 {{ eq.unit_name }} = {{ parseFloat(eq.conversion_factor) }} {{ baseUnit?.abbreviation }}
&nbsp;·&nbsp;
Precio: ${{ parseFloat(eq.retail_price).toFixed(2) }}
</p>
</div>
<div class="flex items-center gap-1 ml-2">
<button
@click="openEditEquivalence(eq)"
class="p-1.5 text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-900 rounded-lg transition-colors"
title="Editar equivalencia"
>
<GoogleIcon name="edit" class="text-base" />
</button>
<button
@click="deleteEquivalence(eq)"
class="p-1.5 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900 rounded-lg transition-colors"
title="Eliminar equivalencia"
>
<GoogleIcon name="delete" class="text-base" />
</button>
</div>
</div>
</div>
<div
v-else-if="!showEquivalenceForm"
class="flex flex-col items-center justify-center py-6 text-gray-400"
>
<GoogleIcon name="straighten" class="text-4xl mb-2 opacity-50" />
<p class="text-sm">Este producto no tiene equivalencias</p>
</div>
<!-- Formulario inline de equivalencia -->
<div v-if="showEquivalenceForm" class="border border-indigo-200 dark:border-indigo-800 rounded-lg p-4 bg-indigo-50 dark:bg-indigo-950 space-y-3">
<h4 class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">
{{ editingEquivalence ? 'Editar equivalencia' : 'Nueva equivalencia' }}
</h4>
<!-- Unidad (solo en creación) -->
<div v-if="!editingEquivalence">
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
UNIDAD
</label>
<select
v-model="eqForm.unit_of_measure_id"
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
required
>
<option :value="null">Seleccionar unidad</option>
<option
v-for="unit in availableUnitsForEquivalence"
:key="unit.id"
:value="unit.id"
>
{{ unit.name }} ({{ unit.abbreviation }})
</option>
</select>
<FormError :message="eqForm.errors?.unit_of_measure_id" />
</div>
<!-- Si estamos editando, mostrar la unidad como texto -->
<div v-else>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
UNIDAD
</label>
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ editingEquivalence.unit_name }} ({{ editingEquivalence.unit_abbreviation }})
</p>
</div>
<div class="grid grid-cols-2 gap-3">
<!-- Factor de conversión -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
FACTOR DE CONVERSIÓN
</label>
<FormInput
v-model.number="eqForm.conversion_factor"
type="number"
min="0"
step="any"
placeholder="Ej: 24"
required
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
1 unidad = X {{ baseUnit?.abbreviation || 'base' }}
</p>
<FormError :message="eqForm.errors?.conversion_factor" />
</div>
<!-- Precio sugerido -->
<div>
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
PRECIO SUGERIDO
</label>
<FormInput
v-model.number="eqForm.retail_price"
type="number"
min="0"
step="0.01"
placeholder="Automático"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Dejar vacío para calcular automáticamente
</p>
<FormError :message="eqForm.errors?.retail_price" />
</div>
</div>
<div class="flex items-center justify-end gap-2 pt-1">
<button
type="button"
@click="cancelEquivalenceForm"
class="px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
type="button"
@click="saveEquivalence"
:disabled="eqForm.processing"
class="px-3 py-1.5 text-xs font-semibold text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-colors"
>
<span v-if="eqForm.processing">Guardando...</span>
<span v-else>{{ editingEquivalence ? 'Actualizar' : 'Agregar' }}</span>
</button>
</div>
</div>
<!-- Botón agregar equivalencia -->
<button
v-if="!showEquivalenceForm"
type="button"
@click="openAddEquivalence"
class="mt-3 w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-indigo-700 bg-indigo-50 border border-dashed border-indigo-300 rounded-lg hover:bg-indigo-100 dark:bg-indigo-900 dark:text-indigo-300 dark:border-indigo-700 dark:hover:bg-indigo-800 transition-colors"
>
<GoogleIcon name="add" class="text-lg" />
Agregar equivalencia
</button>
</div>
<!-- Botón cerrar en tab equivalencias -->
<div class="flex justify-end mt-6">
<button
type="button"
@click="closeModal"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Cerrar
</button>
</div>
</div>
</div>
</Modal>
<CategoryCreateModal
:show="showCategoryCreate"
@close="showCategoryCreate = false"
@created="onCategoryCreated"
/>
<SubcategoryCreateModal
:show="showSubcategoryCreate"
:category-id="form.category_id"
@close="showSubcategoryCreate = false"
@created="onSubcategoryCreated"
/>
</template>