feat: add unit equivalence management functionality
- Implemented UnitsEquivalenceModal for managing unit equivalences. - Added unit equivalence store and service for CRUD operations. - Integrated unit equivalence management into Units.vue component. - Created new HTML page for unit equivalence management layout. - Defined TypeScript interfaces for unit equivalences. - Enhanced user permissions for managing unit equivalences.
This commit is contained in:
parent
47cc7cdb8e
commit
1cf8cc3aa5
@ -38,7 +38,18 @@ const menuItems = ref<MenuItem[]>([
|
||||
],
|
||||
},
|
||||
{ label: 'Clasificaciones Comerciales', icon: 'pi pi-tags', to: '/catalog/classifications-comercial' },
|
||||
{ label: 'Proveedores', icon: 'pi pi-briefcase', to: '/catalog/suppliers' },
|
||||
{
|
||||
label: 'Proveedores',
|
||||
icon: 'pi pi-briefcase',
|
||||
to: '/catalog/suppliers',
|
||||
permission: [
|
||||
'suppliers.index',
|
||||
'suppliers.show',
|
||||
'suppliers.store',
|
||||
'suppliers.update',
|
||||
'suppliers.destroy',
|
||||
],
|
||||
},
|
||||
{ label: 'Documentos del Modelo', icon: 'pi pi-file', to: '/catalog/model-documents' },
|
||||
{
|
||||
label: 'Empresas',
|
||||
|
||||
@ -166,6 +166,8 @@ class AuthService {
|
||||
async getUserPermissions(): Promise<string[]> {
|
||||
try {
|
||||
const response = await api.get('/api/user/permissions');
|
||||
console.log('User permissions response:', response);
|
||||
|
||||
return this.normalizePermissions(response.data);
|
||||
} catch (error: any) {
|
||||
this.resolveAuthError(error, 'Error al obtener permisos del usuario');
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import Card from 'primevue/card';
|
||||
import Button from 'primevue/button';
|
||||
@ -16,9 +15,10 @@ import { useToast } from 'primevue/usetoast';
|
||||
|
||||
|
||||
import { supplierServices } from '../../services/supplier.services';
|
||||
import type { Supplier, SupplierPaginatedResponse, SupplierFormErrors } from '../../types/suppliers.interfaces';
|
||||
import type { Supplier, SupplierFormErrors, SupplierPaginatedResponse } from '../../types/suppliers.interfaces';
|
||||
import { SupplierType } from '../../types/suppliers.interfaces';
|
||||
import SupplierModal from './SupplierModal.vue';
|
||||
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||
|
||||
|
||||
const suppliers = ref<Supplier[]>([]);
|
||||
@ -40,7 +40,26 @@ const formErrors = ref<SupplierFormErrors>({});
|
||||
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const { hasPermission } = useAuth();
|
||||
|
||||
const canViewSuppliers = computed(() =>
|
||||
hasPermission([
|
||||
'suppliers.index',
|
||||
'suppliers.show',
|
||||
'suppliers.store',
|
||||
'suppliers.update',
|
||||
'suppliers.destroy',
|
||||
])
|
||||
);
|
||||
const canCreateSupplier = computed(() => hasPermission('suppliers.store'));
|
||||
const canUpdateSupplier = computed(() => hasPermission('suppliers.update'));
|
||||
const canDeleteSupplier = computed(() => hasPermission('suppliers.destroy'));
|
||||
|
||||
const handleDelete = (supplierId: number) => {
|
||||
if (!canDeleteSupplier.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes eliminar proveedores.', life: 4000 });
|
||||
return;
|
||||
}
|
||||
confirm.require({
|
||||
message: '¿Seguro que deseas eliminar este proveedor?',
|
||||
header: 'Confirmar eliminación',
|
||||
@ -71,11 +90,21 @@ const mapTypeToApi = (typeLabel: string | null) => {
|
||||
};
|
||||
|
||||
const fetchSuppliers = async (page = 1) => {
|
||||
if (!canViewSuppliers.value) {
|
||||
suppliers.value = [];
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
page: 1,
|
||||
total: 0,
|
||||
first: 0,
|
||||
};
|
||||
return;
|
||||
}
|
||||
pagination.value.page = page;
|
||||
loading.value = true;
|
||||
try {
|
||||
const name = searchName.value ? searchName.value : undefined;
|
||||
const type = selectedType.value ? mapTypeToApi(selectedType.value)?.toString() : undefined;
|
||||
console.log('🔎 fetchSuppliers params:', { paginated: true, name, type, page });
|
||||
const response = await supplierServices.getSuppliers(true, name, type);
|
||||
const paginated = response as SupplierPaginatedResponse;
|
||||
suppliers.value = paginated.data;
|
||||
@ -85,15 +114,23 @@ const fetchSuppliers = async (page = 1) => {
|
||||
pagination.value.first = (paginated.current_page - 1) * paginated.per_page;
|
||||
pagination.value.rows = paginated.per_page;
|
||||
} catch (e) {
|
||||
// Manejo de error opcional
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los proveedores.', life: 3000 });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
() => canViewSuppliers.value,
|
||||
(allowed) => {
|
||||
if (allowed) {
|
||||
fetchSuppliers();
|
||||
});
|
||||
} else {
|
||||
suppliers.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const supplierTypes = [
|
||||
{ label: 'Todos', value: null },
|
||||
@ -124,6 +161,10 @@ const onPageChange = (event: any) => {
|
||||
|
||||
|
||||
const openCreateModal = () => {
|
||||
if (!canCreateSupplier.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes crear proveedores.', life: 4000 });
|
||||
return;
|
||||
}
|
||||
isEditMode.value = false;
|
||||
showModal.value = true;
|
||||
currentSupplier.value = null;
|
||||
@ -131,6 +172,10 @@ const openCreateModal = () => {
|
||||
};
|
||||
|
||||
const openEditModal = async (supplier: Supplier) => {
|
||||
if (!canUpdateSupplier.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Sin permisos', detail: 'No puedes actualizar proveedores.', life: 4000 });
|
||||
return;
|
||||
}
|
||||
isEditMode.value = true;
|
||||
formErrors.value = {};
|
||||
loadingSupplier.value = true;
|
||||
@ -164,9 +209,15 @@ const handleModalSubmit = async (form: any) => {
|
||||
formErrors.value = {};
|
||||
try {
|
||||
if (isEditMode.value && currentSupplier.value) {
|
||||
if (!canUpdateSupplier.value) {
|
||||
throw new Error('without-update-permission');
|
||||
}
|
||||
await supplierServices.updateSupplier(currentSupplier.value.id, form, 'patch');
|
||||
toast.add({ severity: 'success', summary: 'Proveedor actualizado', detail: 'Proveedor actualizado correctamente', life: 3000 });
|
||||
} else {
|
||||
if (!canCreateSupplier.value) {
|
||||
throw new Error('without-create-permission');
|
||||
}
|
||||
await supplierServices.createSupplier(form);
|
||||
toast.add({ severity: 'success', summary: 'Proveedor creado', detail: 'Proveedor registrado correctamente', life: 3000 });
|
||||
}
|
||||
@ -176,7 +227,18 @@ const handleModalSubmit = async (form: any) => {
|
||||
if (e?.response?.data?.errors) {
|
||||
formErrors.value = e.response.data.errors;
|
||||
}
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: e?.response?.data?.message || 'No se pudo guardar el proveedor', life: 3000 });
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail:
|
||||
e?.response?.data?.message ||
|
||||
(e?.message === 'without-create-permission'
|
||||
? 'No tienes permisos para crear proveedores.'
|
||||
: e?.message === 'without-update-permission'
|
||||
? 'No tienes permisos para actualizar proveedores.'
|
||||
: 'No se pudo guardar el proveedor'),
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -211,14 +273,19 @@ const typeSeverity = (type: number) => {
|
||||
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Administra la base de datos de proveedores y
|
||||
sus contactos.</p>
|
||||
</div>
|
||||
<Button label="Nuevo Proveedor" icon="pi pi-plus" @click="openCreateModal" />
|
||||
<Button
|
||||
v-if="canCreateSupplier"
|
||||
label="Nuevo Proveedor"
|
||||
icon="pi pi-plus"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
<SupplierModal :visible="showModal" :isEditMode="isEditMode" :supplier="currentSupplier"
|
||||
:formErrors="formErrors" :loading="loadingSupplier" @update:visible="val => { if (!val) closeModal(); }"
|
||||
@submit="handleModalSubmit" @cancel="closeModal" />
|
||||
</div>
|
||||
|
||||
<!-- Filters Toolbar -->
|
||||
<Card>
|
||||
<Card v-if="canViewSuppliers">
|
||||
<template #content>
|
||||
<div class="flex flex-wrap gap-4 items-end">
|
||||
<div class="flex-1 min-w-60">
|
||||
@ -239,10 +306,10 @@ const typeSeverity = (type: number) => {
|
||||
</Card>
|
||||
|
||||
<!-- Table Content -->
|
||||
<Card>
|
||||
<Card v-if="canViewSuppliers">
|
||||
<template #content>
|
||||
<DataTable :value="suppliers" :loading="loading" stripedRows responsiveLayout="scroll"
|
||||
class="p-datatable-sm"><
|
||||
class="p-datatable-sm">
|
||||
<Column field="name" header="Razón Social" style="min-width: 180px">
|
||||
<template #body="{ data }">
|
||||
<div>
|
||||
@ -285,9 +352,23 @@ const typeSeverity = (type: number) => {
|
||||
style="min-width: 120px">
|
||||
<template #body="{ data }">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button icon="pi pi-pencil" text rounded size="small" @click="openEditModal(data)" />
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger"
|
||||
@click="handleDelete(data.id)" />
|
||||
<Button
|
||||
v-if="canUpdateSupplier"
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
@click="openEditModal(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="canDeleteSupplier"
|
||||
icon="pi pi-trash"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
@click="handleDelete(data.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
@ -298,6 +379,14 @@ const typeSeverity = (type: number) => {
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<Card v-else>
|
||||
<template #content>
|
||||
<div class="text-center py-10 text-surface-500 dark:text-surface-400">
|
||||
<i class="pi pi-lock text-4xl mb-3"></i>
|
||||
<p>No tienes permisos para visualizar este módulo.</p>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
<ConfirmDialog />
|
||||
|
||||
@ -14,10 +14,12 @@ import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import UnitsEquivalenceModal from './UnitsEquivalenceModal.vue';
|
||||
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
|
||||
import UnitsForm from './UnitsForm.vue';
|
||||
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
|
||||
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||
import { useUnitEquivalenceStore } from '../../stores/unitEquivalenceStore';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
@ -41,6 +43,8 @@ const showDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const selectedUnit = ref<UnitOfMeasure | null>(null);
|
||||
const unitsFormRef = ref<InstanceType<typeof UnitsForm> | null>(null);
|
||||
const showEquivalenceModal = ref(false);
|
||||
const equivalenceUnit = ref<{ id?: number; name?: string } | undefined>(undefined);
|
||||
const search = ref('');
|
||||
const statusFilter = ref<'all' | 'active' | 'inactive'>('all');
|
||||
|
||||
@ -71,6 +75,16 @@ const canViewUnits = computed(() =>
|
||||
const canCreateUnit = computed(() => hasPermission('units-of-measure.store'));
|
||||
const canUpdateUnit = computed(() => hasPermission('units-of-measure.update'));
|
||||
const canDeleteUnit = computed(() => hasPermission('units-of-measure.destroy'));
|
||||
const equivalenceStore = useUnitEquivalenceStore();
|
||||
const canManageEquivalences = computed(() =>
|
||||
hasPermission([
|
||||
'unit-equivalences.index',
|
||||
'unit-equivalences.show',
|
||||
'unit-equivalences.store',
|
||||
'unit-equivalences.update',
|
||||
'unit-equivalences.destroy',
|
||||
])
|
||||
);
|
||||
|
||||
// Methods
|
||||
const buildFilters = () => ({
|
||||
@ -122,6 +136,14 @@ const openEditDialog = (unit: UnitOfMeasure) => {
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
const openEquivalenceModal = (unit: UnitOfMeasure | null) => {
|
||||
// For now only show the modal with static maquetado
|
||||
equivalenceUnit.value = unit ? { id: unit.id, name: unit.name } : undefined;
|
||||
showEquivalenceModal.value = true;
|
||||
// fetch equivalences for the selected unit (non-blocking, safe if endpoint missing)
|
||||
equivalenceStore.fetchEquivalences({ paginate: false, is_active: true, from_unit_id: unit?.id }).catch(()=>{});
|
||||
};
|
||||
|
||||
const handleCreateUnit = async (data: CreateUnitOfMeasureData) => {
|
||||
await unitStore.createUnit(data);
|
||||
toast.add({
|
||||
@ -435,6 +457,16 @@ watch(
|
||||
@click="confirmDelete(slotProps.data)"
|
||||
v-tooltip.top="'Eliminar'"
|
||||
/>
|
||||
<!-- Equivalence button -->
|
||||
<Button
|
||||
v-if="canManageEquivalences"
|
||||
icon="pi pi-exchange"
|
||||
text
|
||||
rounded
|
||||
severity="help"
|
||||
@click="openEquivalenceModal(slotProps.data)"
|
||||
v-tooltip.top="'Gestionar equivalencias'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
@ -465,5 +497,8 @@ watch(
|
||||
:is-editing="isEditing"
|
||||
@save="handleSaveUnit"
|
||||
/>
|
||||
|
||||
<!-- Equivalences Modal (maquetado, no funcional) -->
|
||||
<UnitsEquivalenceModal v-model="showEquivalenceModal" :unit="equivalenceUnit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
300
src/modules/catalog/components/units/UnitsEquivalenceModal.vue
Normal file
300
src/modules/catalog/components/units/UnitsEquivalenceModal.vue
Normal file
@ -0,0 +1,300 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
|
||||
import { useUnitEquivalenceStore } from '../../stores/unitEquivalenceStore';
|
||||
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore' ;
|
||||
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const props = defineProps<{ modelValue: boolean; unit?: { id?: number; name?: string } }>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v),
|
||||
});
|
||||
|
||||
const unitName = computed(() => props.unit?.name || 'Unidad seleccionada');
|
||||
|
||||
const equivalenceStore = useUnitEquivalenceStore();
|
||||
const unitStore = useUnitOfMeasureStore();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { hasPermission } = useAuth();
|
||||
|
||||
const loading = computed(() => equivalenceStore.loading);
|
||||
|
||||
// Form state for create/edit
|
||||
const isEditing = ref(false);
|
||||
const editingId = ref<number | null>(null);
|
||||
const form = ref<{
|
||||
from_unit_id: number | null;
|
||||
to_unit_id: number | null;
|
||||
factor: number;
|
||||
}>({
|
||||
from_unit_id: props.unit?.id || null,
|
||||
to_unit_id: null,
|
||||
factor: 0,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.unit,
|
||||
async (u) => {
|
||||
form.value.from_unit_id = u?.id || null;
|
||||
// Refresca lista de equivalencias
|
||||
if (visible.value) {
|
||||
equivalenceStore.fetchEquivalences({ paginate: false, to_unit_id: u?.id }).catch(() => {});
|
||||
// Si aún no hay unidades cargadas, las carga
|
||||
if (!unitStore.units.length) {
|
||||
await unitStore.fetchUnits({ paginate: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Si el modal se abre y no hay unidades, hace fetch
|
||||
watch(
|
||||
() => visible.value,
|
||||
async (val) => {
|
||||
if (val && !unitStore.units.length) {
|
||||
await unitStore.fetchUnits({ paginate: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const canCreate = computed(() => hasPermission('unit-equivalences.store'));
|
||||
const canUpdate = computed(() => hasPermission('unit-equivalences.update'));
|
||||
const canDelete = computed(() => hasPermission('unit-equivalences.destroy'));
|
||||
|
||||
function resetForm() {
|
||||
isEditing.value = false;
|
||||
editingId.value = null;
|
||||
form.value = {
|
||||
from_unit_id: props.unit?.id || null,
|
||||
to_unit_id: null,
|
||||
factor: 0,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const errors: string[] = [];
|
||||
if (!form.value.from_unit_id) errors.push('Unidad base es requerida.');
|
||||
if (!form.value.to_unit_id) errors.push('Unidad destino es requerida.');
|
||||
if (form.value.from_unit_id && form.value.to_unit_id && form.value.from_unit_id === form.value.to_unit_id) errors.push('La unidad base y destino deben ser diferentes.');
|
||||
if (!(Number(form.value.factor) > 0)) errors.push('El factor debe ser mayor a 0.');
|
||||
return errors;
|
||||
};
|
||||
|
||||
const editEquivalence = (item: any) => {
|
||||
isEditing.value = true;
|
||||
editingId.value = item.id;
|
||||
form.value.from_unit_id = item.from_unit_id;
|
||||
form.value.to_unit_id = item.to_unit_id;
|
||||
form.value.factor = item.factor;
|
||||
|
||||
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const errors = validateForm();
|
||||
if (errors.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Validación', detail: errors.join(' '), life: 5000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing.value && editingId.value && canUpdate.value) {
|
||||
const payload = {
|
||||
from_unit_id: form.value.from_unit_id!,
|
||||
to_unit_id: form.value.to_unit_id!,
|
||||
factor: form.value.factor,
|
||||
};
|
||||
await equivalenceStore.updateEquivalence(editingId.value, payload);
|
||||
toast.add({ severity: 'success', summary: 'Actualizado', detail: 'Equivalencia actualizada correctamente.' });
|
||||
} else if (!isEditing.value && canCreate.value) {
|
||||
const payload = {
|
||||
from_unit_id: form.value.from_unit_id!,
|
||||
to_unit_id: form.value.to_unit_id!,
|
||||
factor: form.value.factor,
|
||||
|
||||
|
||||
|
||||
};
|
||||
await equivalenceStore.createEquivalence(payload);
|
||||
toast.add({ severity: 'success', summary: 'Creado', detail: 'Equivalencia creada correctamente.' });
|
||||
} else {
|
||||
toast.add({ severity: 'warn', summary: 'Permisos', detail: 'No tienes permisos para realizar esta acción.' });
|
||||
}
|
||||
// refresh list and reset
|
||||
await equivalenceStore.fetchEquivalences({ paginate: false, to_unit_id: form.value.from_unit_id });
|
||||
resetForm();
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || 'Error al guardar equivalencia';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: msg });
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (id: number) => {
|
||||
if (!canDelete.value) {
|
||||
toast.add({ severity: 'warn', summary: 'Permisos', detail: 'No tienes permisos para eliminar.' });
|
||||
return;
|
||||
}
|
||||
|
||||
confirm.require({
|
||||
message: '¿Estás seguro de eliminar esta equivalencia?',
|
||||
header: 'Confirmar eliminación',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
accept: async () => {
|
||||
try {
|
||||
await equivalenceStore.deleteEquivalence(id);
|
||||
toast.add({ severity: 'success', summary: 'Eliminado', detail: 'Equivalencia eliminada correctamente.' });
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || 'Error al eliminar equivalencia';
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: msg });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="visible" :modal="true" :style="{ width: '900px' }" :closable="true">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold">Gestión de Equivalencias</h3>
|
||||
<p class="text-sm text-surface-600">Unidad: {{ unitName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Maquetado estático: tabla de equivalencias y formulario lateral adaptado del HTML original -->
|
||||
<div class="p-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="lg:col-span-2 bg-surface-container-lowest rounded-lg overflow-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-surface-container-high">
|
||||
<th class="px-4 py-3 text-xs uppercase font-bold text-on-surface-variant">Unidad Origen</th>
|
||||
<th class="px-4 py-3 text-xs uppercase font-bold text-on-surface-variant text-center">Factor</th>
|
||||
<th class="px-4 py-3 text-xs uppercase font-bold text-on-surface-variant">Unidad Destino</th>
|
||||
<th class="px-4 py-3 text-xs uppercase font-bold text-on-surface-variant text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
<tr v-if="loading" class="text-center">
|
||||
<td colspan="4" class="py-6">
|
||||
<i class="pi pi-spin pi-spinner" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="equivalenceStore.equivalences.length === 0" class="text-center">
|
||||
<td colspan="4" class="py-6">No hay equivalencias registradas para esta unidad.</td>
|
||||
</tr>
|
||||
<tr v-else v-for="item in equivalenceStore.equivalences" :key="item.id" class="hover:bg-surface-container-low transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 bg-surface-container-low flex items-center justify-center rounded text-primary">
|
||||
<i class="pi pi-box" />
|
||||
</div>
|
||||
<span class="font-semibold">{{ item.from_unit?.name || item.from_unit_id }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-mono">{{ item.factor }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="bg-secondary-container/30 text-secondary px-2 py-1 rounded text-xs font-bold">{{ item.to_unit?.name || item.to_unit_id }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button v-if="canUpdate" icon="pi pi-pencil" text rounded @click="editEquivalence(item)" />
|
||||
<Button v-if="canDelete" icon="pi pi-trash" text rounded severity="danger" @click="remove(item.id)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="px-4 py-3 bg-surface-container-low flex items-center justify-between">
|
||||
<span class="text-xs text-on-surface-variant">Mostrando {{ equivalenceStore.equivalences.length }} entradas</span>
|
||||
<div class="flex gap-1">
|
||||
<!-- pagination could go here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lateral: Formulario estático -->
|
||||
<div class="lg:col-span-1 space-y-4">
|
||||
<div class="bg-surface-container-lowest p-4 rounded-lg border border-outline-variant">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<i class="pi pi-plus text-primary"></i>
|
||||
<h4 class="text-sm font-bold uppercase">Agregar Nueva Equivalencia</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs font-bold text-on-surface-variant uppercase">Unidad Base</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full border-b border-outline-variant py-2 text-sm bg-gray-100 cursor-not-allowed"
|
||||
:value="unitName"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs font-bold text-on-surface-variant uppercase">Unidad Destino</label>
|
||||
<div class="mt-1">
|
||||
<select v-model="form.to_unit_id" class="w-full border-b border-outline-variant py-2 text-sm">
|
||||
<option :value="null">Seleccionar unidad...</option>
|
||||
<option v-for="unit in unitStore.units" :key="unit.id" :value="unit.id">
|
||||
{{ unit.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs font-bold text-on-surface-variant uppercase">Factor</label>
|
||||
<div class="mt-1">
|
||||
<input v-model.number="form.factor" type="number" min="0.0001" step="0.0001" class="w-full border-b border-outline-variant py-2 text-sm" placeholder="1.00" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<Button label="Cancelar" text @click="visible = false" />
|
||||
<Button v-if="canCreate || canUpdate" label="Guardar" class="p-button-success" @click="save" />
|
||||
<Button v-else label="Guardar" disabled />
|
||||
</div>
|
||||
|
||||
<div class="pt-2 text-xs text-on-surface-variant">
|
||||
Solo es necesario definir la unidad de destino y el factor respecto a la unidad base.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-secondary-container to-primary-container p-4 rounded-lg text-on-secondary-container">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs font-bold uppercase opacity-80">Info Rápida</span>
|
||||
<i class="pi pi-info-circle"></i>
|
||||
</div>
|
||||
<p class="text-sm">Asegúrese de que las unidades base correspondan a las medidas de inventario físico.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Scoped minimal adjustments so the maquetado se vea coherente dentro del Dialog */
|
||||
.bg-surface-container-lowest { background-color: var(--surface-container-lowest, #fff); }
|
||||
.bg-surface-container-high { background-color: var(--surface-container-high, #f3f3fe); }
|
||||
.text-secondary { color: var(--secondary, #495c95); }
|
||||
</style>
|
||||
449
src/modules/catalog/components/units/unidades-equvalencia.html
Normal file
449
src/modules/catalog/components/units/unidades-equvalencia.html
Normal file
@ -0,0 +1,449 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet" />
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"outline-variant": "#c3c6d7",
|
||||
"tertiary-fixed": "#ffdbcd",
|
||||
"on-secondary-fixed": "#00174b",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-primary": "#ffffff",
|
||||
"error-container": "#ffdad6",
|
||||
"on-error": "#ffffff",
|
||||
"inverse-on-surface": "#f0f0fb",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-secondary-container": "#394c84",
|
||||
"surface-container": "#ededf9",
|
||||
"tertiary-container": "#bc4800",
|
||||
"on-tertiary-fixed-variant": "#7d2d00",
|
||||
"on-surface": "#191b23",
|
||||
"on-tertiary-fixed": "#360f00",
|
||||
"outline": "#737686",
|
||||
"secondary-fixed-dim": "#b4c5ff",
|
||||
"surface-tint": "#0053db",
|
||||
"surface-container-low": "#f3f3fe",
|
||||
"background": "#faf8ff",
|
||||
"inverse-primary": "#b4c5ff",
|
||||
"on-primary-fixed": "#00174b",
|
||||
"on-tertiary-container": "#ffede6",
|
||||
"tertiary": "#943700",
|
||||
"primary-fixed": "#dbe1ff",
|
||||
"surface-container-high": "#e7e7f3",
|
||||
"on-surface-variant": "#434655",
|
||||
"on-secondary-fixed-variant": "#31447b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-dim": "#d9d9e5",
|
||||
"inverse-surface": "#2e3039",
|
||||
"surface-container-highest": "#e1e2ed",
|
||||
"primary": "#004ac6",
|
||||
"secondary-container": "#acbfff",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-background": "#191b23",
|
||||
"primary-container": "#2563eb",
|
||||
"error": "#ba1a1a",
|
||||
"secondary": "#495c95",
|
||||
"secondary-fixed": "#dbe1ff",
|
||||
"surface": "#faf8ff",
|
||||
"on-primary-container": "#eeefff",
|
||||
"primary-fixed-dim": "#b4c5ff",
|
||||
"on-primary-fixed-variant": "#003ea8",
|
||||
"tertiary-fixed-dim": "#ffb596",
|
||||
"surface-variant": "#e1e2ed"
|
||||
},
|
||||
fontFamily: {
|
||||
"headline": ["Inter"],
|
||||
"body": ["Inter"],
|
||||
"label": ["Inter"]
|
||||
},
|
||||
borderRadius: { "DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem" },
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(250, 248, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-background text-on-surface min-h-screen flex">
|
||||
<!-- SideNavBar Shell -->
|
||||
<aside class="h-screen w-64 fixed left-0 top-0 z-50 bg-[#ffffff] dark:bg-slate-900 flex flex-col p-4 gap-2">
|
||||
<div class="mb-8 px-2">
|
||||
<h1 class="text-lg font-bold text-[#191b23] dark:text-white">The Precise Curator</h1>
|
||||
<p class="text-xs text-slate-500 font-medium uppercase tracking-wider">Management Suite</p>
|
||||
</div>
|
||||
<nav class="flex-grow space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 bg-[#f3f3fe] text-[#004ac6] font-semibold rounded-lg text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="inventory_2">inventory_2</span>
|
||||
<span>Warehouse</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="inventory">inventory</span>
|
||||
<span>Products</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="group">group</span>
|
||||
<span>HR</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto pt-4 border-t border-surface-container space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="contact_support">contact_support</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-slate-500 hover:text-[#004ac6] hover:bg-[#ededf9] rounded-lg transition-all font-medium text-sm"
|
||||
href="#">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 flex-grow flex flex-col min-h-screen">
|
||||
<!-- TopAppBar Shell -->
|
||||
<header class="w-full h-16 sticky top-0 z-40 bg-[#faf8ff] flex justify-between items-center px-8">
|
||||
<div class="flex items-center gap-8">
|
||||
<span class="text-xl font-bold text-[#004ac6]">Architectural ERP</span>
|
||||
<div class="hidden md:flex gap-6 items-center">
|
||||
<a class="text-slate-600 font-medium hover:text-[#004ac6] transition-colors text-sm"
|
||||
href="#">Inventario</a>
|
||||
<a class="text-[#004ac6] font-bold border-b-2 border-[#004ac6] text-sm py-5"
|
||||
href="#">Configuración</a>
|
||||
<a class="text-slate-600 font-medium hover:text-[#004ac6] transition-colors text-sm"
|
||||
href="#">Reportes</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="p-2 text-slate-600 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="p-2 text-slate-600 hover:bg-slate-100 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined" data-icon="help_outline">help_outline</span>
|
||||
</button>
|
||||
<div class="h-8 w-8 rounded-full overflow-hidden bg-primary-container flex items-center justify-center">
|
||||
<img alt="User Profile Avatar"
|
||||
data-alt="professional headshot of a middle-aged architect in a minimalist workspace with soft natural lighting"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBWBuLM7PNHJEV0FZ_f5K3-vmg--FVfXa5_kQAbfarkJeDejtpvXC6TJQTC2geDqYJDl8GYz3QSAA1HpQr3fYwelTpFgvnSQTUPRXSgK-H38In03m51WsW1PP00_4cdEErO6dgAGN_BVReJr3-L6eKpYOiA8BZmDKerxWs4YYgffqz9bmIVybVi1bin083ZBZy0LgQGaDVCru_hs46OonyQHM4qWbyYgmVUTRcCaevGZ68ag6vo2eCXt5fwFknLVpplpZd-ccf0L26n" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Canvas -->
|
||||
<div class="p-8 space-y-8 max-w-7xl mx-auto w-full">
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-3xl font-bold text-on-surface tracking-tight">Gestión de Equivalencias</h2>
|
||||
<p class="text-on-surface-variant text-sm">Configura las relaciones entre diferentes unidades de
|
||||
medida para el control de stock.</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-gradient-to-br from-primary to-primary-container text-white px-5 py-2.5 rounded-md flex items-center gap-2 font-semibold shadow-sm hover:opacity-90 transition-all active:scale-95">
|
||||
<span class="material-symbols-outlined text-lg" data-icon="add">add</span>
|
||||
Nueva Equivalencia
|
||||
</button>
|
||||
</div>
|
||||
<!-- Dashboard Layout (Bento Style) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
<!-- Filters & Search Bento Box -->
|
||||
<div class="lg:col-span-12 bg-surface-container-lowest p-6 rounded-xl space-y-6">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-grow relative">
|
||||
<span
|
||||
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline"
|
||||
data-icon="search">search</span>
|
||||
<input
|
||||
class="w-full pl-10 pr-4 py-2 bg-surface-container-low border-b border-outline-variant focus:border-primary focus:ring-0 transition-all text-sm outline-none"
|
||||
placeholder="Buscar unidades..." type="text" />
|
||||
</div>
|
||||
<div class="flex gap-2 items-center overflow-x-auto pb-2 md:pb-0">
|
||||
<button
|
||||
class="bg-secondary-container text-on-secondary-container px-4 py-1.5 rounded-full text-xs font-bold whitespace-nowrap">Todas</button>
|
||||
<button
|
||||
class="bg-surface-container-low text-on-surface-variant hover:bg-surface-container-high px-4 py-1.5 rounded-full text-xs font-semibold transition-colors whitespace-nowrap">Peso</button>
|
||||
<button
|
||||
class="bg-surface-container-low text-on-surface-variant hover:bg-surface-container-high px-4 py-1.5 rounded-full text-xs font-semibold transition-colors whitespace-nowrap">Volumen</button>
|
||||
<button
|
||||
class="bg-surface-container-low text-on-surface-variant hover:bg-surface-container-high px-4 py-1.5 rounded-full text-xs font-semibold transition-colors whitespace-nowrap">Longitud</button>
|
||||
<button
|
||||
class="bg-surface-container-low text-on-surface-variant hover:bg-surface-container-high px-4 py-1.5 rounded-full text-xs font-semibold transition-colors whitespace-nowrap">Unidades</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Table Section -->
|
||||
<div class="lg:col-span-8 bg-surface-container-lowest rounded-xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-surface-container-high">
|
||||
<th
|
||||
class="px-6 py-4 text-[10px] uppercase font-bold text-on-surface-variant tracking-widest">
|
||||
Unidad Origen</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[10px] uppercase font-bold text-on-surface-variant tracking-widest text-center">
|
||||
Operador</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[10px] uppercase font-bold text-on-surface-variant tracking-widest text-center">
|
||||
Factor</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[10px] uppercase font-bold text-on-surface-variant tracking-widest">
|
||||
Unidad Destino</th>
|
||||
<th
|
||||
class="px-6 py-4 text-[10px] uppercase font-bold text-on-surface-variant tracking-widest text-right">
|
||||
Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y-2 divide-surface">
|
||||
<tr
|
||||
class="bg-surface-container-lowest hover:bg-surface-container-low transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-8 w-8 bg-surface-container-low flex items-center justify-center rounded text-primary">
|
||||
<span class="material-symbols-outlined text-sm"
|
||||
data-icon="inventory_2">inventory_2</span>
|
||||
</div>
|
||||
<span class="font-semibold text-sm">Caja (Master)</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center font-bold text-primary">x</td>
|
||||
<td class="px-6 py-4 text-center font-mono text-sm">12.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="bg-secondary-container/30 text-secondary px-2 py-1 rounded text-xs font-bold">Pieza</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div
|
||||
class="flex justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
class="p-2 text-primary hover:bg-primary-fixed rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="edit">edit</span></button>
|
||||
<button
|
||||
class="p-2 text-error hover:bg-error-container rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="delete">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-surface-container-low hover:bg-surface-container-high transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-8 w-8 bg-surface-container-lowest flex items-center justify-center rounded text-primary">
|
||||
<span class="material-symbols-outlined text-sm"
|
||||
data-icon="scale">scale</span>
|
||||
</div>
|
||||
<span class="font-semibold text-sm">Kilogramo</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center font-bold text-primary">/</td>
|
||||
<td class="px-6 py-4 text-center font-mono text-sm">1000.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="bg-secondary-container/30 text-secondary px-2 py-1 rounded text-xs font-bold">Gramo</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div
|
||||
class="flex justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
class="p-2 text-primary hover:bg-primary-fixed rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="edit">edit</span></button>
|
||||
<button
|
||||
class="p-2 text-error hover:bg-error-container rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="delete">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="bg-surface-container-lowest hover:bg-surface-container-low transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-8 w-8 bg-surface-container-low flex items-center justify-center rounded text-primary">
|
||||
<span class="material-symbols-outlined text-sm"
|
||||
data-icon="widgets">widgets</span>
|
||||
</div>
|
||||
<span class="font-semibold text-sm">Pallet</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center font-bold text-primary">x</td>
|
||||
<td class="px-6 py-4 text-center font-mono text-sm">48.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="bg-secondary-container/30 text-secondary px-2 py-1 rounded text-xs font-bold">Caja</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div
|
||||
class="flex justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
class="p-2 text-primary hover:bg-primary-fixed rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="edit">edit</span></button>
|
||||
<button
|
||||
class="p-2 text-error hover:bg-error-container rounded-lg transition-colors"><span
|
||||
class="material-symbols-outlined text-lg"
|
||||
data-icon="delete">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 bg-surface-container-low flex items-center justify-between">
|
||||
<span class="text-xs text-on-surface-variant">Mostrando 1 a 3 de 24 entradas</span>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="p-1 text-on-surface-variant hover:bg-surface-container-highest rounded transition-colors"><span
|
||||
class="material-symbols-outlined text-sm"
|
||||
data-icon="chevron_left">chevron_left</span></button>
|
||||
<button class="px-2.5 py-1 text-xs font-bold bg-primary text-white rounded">1</button>
|
||||
<button
|
||||
class="px-2.5 py-1 text-xs font-semibold text-on-surface-variant hover:bg-surface-container-highest rounded transition-colors">2</button>
|
||||
<button
|
||||
class="px-2.5 py-1 text-xs font-semibold text-on-surface-variant hover:bg-surface-container-highest rounded transition-colors">3</button>
|
||||
<button
|
||||
class="p-1 text-on-surface-variant hover:bg-surface-container-highest rounded transition-colors"><span
|
||||
class="material-symbols-outlined text-sm"
|
||||
data-icon="chevron_right">chevron_right</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Bento Box -->
|
||||
<div class="lg:col-span-4 space-y-6">
|
||||
<div
|
||||
class="bg-surface-container-lowest p-6 rounded-xl shadow-sm border border-outline-variant border-opacity-10">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="add_box">add_box</span>
|
||||
<h3 class="text-sm font-bold text-on-surface uppercase tracking-tight">Agregar Nueva
|
||||
Equivalencia</h3>
|
||||
</div>
|
||||
<form class="space-y-6">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold text-on-surface-variant uppercase">Unidad
|
||||
Base</label>
|
||||
<select
|
||||
class="w-full bg-surface-container-low border-b border-outline-variant py-2 text-sm focus:border-primary outline-none transition-all appearance-none">
|
||||
<option>Seleccionar unidad...</option>
|
||||
<option>Caja</option>
|
||||
<option>Kilogramo</option>
|
||||
<option>Litro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label
|
||||
class="text-[10px] font-bold text-on-surface-variant uppercase">Operador</label>
|
||||
<select
|
||||
class="w-full bg-surface-container-low border-b border-outline-variant py-2 text-sm focus:border-primary outline-none transition-all appearance-none">
|
||||
<option>Multiplicar (x)</option>
|
||||
<option>Dividir (/)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label
|
||||
class="text-[10px] font-bold text-on-surface-variant uppercase">Factor</label>
|
||||
<input
|
||||
class="w-full bg-surface-container-low border-b border-outline-variant py-2 text-sm focus:border-primary outline-none transition-all"
|
||||
placeholder="1.00" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold text-on-surface-variant uppercase">Unidad
|
||||
Destino</label>
|
||||
<select
|
||||
class="w-full bg-surface-container-low border-b border-outline-variant py-2 text-sm focus:border-primary outline-none transition-all appearance-none">
|
||||
<option>Seleccionar unidad...</option>
|
||||
<option>Pieza</option>
|
||||
<option>Gramo</option>
|
||||
<option>Mililitro</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button
|
||||
class="flex-grow bg-surface-container-high text-on-surface-variant font-semibold py-2 rounded-md hover:bg-surface-dim transition-colors text-sm"
|
||||
type="button">Cancelar</button>
|
||||
<button
|
||||
class="flex-grow bg-primary text-white font-semibold py-2 rounded-md hover:opacity-90 transition-all text-sm shadow-sm"
|
||||
type="submit">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Quick Insights Bento -->
|
||||
<div
|
||||
class="bg-gradient-to-br from-secondary-container to-primary-container p-6 rounded-xl text-on-secondary-container">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest opacity-80">Info Rápida</span>
|
||||
<span class="material-symbols-outlined text-lg" data-icon="info">info</span>
|
||||
</div>
|
||||
<p class="text-sm font-medium leading-relaxed">
|
||||
Asegúrese de que las unidades base correspondan a las medidas de inventario físico para
|
||||
mantener la precisión en los conteos cíclicos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FAB for Mobile (Contextual suppression on larger screens as per logic) -->
|
||||
<button
|
||||
class="md:hidden fixed bottom-6 right-6 h-14 w-14 bg-primary text-white rounded-full shadow-2xl flex items-center justify-center z-50">
|
||||
<span class="material-symbols-outlined" data-icon="add">add</span>
|
||||
</button>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
74
src/modules/catalog/services/unit-equivalences.services.ts
Normal file
74
src/modules/catalog/services/unit-equivalences.services.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import api from '@/services/api';
|
||||
|
||||
import type {
|
||||
UnitEquivalencesListResponse,
|
||||
UnitEquivalenceResponse,
|
||||
CreateUnitEquivalenceDTO,
|
||||
UpdateUnitEquivalenceDTO,
|
||||
ConvertDTO,
|
||||
ConvertResponse,
|
||||
} from '../types/unit-equivalence.interfaces';
|
||||
|
||||
export class UnitEquivalencesService {
|
||||
public async list(params?: Record<string, any>): Promise<UnitEquivalencesListResponse> {
|
||||
try {
|
||||
const response = await api.get('/api/catalogs/unit-equivalences', { params });
|
||||
console.log('Fetched unit equivalences:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching unit equivalences:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async show(id: number): Promise<UnitEquivalenceResponse> {
|
||||
try {
|
||||
const response = await api.get(`/api/catalogs/unit-equivalences/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching unit equivalence:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async create(data: CreateUnitEquivalenceDTO): Promise<UnitEquivalenceResponse> {
|
||||
try {
|
||||
const response = await api.post('/api/catalogs/unit-equivalences', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating unit equivalence:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async update(id: number, data: UpdateUnitEquivalenceDTO): Promise<UnitEquivalenceResponse> {
|
||||
try {
|
||||
const response = await api.put(`/api/catalogs/unit-equivalences/${id}`, data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating unit equivalence:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async destroy(id: number): Promise<void> {
|
||||
try {
|
||||
await api.delete(`/api/catalogs/unit-equivalences/${id}`);
|
||||
} catch (error) {
|
||||
console.error('Error deleting unit equivalence:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async convert(data: ConvertDTO): Promise<ConvertResponse> {
|
||||
try {
|
||||
const response = await api.post('/api/catalogs/unit-equivalences/convert', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error converting units:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const unitEquivalencesService = new UnitEquivalencesService();
|
||||
78
src/modules/catalog/stores/unitEquivalenceStore.ts
Normal file
78
src/modules/catalog/stores/unitEquivalenceStore.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { unitEquivalencesService } from '../services/unit-equivalences.services';
|
||||
import type { UnitEquivalence, CreateUnitEquivalenceDTO, UpdateUnitEquivalenceDTO } from '../types/unit-equivalence.interfaces';
|
||||
|
||||
export const useUnitEquivalenceStore = defineStore('unitEquivalence', () => {
|
||||
const equivalences = ref<UnitEquivalence[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const fetchEquivalences = async (params: Record<string, any> = {}) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
const response = await unitEquivalencesService.list(params);
|
||||
equivalences.value = Array.isArray(response.data) ? response.data : [];
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading equivalences:', err);
|
||||
error.value = err instanceof Error ? err.message : 'Error loading equivalences';
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const createEquivalence = async (data: CreateUnitEquivalenceDTO) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await unitEquivalencesService.create(data);
|
||||
// refresh list
|
||||
await fetchEquivalences();
|
||||
return response.unit_equivalence;
|
||||
} catch (err) {
|
||||
console.error('Error creating equivalence:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateEquivalence = async (id: number, data: Partial<UpdateUnitEquivalenceDTO>) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await unitEquivalencesService.update(id, data);
|
||||
await fetchEquivalences();
|
||||
return response.unit_equivalence;
|
||||
} catch (err) {
|
||||
console.error('Error updating equivalence:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEquivalence = async (id: number) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await unitEquivalencesService.destroy(id);
|
||||
await fetchEquivalences();
|
||||
} catch (err) {
|
||||
console.error('Error deleting equivalence:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
equivalences,
|
||||
loading,
|
||||
error,
|
||||
fetchEquivalences,
|
||||
createEquivalence,
|
||||
updateEquivalence,
|
||||
deleteEquivalence,
|
||||
};
|
||||
});
|
||||
55
src/modules/catalog/types/unit-equivalence.interfaces.ts
Normal file
55
src/modules/catalog/types/unit-equivalence.interfaces.ts
Normal file
@ -0,0 +1,55 @@
|
||||
export interface UnitEquivalence {
|
||||
id: number;
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
product_id: number | null;
|
||||
factor: number;
|
||||
is_active: boolean;
|
||||
from_unit?: any;
|
||||
to_unit?: any;
|
||||
product?: any | null;
|
||||
operator?: string;
|
||||
}
|
||||
|
||||
export interface PaginationMeta {
|
||||
current_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UnitEquivalencesListResponse {
|
||||
data: UnitEquivalence[];
|
||||
}
|
||||
|
||||
export interface UnitEquivalenceResponse {
|
||||
unit_equivalence: UnitEquivalence;
|
||||
}
|
||||
|
||||
export interface CreateUnitEquivalenceDTO {
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
factor: number;
|
||||
is_active?: boolean;
|
||||
product_id?: number | null;
|
||||
operator?: string | null;
|
||||
}
|
||||
|
||||
export type UpdateUnitEquivalenceDTO = Partial<CreateUnitEquivalenceDTO>;
|
||||
|
||||
export interface ConvertDTO {
|
||||
value: number;
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
product_id?: number | null;
|
||||
}
|
||||
|
||||
export interface ConvertResponse {
|
||||
conversion: {
|
||||
input_value: number;
|
||||
result_value: number;
|
||||
factor_used: number;
|
||||
direction: string;
|
||||
source: string;
|
||||
unit_equivalence_id?: number;
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user