feat: Enhance Fixed Asset Class form with blur-based lookups and US fraction masking, and refine pedimento creation flow and button labels.

This commit is contained in:
2026-03-10 08:48:11 -05:00
parent 5441b05114
commit 8f1fa86ae3
4 changed files with 107 additions and 27 deletions

View File

@@ -109,6 +109,10 @@
formData.import_tariff_type = snap.import_tariff_type ?? '';
formData.export_tariff_code = snap.export_tariff_code ?? '';
formData.export_tariff_type = snap.export_tariff_type ?? '';
// Reiniciar banderas de error al cargar datos
showErrors = false;
validationErrors = {};
} else {
// Reset form when initialData is null (new class)
formData.class_code = '';
@@ -128,12 +132,17 @@
formData.import_tariff_type = '';
formData.export_tariff_code = '';
formData.export_tariff_type = '';
// Reiniciar banderas de error al limpiar formulario
showErrors = false;
validationErrors = {};
}
});
// Estado de validación
let validationErrors = $state<Record<string, string>>({});
let showErrors = $state(false);
let isSubmitting = $state(false);
// Estado de diálogos
let showMaterialDialog = $state(false);
@@ -262,6 +271,37 @@
searchMaterial = '';
}
// Fix 1: Buscar descripción de Tipo de Activo Fijo al perder el foco
async function handleMaterialBlur() {
validateField('material_key');
const code = formData.material_key?.trim().toUpperCase();
if (!code) {
formData.material_description = '';
return;
}
// Primero buscar en la caché local (si ya se cargó el catálogo)
if (materialTypes.length > 0) {
const found = materialTypes.find((m) => m.key.toUpperCase() === code);
if (found) {
formData.material_key = found.key;
formData.material_description = found.description;
return;
}
}
// Si no está en caché, consultar el API
try {
const response = await materialTypesApi.get(code);
if (response.data) {
formData.material_key = response.data.key;
formData.material_description = response.data.description;
} else {
formData.material_description = '(Código no encontrado)';
}
} catch {
formData.material_description = '(Código no encontrado)';
}
}
async function openUnitOfMeasureSearch() {
showUnitDialog = true;
searchUnit = '';
@@ -276,6 +316,39 @@
searchUnit = '';
}
// Fix 1: Buscar descripción de U.M. Comercial al perder el foco
async function handleUnitBlur() {
validateField('unit_of_measure');
const code = formData.unit_of_measure?.trim().toUpperCase();
if (!code) {
formData.unit_of_measure_description = '';
return;
}
// Primero buscar en la caché local (si ya se cargó el catálogo)
await loadUnitsOfMeasure();
const found = unitsOfMeasureData.find((u) => u.code.toUpperCase() === code);
if (found) {
formData.unit_of_measure = found.code;
formData.unit_of_measure_description = found.description;
formData.unit_measure_key = found.claveMexicana;
} else {
formData.unit_of_measure_description = '(Código no encontrado)';
}
}
// Fix 2: Máscara para Fracción Americana (formato 0000.00.00.00 = 13 chars)
function formatUSFraction(event: Event) {
const input = event.target as HTMLInputElement;
// Extraer solo dígitos
const digits = input.value.replace(/\D/g, '').slice(0, 10);
// Construir la máscara insertando puntos en las posiciones correctas
let masked = digits;
if (digits.length > 4) masked = digits.slice(0, 4) + '.' + digits.slice(4);
if (digits.length > 6) masked = digits.slice(0, 4) + '.' + digits.slice(4, 6) + '.' + digits.slice(6);
if (digits.length > 8) masked = digits.slice(0, 4) + '.' + digits.slice(4, 6) + '.' + digits.slice(6, 8) + '.' + digits.slice(8);
formData.us_fraction = masked;
}
async function openUSFractionSearch() {
showUSFractionDialog = true;
searchUSFraction = '';
@@ -451,7 +524,8 @@
// Validar campo individual (para validación en blur)
function validateField(fieldName: string) {
if (!showErrors) return; // Solo validar si ya se intentó guardar
// Bloquear validación repetida si: a) no se ha intentado guardar o b) está guardando
if (!showErrors || isSubmitting) return;
const errors = { ...validationErrors };
@@ -496,22 +570,28 @@
validationErrors = errors;
}
function handleSave() {
// Activar visualización de errores
showErrors = true;
async function handleSave() {
// Validar formulario
if (!validateForm()) {
showErrors = true;
toast.error('Por favor, complete todos los campos obligatorios');
return;
}
// Activar bandera the submitting para que no se validen inputs al azar en el blur
isSubmitting = true;
// Tomamos una copia muerta de los datos actuales
const dataToSave = $state.snapshot(formData);
// Ejecutamos el onSave pasándole la copia
if (onSave) {
onSave(dataToSave);
try {
// Ejecutamos el onSave pasándole la copia. Await en caso de que devuelva promesa.
if (onSave) {
await onSave(dataToSave);
}
} finally {
// Liberar el estado de subida solo después de que acabe todo el flujo
isSubmitting = false;
}
}
@@ -522,9 +602,17 @@
}
// Escuchar el evento de guardado del padre
import { onDestroy } from 'svelte';
if (typeof document !== 'undefined') {
document.addEventListener('save-form', handleSave);
}
onDestroy(() => {
if (typeof document !== 'undefined') {
document.removeEventListener('save-form', handleSave);
}
});
</script>
<div class="space-y-3">
@@ -569,7 +657,7 @@
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={10}
onblur={() => validateField('material_key')}
onblur={handleMaterialBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openMaterialSearch}>
<Folder class="h-4 w-4" />
@@ -629,7 +717,7 @@
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={5}
onblur={() => validateField('unit_of_measure')}
onblur={handleUnitBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openUnitOfMeasureSearch}>
<Folder class="h-4 w-4" />
@@ -676,9 +764,10 @@
<Input
id="us_fraction"
bind:value={formData.us_fraction}
placeholder="Fracción americana"
class="flex-1"
maxlength={16}
placeholder="0000.00.00.00"
class="flex-1 font-mono tracking-wider"
maxlength={13}
oninput={formatUSFraction}
/>
<Button type="button" variant="outline" size="icon" onclick={openUSFractionSearch}>
<Folder class="h-4 w-4" />

View File

@@ -1016,7 +1016,7 @@
</Button>
<Button size="sm" onclick={handleAdd} type="button">
<Plus class="mr-1 h-4 w-4" />
Agregar Item
Agregar Partidas
</Button>
</div>
</div>

View File

@@ -532,10 +532,6 @@
<div class="mx-auto max-w-[1400px] px-4 py-4">
<!-- Botones de acción -->
<div class="flex justify-end gap-2">
<Button size="sm" onclick={handleCreateClick}>
<Plus class="mr-1 h-4 w-4" />
Insertar
</Button>
<Button variant="outline" size="sm" onclick={handleEditSelected} disabled={!hasSelection}>
<Edit size={16} class="mr-1" />
Editar

View File

@@ -975,10 +975,6 @@
}
if (!response.data?.id) throw new Error('No se recibió el ID del pedimento creado');
newPedimentoId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/pedimentos/edit/${newPedimentoId}`);
return;
} else {
// Actualizar pedimento existente con todos sus sub-recursos
const response = await pedimentosApi.update(
@@ -988,15 +984,11 @@
);
if (response.error) throw new Error(response.error);
// Recargar los datos del pedimento desde el servidor
// Forzar recarga de datos del router (útil si hay navegación con SvelteKit)
try {
await invalidateAll();
// Forzar recarga de datos esperando un tick
await new Promise((resolve) => setTimeout(resolve, 100));
} catch (invalidateError) {
console.error('❌ Error en invalidateAll:', invalidateError);
// No lanzar el error, solo loguearlo
// El pedimento ya se guardó exitosamente en el backend
}
}
@@ -1005,6 +997,9 @@
? 'Pedimento creado exitosamente'
: 'Todos los cambios se guardaron correctamente'
);
// Redirigir siempre de vuelta al listado principal una vez guardado correctamente
await goto('/dashboard/pedimentos');
} catch (e) {
if (e instanceof Error) {
if (e.message.includes('401')) {