- Added methods to normalize permissions and roles from API responses. - Implemented a centralized error handling method for authentication errors. - Updated API endpoints for login, registration, and user profile management. - Introduced session refresh functionality to retrieve user roles and permissions. feat(catalog): improve companies and units management with permissions and filters - Integrated permission checks for creating, updating, and deleting companies. - Added user role and permission checks to the Companies component. - Enhanced the Units component with search and status filters. - Refactored unit creation and update logic to handle validation errors. fix(catalog): update unit measure services and mapping logic - Improved API service methods for fetching, creating, and updating units of measure. - Added mapping functions to convert API responses to internal data structures. - Enhanced error handling in unit measure services. chore(auth): refactor authentication storage utilities - Created utility functions for managing authentication tokens and user data in local storage. - Updated API interceptor to use new storage utility functions for session management. style: clean up code formatting and improve readability across components and services
316 lines
9.2 KiB
Vue
316 lines
9.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
import Dialog from 'primevue/dialog';
|
|
import Button from 'primevue/button';
|
|
import InputText from 'primevue/inputtext';
|
|
import Dropdown from 'primevue/dropdown';
|
|
import InputSwitch from 'primevue/inputswitch';
|
|
import { satUnitsService } from '../../services/sat-units.services';
|
|
import type { UnitOfMeasure, CreateUnitOfMeasureData, SatUnit } from '../../types/unit-measure.interfaces';
|
|
|
|
interface Props {
|
|
visible: boolean;
|
|
unit?: UnitOfMeasure | null;
|
|
isEditing?: boolean;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:visible', value: boolean): void;
|
|
(e: 'save', data: CreateUnitOfMeasureData): void;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
visible: false,
|
|
unit: null,
|
|
isEditing: false
|
|
});
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Form data
|
|
const formData = ref<CreateUnitOfMeasureData>({
|
|
name: '',
|
|
abbreviation: '',
|
|
code_sat: null,
|
|
is_active: true
|
|
});
|
|
|
|
// Estado interno del switch (boolean para el UI)
|
|
const isActiveSwitch = ref(true);
|
|
const loading = ref(false);
|
|
const errors = ref<Record<string, string>>({});
|
|
|
|
// SAT Units
|
|
const satUnits = ref<SatUnit[]>([]);
|
|
const loadingSatUnits = ref(false);
|
|
const searchQuery = ref('');
|
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
// Options para el dropdown
|
|
const satUnitOptions = computed(() =>
|
|
satUnits.value.map(unit => ({
|
|
label: `${unit.code} - ${unit.name}`,
|
|
value: unit.id
|
|
}))
|
|
);
|
|
|
|
// Computed
|
|
const dialogTitle = computed(() =>
|
|
props.isEditing ? 'Editar Unidad de Medida' : 'Nueva Unidad de Medida'
|
|
);
|
|
|
|
const isFormValid = computed(() => {
|
|
return !!formData.value.name?.trim();
|
|
});
|
|
|
|
const emptyMessage = computed(() => {
|
|
if (props.isEditing && satUnits.value.length === 1) {
|
|
return 'Escribe para buscar otra unidad SAT...';
|
|
}
|
|
return 'No se encontraron unidades. Escribe para buscar.';
|
|
});
|
|
|
|
// Load SAT Units
|
|
const loadSatUnits = async (search: string) => {
|
|
// Solo buscar si hay texto
|
|
if (!search || search.trim().length === 0) {
|
|
// Si estamos editando y hay una unidad actual, mantenerla
|
|
if (props.unit?.sat_unit) {
|
|
satUnits.value = [props.unit.sat_unit];
|
|
} else {
|
|
satUnits.value = [];
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loadingSatUnits.value = true;
|
|
const response = await satUnitsService.getSatUnits(search);
|
|
satUnits.value = response.data;
|
|
} catch (error) {
|
|
console.error('Error loading SAT units:', error);
|
|
// Si hay error y estamos editando, mantener la unidad actual
|
|
if (props.unit?.sat_unit) {
|
|
satUnits.value = [props.unit.sat_unit];
|
|
} else {
|
|
satUnits.value = [];
|
|
}
|
|
} finally {
|
|
loadingSatUnits.value = false;
|
|
}
|
|
};
|
|
|
|
// Debounced search
|
|
const handleSearchChange = (event: { value?: string }) => {
|
|
const query = event.value || '';
|
|
searchQuery.value = query;
|
|
|
|
// Limpiar timeout anterior
|
|
if (searchTimeout) {
|
|
clearTimeout(searchTimeout);
|
|
}
|
|
|
|
// Aplicar debounce de 500ms
|
|
searchTimeout = setTimeout(() => {
|
|
loadSatUnits(query);
|
|
}, 500);
|
|
};
|
|
|
|
// Methods
|
|
const resetForm = () => {
|
|
formData.value = {
|
|
name: '',
|
|
abbreviation: '',
|
|
code_sat: null,
|
|
is_active: true
|
|
};
|
|
isActiveSwitch.value = true;
|
|
errors.value = {};
|
|
loading.value = false;
|
|
};
|
|
|
|
// Watch para actualizar el formulario cuando cambie la unidad
|
|
watch(() => props.unit, (newUnit) => {
|
|
if (newUnit) {
|
|
formData.value = {
|
|
name: newUnit.name,
|
|
abbreviation: newUnit.abbreviation,
|
|
code_sat: newUnit.code_sat,
|
|
is_active: newUnit.is_active
|
|
};
|
|
isActiveSwitch.value = newUnit.is_active;
|
|
|
|
// Precargar la unidad SAT actual en el dropdown
|
|
if (newUnit.sat_unit) {
|
|
satUnits.value = [newUnit.sat_unit];
|
|
}
|
|
} else {
|
|
resetForm();
|
|
satUnits.value = []; // Limpiar el dropdown
|
|
}
|
|
}, { immediate: true });
|
|
|
|
const handleClose = () => {
|
|
emit('update:visible', false);
|
|
satUnits.value = []; // Limpiar las opciones del dropdown
|
|
resetForm();
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
errors.value = {};
|
|
|
|
if (!formData.value.name?.trim()) {
|
|
errors.value.name = 'El nombre es obligatorio';
|
|
} else if (formData.value.name.trim().length > 50) {
|
|
errors.value.name = 'El nombre no puede exceder 50 caracteres';
|
|
}
|
|
|
|
if (formData.value.abbreviation && formData.value.abbreviation.trim().length > 10) {
|
|
errors.value.abbreviation = 'La abreviatura no puede exceder 10 caracteres';
|
|
}
|
|
|
|
return Object.keys(errors.value).length === 0;
|
|
};
|
|
|
|
const handleSave = () => {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
formData.value.is_active = isActiveSwitch.value;
|
|
|
|
emit('save', {
|
|
...formData.value,
|
|
name: formData.value.name.trim(),
|
|
abbreviation: formData.value.abbreviation?.trim() || undefined,
|
|
});
|
|
};
|
|
|
|
const setValidationErrors = (backendErrors: Record<string, string[]>) => {
|
|
loading.value = false;
|
|
errors.value = {};
|
|
|
|
Object.keys(backendErrors).forEach((key) => {
|
|
const firstError = backendErrors[key]?.[0];
|
|
if (firstError) {
|
|
errors.value[key] = firstError;
|
|
}
|
|
});
|
|
};
|
|
|
|
const resetSubmitting = () => {
|
|
loading.value = false;
|
|
};
|
|
|
|
defineExpose({
|
|
setValidationErrors,
|
|
resetSubmitting,
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog
|
|
:visible="visible"
|
|
@update:visible="emit('update:visible', $event)"
|
|
:header="dialogTitle"
|
|
:modal="true"
|
|
:closable="true"
|
|
:draggable="false"
|
|
class="w-full max-w-md"
|
|
>
|
|
<div class="space-y-4 pt-4">
|
|
<!-- Name -->
|
|
<div>
|
|
<label for="name" class="block text-sm font-medium mb-2">
|
|
Nombre <span class="text-red-500">*</span>
|
|
</label>
|
|
<InputText
|
|
id="name"
|
|
v-model="formData.name"
|
|
class="w-full"
|
|
placeholder="Ej: PIEZA, KILOGRAMO, METRO"
|
|
:required="true"
|
|
:class="{ 'p-invalid': errors.name }"
|
|
/>
|
|
<small v-if="errors.name" class="p-error">{{ errors.name }}</small>
|
|
</div>
|
|
|
|
<!-- Abbreviation -->
|
|
<div>
|
|
<label for="abbreviation" class="block text-sm font-medium mb-2">
|
|
Abreviatura <span class="text-red-500">*</span>
|
|
</label>
|
|
<InputText
|
|
id="abbreviation"
|
|
v-model="formData.abbreviation"
|
|
class="w-full"
|
|
placeholder="Ej: PZA, kg, m"
|
|
:class="{ 'p-invalid': errors.abbreviation }"
|
|
/>
|
|
<small v-if="errors.abbreviation" class="p-error">{{ errors.abbreviation }}</small>
|
|
</div>
|
|
|
|
<!-- Code SAT -->
|
|
<div>
|
|
<label for="code_sat" class="block text-sm font-medium mb-2">
|
|
Unidad SAT <span class="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
id="code_sat"
|
|
v-model="formData.code_sat"
|
|
:options="satUnitOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
:loading="loadingSatUnits"
|
|
placeholder="Escribe para buscar una unidad SAT..."
|
|
class="w-full"
|
|
:filter="true"
|
|
filterPlaceholder="Buscar unidad SAT"
|
|
:showClear="true"
|
|
@filter="handleSearchChange"
|
|
:emptyFilterMessage="emptyMessage"
|
|
:class="{ 'p-invalid': errors.code_sat }"
|
|
/>
|
|
<small v-if="errors.code_sat" class="p-error">{{ errors.code_sat }}</small>
|
|
<small class="text-surface-500 dark:text-surface-400">
|
|
Unidad del catálogo SAT
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div>
|
|
<label for="is_active" class="block text-sm font-medium mb-2">
|
|
Estado
|
|
</label>
|
|
<div class="flex items-center gap-3">
|
|
<InputSwitch
|
|
id="is_active"
|
|
v-model="isActiveSwitch"
|
|
/>
|
|
<span class="text-sm font-medium">
|
|
{{ isActiveSwitch ? 'Activa' : 'Inactiva' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<Button
|
|
label="Cancelar"
|
|
severity="secondary"
|
|
outlined
|
|
@click="handleClose"
|
|
/>
|
|
<Button
|
|
:label="isEditing ? 'Actualizar' : 'Crear'"
|
|
:disabled="!isFormValid"
|
|
:loading="loading"
|
|
@click="handleSave"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|